.. include:: /Includes.rst.txt .. _feature-84112: =================================================================== Feature: #84112 - Symfony dependency injection for core and extbase =================================================================== See :issue:`84112` Description =========== The PHP library `symfony/dependency-injection` has been integrated and is used to manage system wide dependency management and injection for classes. With the integration provided in TYPO3 the symfony dependency injection container features support for Extbase and non-Extbase classes and is thus intended to replace the Extbase dependency injection container and object manager. The symfony container implements :php:`\Psr\Container\ContainerInterface` as specified by PSR-11. This interface should be used when requiring access to the container. Therefore :php:`\TYPO3\CMS\Extbase\Object\ObjectManager` now resorts to this new dependency injection container and prioritizes its entries over classical Extbase dependency injection (which is still available), also :php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()` has been adapted to retrieve instances from the container, if possible. Classes should be adapted to avoid both, :php:`ObjectManager` and :php:`GeneralUtility::makeInstance()` whenever possible. Service dependencies should be injected via constructor injection or setter methods (inject methods as in Extbase are supported). Configuration ^^^^^^^^^^^^^ Extensions are encouraged to configure their classes to make use of the new dependency injection. A symfony flavored yaml (or, for advanced functionality, php) service configuration file may be used to do so. That means symfony dependency injection is not applied automatically, extensions need to define the desired dependency injection strategies. Extensions that do not configure dependency injection will keep working – the legacy instance management in :php:`\TYPO3\CMS\Extbase\Object\ObjectManager` and :php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()` is still available. Whenever service configuration or class dependencies change, the core cache needs to be flushed to rebuild the compiled symfony container. Autowiring ---------- A :file:`Configuration/Services.yaml` which uses autowiring pretty much reflects the current feature set of Extbase DI. The configuration looks like: .. code-block:: yaml # Configuration/Services.yaml services: _defaults: autowire: true autoconfigure: true public: false Your\Namespace\: resource: '../Classes/*' Extensions which have used Extbase dependency injection in the past, will want to enable :yaml:`autowire` for a smooth migration. :yaml:`autowire: true` instructs symfony to calculate the required dependencies from type declarations of the constructor and inject methods. This calculation yields to a service initialization recipe which is cached in php code (in TYPO3 core cache). Note: An extension doesn't need to use autowiring, it is free to manually wire dependencies in the service configuration file. It is suggested to enable :yaml:`autoconfigure: true` as this will automatically add symfony service tags based on implemented interfaces or base classes. An Example: autoconfiguration ensured that classes which implement :php:`\TYPO3\CMS\Core\SingletonInterface` will be publicly available from the symfony container (which is required for legacy singleton lookups through :php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()`). :yaml:`public: false` is a performance optimization and is therefore suggested to be enabled in extensions (symfony does not enable this by default for backwards compatibility reasons only). This settings controls which services are available through :php:`\Psr\Container\ContainerInterface->get()`. Services that need to be public (e.g. Singletons, because they need to be shared with legacy code that uses :php:`\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()`, or Extbase controllers) will be marked public automatically due to :yaml:`autoconfigure: true` by custom TYPO3 provided symfony compiler passes. Manual wiring ------------- Manual dependency wiring and service configuration can be used instead of autowiring (it can actually be combined). This speeds up container compilation and allows for custom service configuration/wiring. It has the drawback of having to write some boilerplate. .. code-block:: yaml # Configuration/Services.yaml services: _defaults: autoconfigure: false public: false Your\Namespace\Service\ExampleService: # mark public – means this service should be accessible from $container->get() # and (often more important), both GeneralUtility::makeInstance() and the Extbase # ObjectManager will be able to use the Symfony DI managed service public: true # Defining a service to be shared is equal to TYPO3's SingletonInterface behaviour shared: true # Configure constructor arguments arguments: $siteConfiguration: '@TYPO3\CMS\Core\Configuration\SiteConfiguration' # Example Extbase controller Your\Namespace\Controller\ExampleController: # mark public to be dispatchable public: true # Defining to be a prototype, as Extbase controllers are stateful (i. e. could not be defined as singleton) shared: false # Configure constructor arguments arguments: $exampleService: '@Your\Namespace\Service\ExampleService' For more information please refer to the official documentation: https://symfony.com/doc/4.3/service_container.html Advanced functionality ---------------------- Container compilation and configuration can be enhanced using a callback function returned from :file:`Configuration/Services.php`. Here is an example: Given an interface :php:`MyCustomInterface`, you can automatically add a symfony tag for (autoconfigured) services that implement this interface. A compiler pass can use that tag and configure autoregistration into a registry service :php:`MyRegistry`. .. code-block:: php # Configuration/Services.php registerForAutoconfiguration(MyCustomInterface::class)->addTag('my.custom.interface'); $containerBuilder->addCompilerPass(new DependencyInjection\MyCustomPass('my.custom.interface')); }; .. code-block:: php # Classes/DependencyInjection/MyCustomPass.php findDefinition(MyRegistry::class); foreach ($container->findTaggedServiceIds('my.custom.interface') as $id => $tags) { $definition = $container->findDefinition($id); if (!$definition->isAutoconfigured() || $definition->isAbstract()) { continue; } // Services that implement MyCustomInterface need to be public, // to be lazy loadable by the registry via $container->get() $container->findDefinition($id)->setPublic(true); // Add a method call to the registry class to the (auto-generated) factory for // the registry service. // This supersedes explicit registrations in ext_localconf.php (which're // still possible and can be combined with this autoconfiguration). $myRegistry->addMethodCall('registerMyCustomInterfaceImplementation', [$id]); } } } Impact ====== * Symfony automatically resolves interfaces to classes when only one class implementing an interface is available. Otherwise an explicit alias is required. That means you SHOULD define an alias for interface to class mappings where the implementation currently defaults to the interface minus the trailing Interface suffix (which is the default for Extbase). * Dependency Injection can be added to constructors of existing services without being breaking. :php:`GeneralUtility::makeInstance(ServiceName::class)` will keep working, as :php:`makeInstance` has been adapted to resort to the symfony container. * Cyclic dependencies are not supported with Symfony DI (Extbase DI did so). * Prototypes/Data classes (non singletons, e.g. models) that need both, runtime constructor arguments (as passed to :php:`\TYPO3\CMS\Extbase\Object\ObjectManager->get()`) and injected dependencies are not supported in :php:`\Psr\Container\ContainerInterface->get()`. It is suggested to switch to factories or stick with the object manager for now. .. index:: PHP-API, ext:core