diff --git a/_build/redirection_map b/_build/redirection_map index 66fc6db1b47..6c49dc6bed6 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -508,3 +508,4 @@ /components/mailer /mailer /messenger/message-recorder messenger/dispatch_after_current_bus /components/stopwatch https://github.com/symfony/stopwatch +/service_container/3.3-di-changes https://symfony.com/doc/3.4/service_container/3.3-di-changes.html diff --git a/service_container/3.3-di-changes.rst b/service_container/3.3-di-changes.rst deleted file mode 100644 index d57be2c0e5b..00000000000 --- a/service_container/3.3-di-changes.rst +++ /dev/null @@ -1,873 +0,0 @@ -The Symfony 3.3 DI Container Changes Explained (autowiring, _defaults, etc) -=========================================================================== - -If you look at the ``services.yaml`` file in a new Symfony 3.3 or newer project, you'll -notice some big changes: ``_defaults``, ``autowiring``, ``autoconfigure`` and more. -These features are designed to *automate* configuration and make development faster, -without sacrificing predictability, which is very important! Another goal is to make -controllers and services behave more consistently. In Symfony 3.3, controllers *are* -services by default. - -The documentation has already been updated to assume you have these new features -enabled. If you're an existing Symfony user and want to understand the "what" -and "why" behind these changes, this article is for you! - -All Changes are Optional ------------------------- - -Most importantly, **you can upgrade to Symfony 3.3 today without making any changes to your app**. -Symfony has a strict :doc:`backwards compatibility promise `, -which means it's always safe to upgrade across minor versions. - -All of the new features are **optional**: they are not enabled by default, so you -need to actually change your configuration files to use them. - -.. _`service-33-default_definition`: - -The new Default services.yaml File ----------------------------------- - -To understand the changes, look at the new default ``services.yaml`` file (this is -what the file looks like in Symfony 4): - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # default configuration for services in *this* file - _defaults: - autowire: true # Automatically injects dependencies in your services. - autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. - public: false # Allows optimizing the container by removing unused services; this also means - # fetching services directly from the container via $container->get() won't work. - # The best practice is to be explicit about your dependencies anyway. - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' - - # controllers are imported separately to make sure services can be injected - # as action arguments even if you don't extend any base controller class - App\Controller\: - resource: '../src/Controller' - tags: ['controller.service_arguments'] - - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones - - .. code-block:: xml - - - - - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return function(ContainerConfigurator $configurator) { - // default configuration for services in *this* file - $services = $configurator->services() - ->defaults() - ->autowire() // Automatically injects dependencies in your services. - ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. - ; - - // makes classes in src/ available to be used as services - // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/*') - ->exclude('../src/{Entity,Migrations,Tests}'); - - // controllers are imported separately to make sure services can be injected - // as action arguments even if you don't extend any base controller class - $services->load('App\\Controller\\', '../src/Controller') - ->tag('controller.service_arguments'); - - // add more service definitions when explicit configuration is needed - // please note that last definitions always *replace* previous ones - }; - -This small bit of configuration contains a paradigm shift of how services -are configured in Symfony. - -.. _`service-33-changes-automatic-registration`: - -1) Services are Loaded Automatically ------------------------------------- - -.. seealso:: - - Read the documentation for :ref:`automatic service loading `. - -The first big change is that services do *not* need to be defined one-by-one anymore, -thanks to the following config: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - # makes classes in src/ available to be used as services - # this creates a service per class whose id is the fully-qualified class name - App\: - resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return function(ContainerConfigurator $configurator) { - // ... - - // makes classes in src/ available to be used as services - // this creates a service per class whose id is the fully-qualified class name - $services->load('App\\', '../src/*') - ->exclude('../src/{Entity,Migrations,Tests}'); - }; - -This means that every class in ``src/`` is *available* to be used as a -service. And thanks to the ``_defaults`` section at the top of the file, all of -these services are **autowired** and **private** (i.e. ``public: false``). - -The service ids are equal to the class name (e.g. ``App\Service\InvoiceGenerator``). -And that's another change you'll notice in Symfony 3.3: we recommend that you use -the class name as your service id, unless you have :ref:`multiple services for the same class `. - - But how does the container know the arguments to my services? - -Since each service is :ref:`autowired `, the container is able -to determine most arguments automatically. But, you can always override the service -and :ref:`manually configure arguments ` or anything -else special about your service. - - But wait, if I have some model (non-service) classes in my ``src/`` - directory, doesn't this mean that *they* will also be registered as services? - Isn't that a problem? - -Actually, this is *not* a problem. Since all the new services are :ref:`private ` -(thanks to ``_defaults``), if any of the services are *not* used in your code, they're -automatically removed from the compiled container. This means that the number of -services in your container should be the *same* whether your explicitly configure -each service or load them all at once with this method. - - Ok, but can I exclude some paths that I *know* won't contain services? - -Yes! The ``exclude`` key is a glob pattern that can be used to *ignore* paths -that you do *not* want to be included as services. But, since unused services are -automatically removed from the container, ``exclude`` is not that important. The -biggest benefit is that those paths are not *tracked* by the container, and so may -result in the container needing to be rebuilt less-often in the ``dev`` environment. - -2) Autowiring by Default: Use Type-hint instead of Service id -------------------------------------------------------------- - -The second big change is that autowiring is enabled (via ``_defaults``) for all -services you register. This also means that service id's are now *less* important -and "types" (i.e. class or interface names) are now *more* important. - -For example, before Symfony 3.3 (and this is still allowed), you could pass one -service as an argument to another with the following config: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - app.invoice_generator: - class: App\Service\InvoiceGenerator - - app.invoice_mailer: - class: App\Service\InvoiceMailer - arguments: - - '@app.invoice_generator' - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - use App\Service\InvoiceGenerator; - use App\Service\InvoiceMailer; - use Symfony\Component\DependencyInjection\Reference; - - $container->register('app.invoice_generator', InvoiceGenerator::class); - $container->register('app.invoice_mailer', InvoiceMailer::class) - ->setArguments([new Reference('app.invoice_generator')]); - -To pass the ``InvoiceGenerator`` as an argument to ``InvoiceMailer``, you needed -to specify the service's *id* as an argument: ``app.invoice_generator``. Service -id's were the main way that you configured things. - -But in Symfony 3.3, thanks to autowiring, all you need to do is type-hint the -argument with ``InvoiceGenerator``:: - - // src/Service/InvoiceMailer.php - namespace App\Service; - - // ... - - class InvoiceMailer - { - private $generator; - - public function __construct(InvoiceGenerator $generator) - { - $this->generator = $generator - } - - // ... - } - -That's it! Both services are :ref:`automatically registered ` -and set to autowire. Without *any* configuration, the container knows to pass the -auto-registered ``App\Service\InvoiceGenerator`` as the first argument. As -you can see, the *type* of the class - ``App\Service\InvoiceGenerator`` - is -what's most important, not the id. You request an *instance* of a specific type and -the container automatically passes you the correct service. - - Isn't that magic? How does it know which service to pass me exactly? What if - I have multiple services of the same instance? - -The autowiring system was designed to be *super* predictable. It first works by looking -for a service whose id *exactly* matches the type-hint. This means you're in full -control of what type-hint maps to what service. You can even use service aliases -to get more control. If you have multiple services for a specific type, *you* choose -which should be used for autowiring. For full details on the autowiring logic, see :ref:`autowiring-logic-explained`. - - But what if I have a scalar (e.g. string) argument? How does it autowire that? - -If you have an argument that is *not* an object, it can't be autowired. But that's -ok! Symfony will give you a clear exception (on the next refresh of *any* page) telling -you which argument of which service could not be autowired. To fix it, you can -:ref:`manually configure *just* that one argument `. -This is the philosophy of autowiring: only configure the parts that you need to. -Most configuration is automated. - - Ok, but autowiring makes your applications less stable. If you change one thing - or make a mistake, unexpected things might happen. Isn't that a problem? - -Symfony has always valued stability, security and predictability first. Autowiring -was designed with that in mind. Specifically: - -* If there is a problem wiring *any* argument to *any* service, a clear exception - is thrown on the next refresh of *any* page, even if you don't use that service - on that page. That's *powerful*: it is *not* possible to make an autowiring mistake - and not realize it. - -* The container determines *which* service to pass in an explicit way: it looks for - a service whose id matches the type-hint exactly. It does *not* scan all services - looking for objects that have that class/interface. - -Autowiring aims to *automate* configuration without magic. - -3) Controllers are Registered as Services ------------------------------------------ - -The third big change is that, in a new Symfony 3.3 project, your controllers are *services*: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - # controllers are imported separately to make sure they're public - # and have a tag that allows actions to type-hint services - App\Controller\: - resource: '../src/Controller' - tags: ['controller.service_arguments'] - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - return function(ContainerConfigurator $configurator) { - // ... - - // controllers are imported separately to make sure they're public - // and have a tag that allows actions to type-hint services - $services->load('App\\Controller\\', '../src/Controller') - ->tag('controller.service_arguments'); - }; - - -But, you might not even notice this. First, your controllers *can* still extend -the same base controller class (``AbstractController``). -This means you have access to all of the same shortcuts as before. Additionally, -the ``@Route`` annotation and ``_controller`` syntax (e.g. ``App:Default:homepage``) -used in routing will automatically use your controller as a service (as long as its -service id matches its class name, which it *does* in this case). See :doc:`/controller/service` -for more details. You can even create :ref:`invokable controllers ` - -In other words, everything works the same. You can even add the above configuration -to your existing project without any issues: your controllers will behave the same -as before. But now that your controllers are services, you can use dependency injection -and autowiring like any other service. - -To make life even easier, it's now possible to autowire arguments to your controller -action methods, like you can with the constructor of services. For example:: - - // src/Controller/InvoiceController.php - namespace App\Controller; - - use Psr\Log\LoggerInterface; - - class InvoiceController extends AbstractController - { - public function listInvoices(LoggerInterface $logger) - { - $logger->info('A new way to access services!'); - } - } - -This is *only* possible in a controller, and your controller service must be tagged -with ``controller.service_arguments`` to make it happen. This new feature is used -throughout the documentation. - -In general, the new best practice is to use normal constructor dependency injection -(or "action" injection in controllers) instead of fetching public services via -``$this->get()`` (though that does still work). - -.. _service_autoconfigure: - -4) Auto-tagging with autoconfigure ----------------------------------- - -The fourth big change is the ``autoconfigure`` key, which is set to ``true`` under -``_defaults``. Thanks to this, the container will auto-tag services registered in -this file. For example, suppose you want to create an event subscriber. First, you -create the class:: - - // src/EventSubscriber/SetHeaderSusbcriber.php - namespace App\EventSubscriber; - - // ... - - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\HttpKernel\Event\ResponseEvent; - use Symfony\Component\HttpKernel\KernelEvents; - - class SetHeaderSusbcriber implements EventSubscriberInterface - { - public function onKernelResponse(ResponseEvent $event) - { - $event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config'); - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::RESPONSE => 'onKernelResponse' - ]; - } - } - -Great! In Symfony 3.2 or lower, you would now need to register this as a service -in ``services.yaml`` and tag it with ``kernel.event_subscriber``. In Symfony 3.3, -you're already done! - -The service is :ref:`automatically registered `. -And thanks to ``autoconfigure``, Symfony automatically tags the service because -it implements ``EventSubscriberInterface``. - - That sounds like magic - it *automatically* tags my services? - -In this case, you've created a class that implements ``EventSubscriberInterface`` -and registered it as a service. This is more than enough for the container to know -that you want this to be used as an event subscriber: more configuration is not needed. -And the tags system is its own, Symfony-specific mechanism. And you can -always set ``autoconfigure`` to ``false`` in ``services.yaml``, or disable it -for a specific service. - - Does this mean tags are dead? Does this work for all tags? - -This does *not* work for all tags. Many tags have *required* attributes, like event -*listeners*, where you also need to specify the event name and method in your tag. -Autoconfigure works only for tags without any required tag attributes, and as you -read the docs for a feature, it'll tell you whether or not the tag is needed. You -can also look at the extension classes (e.g. `FrameworkExtension for 3.3.0`_) to -see what it autoconfigures. - - What if I need to add a priority to my tag? - -Many autoconfigured tags have an optional priority. If you need to specify a priority -(or any other optional tag attribute), no problem! :ref:`Manually configure your service ` -and add the tag. Your tag will take precedence over the one added by auto-configuration. - -5) Auto-configure with _instanceof ----------------------------------- - -And the final big change is ``_instanceof``. It acts as a default definition -template (see `service-33-default_definition`_), but only for services whose -class matches a defined one. - -This can be very useful when many services share some tag that cannot be -inherited from an abstract definition: - -.. configuration-block:: - - .. code-block:: yaml - - # config/services.yaml - services: - # ... - - _instanceof: - App\Domain\LoaderInterface: - public: true - tags: ['app.domain_loader'] - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // config/services.php - namespace Symfony\Component\DependencyInjection\Loader\Configurator; - - use App\Domain\LoaderInterface; - - return function(ContainerConfigurator $configurator) { - // ... - - $services->instanceof(LoaderInterface::class) - ->public() - ->tag('app.domain_loader'); - }; - -What about Performance ----------------------- - -Symfony is unique because it has a *compiled* container. This means that there is -*no* runtime performance impact for using any of these features. That's also why -the autowiring system can give you such clear errors. - -However, there is some performance impact in the ``dev`` environment. Most importantly, -your container will likely be rebuilt more often when you modify your service classes. -This is because it needs to rebuild whenever you add a new argument to a service, -or add an interface to your class that should be autoconfigured. - -In very big projects, this may be a problem. If it is, you can always opt to *not* -use autowiring. If you think the cache rebuilding system could be smarter in some -situation, please open an issue! - -Upgrading to the new Symfony 3.3 Configuration ----------------------------------------------- - -Ready to upgrade your existing project? Great! Suppose you have the following configuration: - -.. code-block:: yaml - - # config/services.yaml - services: - app.github_notifier: - class: App\Service\GitHubNotifier - arguments: - - '@app.api_client_github' - - markdown_transformer: - class: App\Service\MarkdownTransformer - - app.api_client_github: - class: App\Service\ApiClient - arguments: - - 'https://api.github.com' - - app.api_client_sl_connect: - class: App\Service\ApiClient - arguments: - - 'https://connect.symfony.com/api' - -It's optional, but let's upgrade this to the new Symfony 3.3 configuration step-by-step, -*without* breaking our application. - -Step 1): Adding _defaults -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Start by adding a ``_defaults`` section with ``autowire`` and ``autoconfigure``. - -.. code-block:: diff - - # config/services.yaml - services: - + _defaults: - + autowire: true - + autoconfigure: true - - # ... - -You're already *explicitly* configuring all of your services. So, ``autowire`` -does nothing. You're also already tagging your services, so ``autoconfigure`` -also doesn't change any existing services. - -You have not added ``public: false`` yet. That will come in a minute. - -Step 2) Using Class Service id's -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Right now, the service ids are machine names - e.g. ``app.github_notifier``. To -work well with the new configuration system, your service ids should be class names, -except when you have multiple instances of the same service. - -Start by updating the service ids to class names: - -.. code-block:: diff - - # config/services.yaml - services: - # ... - - - app.github_notifier: - - class: App\Service\GitHubNotifier - + App\Service\GitHubNotifier: - arguments: - - '@app.api_client_github' - - - markdown_transformer: - - class: App\Service\MarkdownTransformer - + App\Service\MarkdownTransformer: ~ - - # keep these ids because there are multiple instances per class - app.api_client_github: - # ... - app.api_client_sl_connect: - # ... - -.. caution:: - - Services associated with global PHP classes (i.e. not using PHP namespaces) - must maintain the ``class`` parameter. For example, when using the old Twig - classes (e.g. ``Twig_Extensions_Extension_Intl`` instead of ``Twig\Extensions\IntlExtension``), - you can't redefine the service as ``Twig_Extensions_Extension_Intl: ~`` and - you must keep the original ``class`` parameter. - -.. caution:: - - If a service is processed by a :doc:`compiler pass `, - you could face a "You have requested a non-existent service" error. - To get rid of this, be sure that the Compiler Pass is using ``findDefinition()`` - instead of ``getDefinition()``. The latter won't take aliases into - account when looking up for services. - Furthermore it is always recommended to check for definition existence - using ``has()`` function. - -.. note:: - - If you get rid of deprecations and make your controllers extend from - ``AbstractController`` instead of ``Controller``, you can skip the rest of - this step because ``AbstractController`` doesn't provide a container where - you can get the services from. All services need to be injected as explained - in the :ref:`step 5 of this article `. - -But, this change will break our app! The old service ids (e.g. ``app.github_notifier``) -no longer exist. The simplest way to fix this is to find all your old service ids -and update them to the new class id: ``app.github_notifier`` to ``App\Service\GitHubNotifier``. - -In large projects, there's a better way: create legacy aliases that map the old id -to the new id. Create a new ``legacy_aliases.yaml`` file: - -.. code-block:: yaml - - # config/legacy_aliases.yaml - services: - _defaults: - public: true - # aliases so that the old service ids can still be accessed - # remove these if/when you are not fetching these directly - # from the container via $container->get() - app.github_notifier: '@App\Service\GitHubNotifier' - markdown_transformer: '@App\Service\MarkdownTransformer' - -Then import this at the top of ``services.yaml``: - -.. code-block:: diff - - # config/services.yaml - + imports: - + - { resource: legacy_aliases.yaml } - - # ... - -That's it! The old service ids still work. Later, (see the cleanup step below), you -can remove these from your app. - -Step 3) Make the Services Private -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now you're ready to default all services to be private: - -.. code-block:: diff - - # config/services.yaml - # ... - - services: - _defaults: - autowire: true - autoconfigure: true - + public: false - -Thanks to this, any services created in this file cannot be fetched directly from -the container. But, since the old service id's are aliases in a separate file (``legacy_aliases.yaml``), -these *are* still public. This makes sure the app keeps working. - -If you did *not* change the id of some of your services (because there are multiple -instances of the same class), you may need to make those public: - -.. code-block:: diff - - # config/services.yaml - # ... - - services: - # ... - - app.api_client_github: - # ... - - + # remove this if/when you are not fetching this - + # directly from the container via $container->get() - + public: true - - app.api_client_sl_connect: - # ... - + public: true - -This is to guarantee that the application doesn't break. If you're not fetching -these services directly from the container, this isn't needed. In a minute, you'll -clean that up. - -Step 4) Auto-registering Services -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You're now ready to automatically register all services in ``src/`` -(and/or any other directory/bundle you have): - -.. code-block:: diff - - # config/services.yaml - - services: - _defaults: - # ... - - + App\: - + resource: '../src/*' - + exclude: '../src/{Entity,Migrations,Tests}' - + - + App\Controller\: - + resource: '../src/Controller' - + tags: ['controller.service_arguments'] - - # ... - -That's it! Actually, you're already overriding and reconfiguring all the services -you're using (``App\Service\GitHubNotifier`` and ``App\Service\MarkdownTransformer``). -But now, you won't need to manually register future services. - -Once again, there is one extra complication if you have multiple services of the -same class: - -.. code-block:: diff - - # config/services.yaml - - services: - # ... - - + # alias ApiClient to one of our services below - + # app.api_client_github will be used to autowire ApiClient type-hints - + App\Service\ApiClient: '@app.api_client_github' - - app.api_client_github: - # ... - app.api_client_sl_connect: - # ... - -This guarantees that if you try to autowire an ``ApiClient`` instance, the ``app.api_client_github`` -will be used. If you *don't* have this, the auto-registration feature will try to -register a third ``ApiClient`` service and use that for autowiring (which will fail, -because the class has a non-autowireable argument). - -.. _step-5: - -Step 5) Cleanup! -~~~~~~~~~~~~~~~~ - -To make sure your application didn't break, you did some extra work. Now it's time -to clean things up! First, update your application to *not* use the old service id's (the -ones in ``legacy_aliases.yaml``). This means updating any service arguments (e.g. -``@app.github_notifier`` to ``@App\Service\GitHubNotifier``) and updating your -code to not fetch this service directly from the container. For example: - -.. code-block:: diff - - - public function index() - + public function index(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer) - { - - // the old way of fetching services - - $githubNotifier = $this->container->get('app.github_notifier'); - - $markdownTransformer = $this->container->get('markdown_transformer'); - - // ... - } - -As soon as you do this, you can delete ``legacy_aliases.yaml`` and remove its import. -You should do the same thing for any services that you made public, like -``app.api_client_github`` and ``app.api_client_sl_connect``. Once you're not fetching -these directly from the container, you can remove the ``public: true`` flag: - -.. code-block:: diff - - # config/services.yaml - services: - # ... - - app.api_client_github: - # ... - - public: true - - app.api_client_sl_connect: - # ... - - public: true - -Finally, you can optionally remove any services from ``services.yaml`` whose arguments -can be autowired. The final configuration looks like this: - -.. code-block:: yaml - - services: - _defaults: - autowire: true - autoconfigure: true - public: false - - App\: - resource: '../src/*' - exclude: '../src/{Entity,Migrations,Tests}' - - App\Controller\: - resource: '../src/Controller' - tags: ['controller.service_arguments'] - - App\Service\GitHubNotifier: - # this could be deleted, or I can keep being explicit - arguments: - - '@app.api_client_github' - - # alias ApiClient to one of our services below - # app.api_client_github will be used to autowire ApiClient type-hints - App\Service\ApiClient: '@app.api_client_github' - - # keep these ids because there are multiple instances per class - app.api_client_github: - class: App\Service\ApiClient - arguments: - - 'https://api.github.com' - - app.api_client_sl_connect: - class: App\Service\ApiClient - arguments: - - 'https://connect.symfony.com/api' - -You can now take advantage of the new features going forward. - -.. _`FrameworkExtension for 3.3.0`: https://github.com/symfony/symfony/blob/7938fdeceb03cc1df277a249cf3da70f0b50eb98/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L247-L284 diff --git a/setup/flex.rst b/setup/flex.rst index df1bf1eaa14..eaba102d073 100644 --- a/setup/flex.rst +++ b/setup/flex.rst @@ -100,7 +100,7 @@ manual steps: located at ``config/services.yaml``. Copy the contents of the `default services.yaml file`_ and then add your own service configuration. Later you can revisit this file because thanks to Symfony's - :doc:`autowiring feature ` you can remove + :doc:`autowiring feature ` you can remove most of the service configuration. .. note::