diff --git a/controller.rst b/controller.rst index 126865ba792..c93bcb0e49c 100644 --- a/controller.rst +++ b/controller.rst @@ -116,8 +116,7 @@ For more information on routing, see :doc:`/routing`. .. index:: single: Controller; Base controller class -.. _anchor-name: - :ref:`The Base Controller Classes & Services ` +.. _the-base-controller-class-services: The Base Controller Classes & Services -------------------------------------- @@ -125,20 +124,7 @@ The Base Controller Classes & Services For convenience, Symfony comes with two optional base :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` and :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController` -classes. -If you extend one or the other, this won't change anything about how your -controller works, but you'll get access to a number of **helper methods**. - -The base ``Controller`` also allows you to access the **service container** (see :ref:`controller-accessing-services`): an -array-like object that gives you access to every useful object in the -system. These useful objects are called **services**, and Symfony ships -with a service object that can render Twig templates, another that can -log messages and many more. - -On the other hand, the ``AbstractController`` prevents you from accessing the -**service container**. When you need an external dependency, this forces you to -write a code more robust as you have to explicitly define your dependencies by -using :doc:`the controller as a service `. +classes. You can extend either to get access to a number of `helper methods`_. Add the ``use`` statement atop the ``Controller`` class and then modify ``LuckyController`` to extend it:: @@ -153,11 +139,19 @@ Add the ``use`` statement atop the ``Controller`` class and then modify // ... } -Helper methods are just shortcuts to using core Symfony functionality -that's available to you with or without the use of the base -controller classes. A great way to see the core functionality in -action is to look in the -:class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` class. +That's it! You now have access to methods like :ref:`$this->render() ` +and many others that you'll learn about next. + +.. tip:: + + You can extend either ``Controller`` or ``AbstractController``. The difference + is that when you extend ``AbstractController``, you can't access services directly + via ``$this->get()`` or ``$this->container->get()``. This forces you to write + more robust code to access services. But if you *do* need direct access to the + container, using ``Controller`` is fine. + +.. versionadded:: 3.3 + The ``AbstractController`` class was added in Symfony 3.3. .. index:: single: Controller; Redirecting @@ -244,15 +238,121 @@ The Symfony templating system and Twig are explained more in the .. _controller-accessing-services: -Accessing Other Services -~~~~~~~~~~~~~~~~~~~~~~~~ +Fetching Services as Controller Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.3 + The ability to type-hint a controller argument in order to receive a service + was added in Symfony 3.3. + +Symfony comes *packed* with a lot of useful objects, called :doc:`services `. +These are used for rendering templates, sending emails, querying the database and +any other "work" you can think of. + +If you need a service in a controller, just type-hint an argument with its class +(or interface) name. Symfony will automatically pass you the service you need:: + + use Psr\Log\LoggerInterface + // ... + + /** + * @Route("/lucky/number/{max}") + */ + public function numberAction($max, LoggerInterface $logger) + { + $logger->info('We are logging!'); + // ... + } + +Awesome! + +What other services can you type-hint? To see them, use the ``debug:container`` console +command: + +.. code-block:: terminal + + $ php bin/console debug:container --types -Symfony comes packed with a lot of useful objects, called *services*. These -are used for rendering templates, sending emails, querying the database and -any other "work" you can think of. When you install a new bundle, it probably -brings in even *more* services. +If you need control over the *exact* value of an argument, you can override your +controller's service config: -When extending the base ``Controller`` class, you can access any Symfony service +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # ... + + # explicitly configure the service + AppBundle\Controller\LuckyController: + public: true + tags: + # add multiple tags to controller multiple args + - name: controller.service_arguments + action: numberAction + argument: logger + # pass this specific service id + id: monolog.logger.doctrine + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\Controller\LuckyController; + + $container->register(LuckyController::class) + ->setPublic(true) + ->addTag('controller.service_arguments', [ + 'action' => 'numberAction', + 'argument' => 'logger', + 'id' => 'monolog.logger.doctrine', + ]) + ; + +You can of course also use normal :ref:`constructor injection ` +in your controllers. + +For more information about services, see the :doc:`/service_container` article. + +.. note:: + If this isn't working, make sure your controller is registered as a service, + is :ref:`autoconfigured ` and extends either + :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller` or + :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController`. Or, + you can tag your service manually with ``controller.service_arguments``. All + of this is done for you in a fresh Symfony install. + +.. _accessing-other-services: +.. _controller-access-services-directly: + +Accessing the Container Directly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you extend the base ``Controller`` class, you can access any Symfony service via the :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::get` method. Here are several common services you might need:: @@ -262,23 +362,17 @@ method. Here are several common services you might need:: $mailer = $this->get('mailer'); -What other services exist? To list all services, use the ``debug:container`` -console command: - -.. code-block:: terminal + // you can also fetch parameters + $someParameter = $this->getParameter('some_parameter'); - $ php bin/console debug:container +If you receive an eror like: -For more information, see the :doc:`/service_container` article. - -.. tip:: +.. code-block:: text - To get a :ref:`container configuration parameter `, - use the - :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::getParameter` - method:: + You have requested a non-existent service "my_service_id" - $from = $this->getParameter('app.mailer.from'); +Check to make sure the service exists (use :ref:`debug:container `) +and that it's :ref:`public `. .. index:: single: Controller; Managing errors @@ -355,7 +449,6 @@ Symfony provides a nice session object that you can use to store information about the user between requests. By default, Symfony stores the attributes in a cookie by using native PHP sessions. - .. versionadded:: 3.3 The ability to request a ``Session`` instance in controllers was introduced in Symfony 3.3. @@ -418,20 +511,6 @@ For example, imagine you're processing a :doc:`form ` submission:: return $this->render(...); } -.. tip:: - - As a developer, you might prefer not to extend the ``Controller``. To - use the flash message functionality, you can request the flash bag from - the :class:`Symfony\\Component\\HttpFoundation\\Session\\Session`:: - - use Symfony\Component\HttpFoundation\Session\Session; - - public function indexAction(Session $session) - { - // getFlashBag is not available in the SessionInterface and requires the Session - $flashBag = $session->getFlashBag(); - } - After processing the request, the controller sets a flash message in the session and then redirects. The message key (``notice`` in this example) can be anything: you'll use this key to retrieve the message. @@ -636,12 +715,7 @@ and it's a PHP function where you can do anything in order to return the final ``Response`` object that will be returned to the user. To make life easier, you'll probably extend the base ``Controller`` class because -this gives two things: - -A) Shortcut methods (like ``render()`` and ``redirectToRoute()``); - -B) Access to *all* of the useful objects (services) in the system via the - :ref:`get() ` method. +this gives access to shortcut methods (like ``render()`` and ``redirectToRoute()``). In other articles, you'll learn how to use specific services from inside your controller that will help you persist and fetch objects from a database, process form submissions, @@ -666,4 +740,5 @@ Learn more about Controllers controller/* +.. _`helper methods`: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php .. _`unvalidated redirects security vulnerability`: https://www.owasp.org/index.php/Open_redirect diff --git a/doctrine.rst b/doctrine.rst index 398aa521b02..29c9fcedca1 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -548,6 +548,7 @@ a controller, this is pretty easy. Add the following method to the // ... use AppBundle\Entity\Product; use Symfony\Component\HttpFoundation\Response; + use Doctrine\ORM\EntityManagerInterface; // ... public function createAction() @@ -568,6 +569,12 @@ a controller, this is pretty easy. Add the following method to the return new Response('Saved new product with id '.$product->getId()); } + // you can also receive the $em as an argument + public function editAction(EntityManagerInterface $em) + { + // ... + } + .. note:: If you're following along with this example, you'll need to create a diff --git a/email.rst b/email.rst index eabb1a155af..e573bd3bbdc 100644 --- a/email.rst +++ b/email.rst @@ -100,7 +100,7 @@ The Swift Mailer library works by creating, configuring and then sending of the message and is accessible via the ``mailer`` service. Overall, sending an email is pretty straightforward:: - public function indexAction($name) + public function indexAction($name, \Swift_Mailer $mailer) { $message = \Swift_Message::newInstance() ->setSubject('Hello Email') @@ -125,7 +125,11 @@ an email is pretty straightforward:: ) */ ; - $this->get('mailer')->send($message); + + $mailer->send($message); + + // or, you can also fetch the mailer service this way + // $this->get('mailer')->send($message); return $this->render(...); } diff --git a/logging.rst b/logging.rst index 0c7ebd789c9..e46398b44de 100644 --- a/logging.rst +++ b/logging.rst @@ -10,9 +10,13 @@ Logging a Message To log a message, fetch the ``logger`` service from the container in your controller:: - public function indexAction() + use Psr\Log\LoggerInterface; + + public function indexAction(LoggerInterface $logger) { - $logger = $this->get('logger'); + // alternative way of getting the logger + // $logger = $this->get('logger'); + $logger->info('I just got the logger'); $logger->error('An error occurred'); diff --git a/service_container.rst b/service_container.rst index 7d2245ee87f..ea756eb7836 100644 --- a/service_container.rst +++ b/service_container.rst @@ -6,7 +6,7 @@ Service Container ================= Your application is *full* of useful objects: one "Mailer" object might help you -deliver email messages while another object might help you save things to the database. +send email messages while another object might help you save things to the database. Almost *everything* that your app "does" is actually done by one of these objects. And each time you install a new bundle, you get access to even more! @@ -17,46 +17,38 @@ then you can fetch a service by using that service's id:: $logger = $container->get('logger'); $entityManager = $container->get('doctrine.entity_manager'); -The container is the *heart* of Symfony: it allows you to standardize and centralize -the way objects are constructed. It makes your life easier, is super fast, and emphasizes -an architecture that promotes reusable and decoupled code. It's also a big reason -that Symfony is so fast and extensible! - -Finally, configuring and using the service container is easy. By the end -of this article, you'll be comfortable creating your own objects via the -container and customizing objects from any third-party bundle. You'll begin -writing code that is more reusable, testable and decoupled, simply because -the service container makes writing good code so easy. +The container allows you to centralize the way objects are constructed. It makes +your life easier, promotes a strong architecture and is super fast! Fetching and using Services --------------------------- -The moment you start a Symfony app, the container *already* contains many services. +The moment you start a Symfony app, your container *already* contains many services. These are like *tools*, waiting for you to take advantage of them. In your controller, -you have access to the container via ``$this->container``. Want to :doc:`log ` -something? No problem:: +you can "ask" for a service from the container by type-hinting an argument with the +service's class or interface name. Want to :doc:`log ` something? No problem:: // src/AppBundle/Controller/ProductController.php - namespace AppBundle\Controller; + // ... - use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Psr\Log\LoggerInterface; - class ProductController extends Controller + /** + * @Route("/products") + */ + public function listAction(LoggerInterface $logger) { - /** - * @Route("/products") - */ - public function listAction() - { - $logger = $this->container->get('logger'); - $logger->info('Look! I just used a service'); + $logger->info('Look! I just used a service'); - // ... - } + // ... } -``logger`` is a unique key for the ``Logger`` object. What other services are available? -Find out by running: +.. versionadded:: 3.3 + The ability to type-hint a service in order to receive it was added in Symfony 3.3. + +.. _container-debug-container: + +What other services are available? Find out by running: .. code-block:: terminal @@ -81,16 +73,39 @@ twig ``Twig_Environment`` validator ``Symfony\Component\Validator\Validator\ValidatorInterface`` =============================== ======================================================================= +You can also use the unique "Service ID" to access a service directly:: + + // src/AppBundle/Controller/ProductController.php + namespace AppBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + + class ProductController extends Controller + { + /** + * @Route("/products") + */ + public function listAction() + { + $logger = $this->container->get('logger'); + $logger->info('Look! I just used a service'); + + // ... + } + } + +:ref:`Fetching a service directly from the container ` +like this only works if you extend the ``Controller`` class. + Throughout the docs, you'll see how to use the many different services that live in the container. .. sidebar:: Container: Lazy-loaded for speed - If the container holds so many useful objects (services), does that mean those - objects are instantiated on *every* request? No! The container is lazy: it doesn't - instantiate a service until (and unless) you ask for it. For example, if you - never use the ``validator`` service during a request, the container will never - instantiate it. + Wait! Are all the services (objects) instantiated on *every* request? No! The + container is lazy: it doesn't instantiate a service until (and unless) you ask + for it. For example, if you never use the ``validator`` service during a request, + the container will never instantiate it. .. index:: single: Service Container; Configuring services @@ -100,10 +115,9 @@ in the container. Creating/Configuring Services in the Container ---------------------------------------------- -You can also leverage the container to organize your *own* code into services. For -example, suppose you want to show your users a random, happy message every time -they do something. If you put this code in your controller, it can't be re-used. -Instead, you decide to create a new class:: +You can also organize your *own* code into services. For example, suppose you need +to show your users a random, happy message. If you put this code in your controller, +it can't be re-used. Instead, you decide to create a new class:: // src/AppBundle/Service/MessageGenerator.php namespace AppBundle\Service; @@ -133,9 +147,15 @@ the service container *how* to instantiate it: # app/config/services.yml services: - app.message_generator: - class: AppBundle\Service\MessageGenerator - arguments: [] + # default configuration for services in *this* file + _defaults: + autowire: true + autoconfigure: true + public: false + + # loads services from whatever directories you want (you can update this!) + AppBundle\: + resource: '../../src/AppBundle/{Service,Command,Form,EventSubscriber,Twig,Security}' .. code-block:: xml @@ -147,64 +167,92 @@ the service container *how* to instantiate it: http://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + + + + .. code-block:: php // app/config/services.php - use AppBundle\Service\MessageGenerator; - use Symfony\Component\DependencyInjection\Definition; + // _defaults and loading entire directories is not possible with PHP configuration + // you need to define your servicess one-by-one + use AppBundle/Service/MessageGenerator; - $container->setDefinition('app.message_generator', new Definition( - MessageGenerator::class, - array() - )); + $container->autowire(MessageGenerator::class) + ->setAutoconfigured(true) + ->setPublic(false); -That's it! Your service - with the unique key ``app.message_generator`` - is now -available in the container. You can use it immediately inside your controller:: +.. versionadded:: 3.3 + The ``_defaults`` key and ability to load services from a directory were added + in Symfony 3.3. - public function newAction() - { - // ... +That's it! Thanks to the ``AppBundle\`` line and ``resource`` key below it, a service +will be registered for each class in the ``src/AppBundle/Service`` directory (and +the other directories listed). - // the container will instantiate a new MessageGenerator() - $messageGenerator = $this->container->get('app.message_generator'); +You can use it immediately inside your controller:: - // or use this shorter syntax - // $messageGenerator = $this->get('app.message_generator'); + use AppBundle\Service\MessageGenerator; + + public function newAction(MessageGenerator $messageGenerator) + { + // thanks to the type-hint, the container will instantiate a + // new MessageGenerator and pass it to you! + // ... $message = $messageGenerator->getHappyMessage(); $this->addFlash('success', $message); // ... } -When you ask for the ``app.message_generator`` service, the container constructs -a new ``MessageGenerator`` object and returns it. If you never ask for the -``app.message_generator`` service during a request, it's *never* constructed, saving -you memory and increasing the speed of your app. This also means that there's almost -no performance overhead for defining a lot of services. +When you ask for the ``MessageGenerator`` service, the container constructs a new +``MessageGenerator`` object and returns it. But if you never ask for the service, +it's *never* constructed: saving memory and speed. -As a bonus, the ``app.message_generator`` service is only created *once*: the same +As a bonus, the ``MessageGenerator`` service is only created *once*: the same instance is returned each time you ask for it. +You can also fetch a service directly from the container via its "id", which will +be its class name in this case:: + + use AppBundle\Service\MessageGenerator; + + // accessing services like this only works if you extend Controller + class ProductController extend Controller + { + public function newAction() + { + $messageGenerator = $this->get(MessageGenerator::class); + + $message = $messageGenerator->getHappyMessage(); + $this->addFlash('success', $message); + // ... + } + } + +However, this only works if you make your service :ref:`public `. + .. caution:: - Service ids are case-insensitive (e.g. ``app.message_generator`` and ``APP.Message_Generator`` - refer to the same service). But this was deprecated in Symfony 3.3. Starting - in 4.0, service ids will be case sensitive. + Service ids are case-insensitive (e.g. ``AppBundle\Service\MessageGenerator`` + and ``appbundle\service\messagegenerator`` refer to the same service). But this + was deprecated in Symfony 3.3. Starting in 4.0, service ids will be case sensitive. + +.. _services-constructor-injection: Injecting Services/Config into a Service ---------------------------------------- -What if you want to use the ``logger`` service from within ``MessageGenerator``? -Your service does *not* have a ``$this->container`` property: that's a special power -only controllers have. +What if you need to access the ``logger`` service from within ``MessageGenerator``? +Your service does *not* have access to the container directly, so you can't fetch +it via ``$this->container->get()``. -Instead, you should create a ``__construct()`` method, add a ``$logger`` argument -and set it on a ``$logger`` property:: +No problem! Instead, create a ``__construct()`` method with a ``$logger`` argument +that has the ``LoggerInterface`` type-hint:: // src/AppBundle/Service/MessageGenerator.php // ... @@ -227,13 +275,65 @@ and set it on a ``$logger`` property:: } } +That's it! The container will *automatically* know to pass the ``logger`` service +when instantiating the ``MessageGenerator``. How does it know to do this? +:doc:`Autowiring `. The key is the ``LoggerInterface`` +type-hint in your ``__construct()`` method and the ``autowire: true`` config in +``services.yml``. When you type-hint an argument, the container will automatically +find the matching service. If it can't, you'll see a clear exception with a helpful +suggestion. + +Be sure to read more about :doc:`autowiring `. + .. tip:: - The ``LoggerInterface`` type-hint in the ``__construct()`` method is optional, - but a good idea. You can find the correct type-hint by reading the docs for the - service or by using the ``php bin/console debug:container`` console command. + How should you know to use ``LoggerInterface`` for the type-hint? The best way + is by reading the docs for whatever feature you're using. You can also use the + ``php bin/console debug:container --types`` console command to get a list of + available type-hints. + +Handling Multiple Services +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you also want to email a site administrator each time a site update is +made. To do that, you create a new class:: -Next, tell the container the service has a constructor argument: + // src/AppBundle/Updates/SiteUpdateManager.php + namespace AppBundle\Updates; + + use AppBundle\Service\MessageGenerator; + + class SiteUpdateManager + { + private $messageGenerator; + private $mailer; + + public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer) + { + $this->messageGenerator = $messageGenerator; + $this->mailer = $mailer; + } + + public function notifyOfSiteUpdate() + { + $happyMessage = $this->messageGenerator->getHappyMessage(); + + $message = \Swift_Message::newInstance() + ->setSubject('Site update just happened!') + ->setFrom('admin@example.com') + ->setTo('manager@example.com') + ->addPart( + 'Someone just updated the site. We told them: '.$happyMessage + ); + $this->mailer->send($message); + + return $message; + } + } + +This uses the ``MessageGenerator`` *and* the ``Swift_Mailer`` service. To register +this as a new service in the container, simply tell your configuration to load from +the new ``Updates`` sub-directory: .. configuration-block:: @@ -241,9 +341,11 @@ Next, tell the container the service has a constructor argument: # app/config/services.yml services: - app.message_generator: - class: AppBundle\Service\MessageGenerator - arguments: ['@logger'] + # ... + + # registers all classes in Services & Updates directories + AppBundle\: + resource: '../../src/AppBundle/{Service,Updates,Command,Form,EventSubscriber,Twig,Security}' .. code-block:: xml @@ -255,61 +357,74 @@ Next, tell the container the service has a constructor argument: http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - + + + + - .. code-block:: php +Now, you can use the service immediately:: - // app/config/services.php - use AppBundle\Service\MessageGenerator; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; + use AppBundle\Updates\SiteUpdateManager; - $container->setDefinition('app.message_generator', new Definition( - MessageGenerator::class, - array(new Reference('logger')) - )); + public function newAction(SiteUpdateManager $siteUpdateManager) + { + // ... -That's it! The container now knows to pass the ``logger`` service as an argument -when it instantiates the ``MessageGenerator``. This is called dependency injection. + $message = $siteUpdateManager->notifyOfSiteUpdate(); + $this->addFlash('success', $message); + // ... + } -The ``arguments`` key holds an array of all of the constructor arguments to the -service (just 1 so far). The ``@`` symbol before ``@logger`` is important: it tells -Symfony to pass the *service* named ``logger``. +Thanks to autowiring and your type-hints in ``__construct()``, the container creates +the ``SiteUpdateManager`` object and passes it the correct argument. In most cases, +this works perfectly. -But you can pass anything as arguments. For example, suppose you want to make your -class a bit more configurable:: +Manually Wiring Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~ - // src/AppBundle/Service/MessageGenerator.php - // ... +But there are a few cases when an argument to a service cannot be autowired. For +example, suppose you want to make the admin email configurable: - use Psr\Log\LoggerInterface; +.. code-block:: diff - class MessageGenerator + // src/AppBundle/Updates/SiteUpdateManager.php + // ... + + class SiteUpdateManager { - private $logger; - private $loggingEnabled; + // ... + + private $adminEmail; - public function __construct(LoggerInterface $logger, $loggingEnabled) + - public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer) + + public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer, $adminEmail) { - $this->logger = $logger; - $this->loggingEnabled = $loggingEnabled; + // ... + + $this->adminEmail = $adminEmail; } - public function getHappyMessage() + public function notifyOfSiteUpdate() { - if ($this->loggingEnabled) { - $this->logger->info('About to find a happy message!'); - } + // ... + + $message = \Swift_Message::newInstance() + // ... + - ->setTo('manager@example.com') + + ->setTo($this->adminEmail) + // ... + ; // ... } } -The class now has a *second* constructor argument. No problem, just update your -service config: +If you make this change and refresh, you'll see an error: + + Cannot autowire service "AppBundle\Updates\SiteUpdateManager": argument "$adminEmail" + of method "__construct()" must have a type-hint or be given a value explicitly. + +That makes sense! There is no way that the container knows what value you want to +pass here. No problem! In your configuration, you can explicitly set this argument: .. configuration-block:: @@ -317,9 +432,16 @@ service config: # app/config/services.yml services: - app.message_generator: - class: AppBundle\Service\MessageGenerator - arguments: ['@logger', true] + # ... + + # same as before + AppBundle\: + resource: '../../src/AppBundle/{Service,Updates,Command,Form,EventSubscriber,Twig,Security}' + + # explicitly configure the service + AppBundle\Updates\SiteUpdateManager: + arguments: + $adminEmail: 'manager@example.com' .. code-block:: xml @@ -331,9 +453,14 @@ service config: http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - true + + + + + + + + %admin_email% @@ -341,17 +468,23 @@ service config: .. code-block:: php // app/config/services.php - use AppBundle\Service\MessageGenerator; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; - - $container->setDefinition('app.message_generator', new Definition( - MessageGenerator::class, - array(new Reference('logger'), true) - )); - -You can even leverage :doc:`environments ` to control -this new value in different situations. + use AppBundle\Updates\SiteUpdateManager; + + // _defaults and importing directories does not work in PHP + // but registering a service explicitly does + $container->autowire(SiteUpdateManager::class) + ->setAutoconfigured(true) + ->setPublic(false) + ->setArgument('$adminEmail', 'manager@example.com'); + +.. versionadded:: 3.3 + The ability to configure an argument by its name (``$adminEmail``) was added + in Symfony 3.3. Previously, you could configure it only by its index (``2`` in + this case). + +Thanks to this, the container will pass ``manager@example.com`` as the third argument +to ``__construct`` when creating the ``SiteUpdateManager`` service. The other arguments +will still be autowired. .. _service-container-parameters: @@ -368,12 +501,14 @@ and reference it with the ``%parameter_name%`` syntax: # app/config/services.yml parameters: - enable_generator_logging: true + admin_email: manager@example.com services: - app.message_generator: - class: AppBundle\Service\MessageGenerator - arguments: ['@logger', '%enable_generator_logging%'] + # ... + + AppBundle\Updates\SiteUpdateManager: + arguments: + $adminEmail: '%admin_email%' .. code-block:: xml @@ -384,14 +519,15 @@ and reference it with the ``%parameter_name%`` syntax: xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + manager@example.com + + - - true - + - - - %enable_generator_logging% + + %admin_email% @@ -399,16 +535,12 @@ and reference it with the ``%parameter_name%`` syntax: .. code-block:: php // app/config/services.php - use AppBundle\Service\MessageGenerator; - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; + use AppBundle\Updates\SiteUpdateManager; + $container->setParameter('admin_email', 'manager@example.com'); - $container->setParameter('enable_generator_logging', true); - - $container->setDefinition('app.message_generator', new Definition( - MessageGenerator::class, - array(new Reference('logger'), '%enable_generator_logging%') - )); + $container->autowire(SiteUpdateManager::class) + // ... + ->setArgument('$adminEmail', '%admin_email%'); Actually, once you define a parameter, it can be referenced via the ``%parameter_name%`` syntax in *any* other service configuration file - like ``config.yml``. Many parameters @@ -420,9 +552,11 @@ You can also fetch parameters directly from the container:: { // ... - $isLoggingEnabled = $this->container - ->getParameter('enable_generator_logging'); - // ... + // this ONLY works if you extend Controller + $adminEmail = $this->container->getParameter('admin_email'); + + // or a shorter way! + // $adminEmail = $this->getParameter('admin_email'); } .. note:: @@ -442,6 +576,268 @@ You can also fetch parameters directly from the container:: For more info about parameters, see :doc:`/service_container/parameters`. +Choose a Specific Service +------------------------- + +The ``MessageGenerator`` service created earlier requires a ``LoggerInterface`` argument:: + + // src/AppBundle/Service/MessageGenerator.php + // ... + + use Psr\Log\LoggerInterface; + + class MessageGenerator + { + private $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + // ... + } + +However, there are *multiple* services in the container that implement ``LoggerInterface``, +such as ``logger``, ``monolog.logger.request``, ``monolog.logger.php``, etc. How +does the container know which one to use? + +In these situations, the container is usually configured to automatically choose +one of the services - ``logger`` in this case (read more about why in :ref:`service-autowiring-alias`). +But, you can control this and pass in a different logger: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # ... same code as before + + # explicitly configure the service + AppBundle\Service\MessageGenerator: + arguments: + $logger: '@monolog.logger.request' + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\Service\MessageGenerator; + use Symfony\Component\DependencyInjection\Reference; + + $container->autowire(MessageGenerator::class) + ->setAutoconfigured(true) + ->setPublic(false) + ->setArgument('$logger', new Reference('monolog.logger.request')); + +This tells the container that the ``$logger`` argument to ``__construct`` should use +service whose id is ``monolog.logger.request``. + +.. tip:: + + The ``@`` symbol is important: that's what tells the container you want to pass + the *service* whose id is ``monolog.logger.request``, and not just the *string* + ``monolog.logger.request``. + +.. _services-autoconfigure: + +The autoconfigure Option +------------------------ + +.. versionadded:: 3.3 + The ``autoconfigure`` option was added in Symfony 3.3. + +Above, we've set ``autoconfigure: true`` in the ``_defaults`` section so that it +applies to all services defined in that file. With this setting, the container will +automatically apply certain configuration to your services, based on your service's +*class*. This is mostly used to *auto-tag* your services. + +For example, to create a Twig Extension, you need to create a class, register it +as a service, and :doc:`tag ` it with ``twig.extension``: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # ... + + AppBundle\Twig\MyTwigExtension: + tags: [twig.extension] + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\Twig\MyTwigExtension; + + $container->autowire(MyTwigExtension::class) + ->addTag('twig.extension'); + +But, with ``autoconfigure: true``, you don't need the tag. In fact, all you need +to do is load your service from the ``Twig`` directory, which is already loaded +by default in a fresh Symfony install: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + _defaults: + # ... + autoconfigure: true + + # load your services from the Twig directory + AppBundle\: + resource: '../../src/AppBundle/{Service,Updates,Command,Form,EventSubscriber,Twig,Security}' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use AppBundle\Twig\MyTwigExtension; + + $container->autowire(MyTwigExtension::class) + ->setAutoconfigure(true); + +That's it! The container will find your class in the ``Twig/`` directory and register +it as a service. Then ``autoconfigure`` will add the ``twig.extension`` tag *for* +you, because your class implements ``Twig_ExtensionInterface``. And thanks to ``autowire``, +you can even add constructor arguments without any configuration. + +.. _container-public: + +Public Versus Private Services +------------------------------ + +Thanks to the ``_defaults`` section in ``services.yml``, every service defined in +this file is ``public: false`` by default: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # default configuration for services in *this* file + _defaults: + # ... + public: false + + .. code-block:: xml + + + + + + + + + + + +What does this mean? When a service is **not** public, you cannot access it directly +from the container:: + + use AppBundle\Service\MessageGenerator; + + public function newAction(MessageGenerator $messageGenerator) + { + // type-hinting it as an argument DOES work + + // but accessing it directly from the container does NOT Work + $this->container->get(MessageGenerator::class); + } + +Usually, this is ok: there are better ways to access a service. But, if you *do* +need to make your service public, just override this setting: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + # ... same code as before + + # explicitly configure the service + AppBundle\Service\MessageGenerator: + public: true + + .. code-block:: xml + + + + + + + + + + + + + Learn more ---------- diff --git a/service_container/autowiring.rst b/service_container/autowiring.rst index 46103308ea2..92c30a44afe 100644 --- a/service_container/autowiring.rst +++ b/service_container/autowiring.rst @@ -223,6 +223,8 @@ the ``TransformerInterface`` and injects it automatically. Even when using interfaces (and you should), building the service graph and refactoring the project is easier than with standard definitions. +.. _service-autowiring-alias: + Dealing with Multiple Implementations of the Same Type ------------------------------------------------------