Feature: #84112 - Symfony dependency injection for core and extbase¶
See forge#84112
Description¶
The PHP library symfony/
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\
as specified by PSR-11. This interface should be used when requiring access
to the container.
Therefore \TYPO3\
now resorts to this
new dependency injection container and prioritizes its entries over classical
Extbase dependency injection (which is still available), also
\TYPO3\
has been adapted
to retrieve instances from the container, if possible.
Classes should be adapted to avoid both, Object
and
General
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\
and
\TYPO3\
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/
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\
will be publicly available from the
symfony container (which is required for legacy singleton lookups through
\TYPO3\
).
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\
. Services that need to be public
(e.g. Singletons, because they need to be shared with legacy code that uses
\TYPO3\
, 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/
.
Here is an example: Given an interface My
,
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 My
.
# 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.
General
will keep working, asUtility:: make Instance (Service Name:: class) make
has been adapted to resort to the symfony container.Instance - 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\
) and injected dependencies are not supported inCMS\ Extbase\ Object\ Object Manager->get () \Psr\
. It is suggested to switch to factories or stick with the object manager for now.Container\ Container Interface->get ()