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 \Psr\Container\ContainerInterface as specified by PSR-11. This interface should be used when requiring access to the container. Therefore \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 \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance() has been adapted to retrieve instances from the container, if possible.

Classes should be adapted to avoid both, ObjectManager and 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 \TYPO3\CMS\Extbase\Object\ObjectManager and \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 Configuration/Services.yaml which uses autowiring pretty much reflects the current feature set of Extbase DI. The configuration looks like:

# 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 autowire for a smooth migration. 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 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 \TYPO3\CMS\Core\SingletonInterface will be publicly available from the symfony container (which is required for legacy singleton lookups through \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()).

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 \Psr\Container\ContainerInterface->get(). Services that need to be public (e.g. Singletons, because they need to be shared with legacy code that uses \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(), or Extbase controllers) will be marked public automatically due to 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.

# 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 Configuration/Services.php. Here is an example: Given an interface 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 MyRegistry.

# Configuration/Services.php
<?php
declare(strict_types = 1);
namespace Your\Namespace;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
    $containerBuilder->registerForAutoconfiguration(MyCustomInterface::class)->addTag('my.custom.interface');

    $containerBuilder->addCompilerPass(new DependencyInjection\MyCustomPass('my.custom.interface'));
};
# Classes/DependencyInjection/MyCustomPass.php
<?php
declare(strict_types = 1);
namespace Your\Namespace\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Your\Namespace\MyRegistry;

final class MyCustomPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $myRegistry = $container->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. GeneralUtility::makeInstance(ServiceName::class) will keep working, as 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 \TYPO3\CMS\Extbase\Object\ObjectManager->get()) and injected dependencies are not supported in \Psr\Container\ContainerInterface->get(). It is suggested to switch to factories or stick with the object manager for now.