You too can become a hero and support this Blog at GitHub Sponsors or Patreon.

More about support

In the course of embracing more and more PHP standards another great improvement has come to the TYPO3 core: Dependency Injection. For a long time only the extbase micro cosmos had known a concept for injection of dependencies while the rest of the core had no such thing. During the development of version 10.0 (aka X) of TYPO3 the well-known and PSR-11 compatible symfony component for dependency injection has been integrated (Patch, btw. please read the commit message). Big shoutout to Benjamin Franzke who did the heavy lifting on this achievement. Drop him a "Thank You" at the next opportunity (or via this Tweet)!

The Concept of Dependency Injection

To quote from the Dependency Injection entry in the Wikipedia:

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A "dependency" is an object that can be used, for example a service. Instead of a client specifying which service it will use, something tells the client what service to use. The "injection" refers to the passing of a dependency [...] into the object [...] that would use it. [...] Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

In the TYPO3 world we already know this from extbase. For example a controller might need a repository class (we could call it a service as well) as a dependency to get certain data from or into the persistence layer (e.g. the database). The controller does not instantiate the repository itself, it rather gets injected into the controller by extbase. A quick recap and update regarding the dependency injection possibilities we know from extbase: We can

  • Use the @TYPO3\CMS\Extbase\Annotation\Inject annotation. The namespaced annotation was introduced in TYPO3 9.0 (Feature RST). Be aware: the old @inject is no longer supported in v10. Also be aware that this will only be possible on public properties to avoid expensive and slow runtime operations (Deprecation RST).

Extbase dependency injection through property annotation is highly discouraged and currently not supported in the system-wide solution we'll talk about in a minute. You should rather get rid of the annotations and use one of the following strategies instead!

  • Provide a inject<Dependency>() method. These methods are supported by extbase since the very beginning and are still a valid way of injecting dependencies. Looks like this:
/**
 * @var DummyService
 */
protected $dummyService;

/**
 * @param DummyService $service
 */
public function injectDummyService(DummyService $service)
{
    $this->dummyService = $service;
}
  • Specify the dependencies as constructor arguments. The above example could also be written as
/**
 * @var DummyService
 */
protected $dummyService;

/**
 * @param DummyService $service
 */
public function __construct(DummyService $service)
{
    $this->dummyService = $service;
}

The constructor could take more than one dependency of course. But we will come to this later. For now these are the options TYPO3 offers for dependency injection solely in extbase context. Outside of extbase in the rest of the core no concept of dependency injection was implemented. Classes pretty much instantiated their dependencies how and when they needed them. Usually GeneralUtility::makeInstance() was used for this.

Gladly this has now changed! <3

Let's have a look at the shiny new component for dependency injection that is made available everywhere while still being (mostly) backwards compatible with the above mentioned extbase injection strategies.

Top

The Symfony Dependency Injection Component

The documentation and the code often mentions services. That is just terminology. There is no technical limitation to what classes are included in the dependency injection. Service classes are the first thing that comes to mind as an example of what kind of classes need to be available as dependencies in other classes but it does not mean that only classes under Classes/Service/ can be injected.

The symfony/dependency-injection component provides a container for class instantiation. It is the heart of the component and the place where the dependency injection is happening. To quote the official documentation:

The container allows you to centralize the way objects are constructed. It makes your life easier, promotes a strong architecture and is super fast!

Sounds good, right? It is!

The component implements the interfaces specified in PSR-11 and therefore adding another PSR to the stack of standard recommendations TYPO3 is compatible with. But let's take a closer look:

The container only needs a little bit of configuration. We need to specify what classes should be available and a few other things. The configuration can be done in a file called Services, that is either a YAML, a XML or a PHP file. We will use YAML in the following examples. Note that within the PHP version of the configuration files more advanced things are possible. But for now let's have a look at a default version of a Services.yaml:

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    Vendor\Namespace\:
        resource: '../Classes/*'

Let's walk through this configuration.

  • namespace and resource (line 7 and 8): This specifies all classes this configuration is used for. This is already how it could look in a TYPO3 extension. In fact every system extension (that provides classes that could be injected or need dependencies injected) has this set for its own namespace. It means that all Classes under ../Classes/* will be respected during container compilation. It is also possible to provide a exclude expression if needed.
  • _defaults (line 2): This holds the default configuration for the files specified under resource. It is applied only to those classes.
  • autowire (line 3): With this set to true, type-hinting arguments is enabled in the __construct() method of classes (we will see an example later). The container will automatically inject the correct dependency into type-hinted arguments. If you want to find out more about how this works, read the section Defining Services Dependencies Automatically (Autowiring) in the official documentation. As a rule of thumb you want this in almost every case. However you can disable autowiring and wire your dependencies manually if you so prefer.
  • autoconfigure (line 4): If this is set services will automatically be configured according to certain interface implementations. For example classes that implement SingletonInterface or LoggerAwareInterface will receive a certain tag for further processing (If you are interested in the details have a look at  EXT:core/Configuration/Services.php). It is recommended to set autoconfigure to true.
  • public (line 5): This defines whether classes are available through the get() method of the container. A quote from the symfony documentation:

    As a best practice, you should only create private services, which will happen automatically. And also, you should not use the $container->get() method to fetch public services.

    But, if you do need to make a service public, override the public setting

    To make a class public is only necessary in some cases. For example Singletons need to be public but thanks to autoconfigure:true those are handled automatically. We will see an example later on where we need to turn this on. Generally speaking, you probably do not want to turn this on in the _defaults section as every setting can be overwritten on a per-class base.

OK. Now let's finally see how to use this in our TYPO3 extensions!

Top

How to use it in Extensions

As you might have thought already it is pretty simple. The above mentioned Services.yaml file lives under Configuration/Services.yaml in your extension directory. Every extension needs its own. You can also have a Services.yaml and a Services.php, both will be respected. You can start each time with the default version from above (adapt the namespace, though). In many cases that will be enough and you are good to go.

As a showcase and an example I created a small extension (GitHub) that provides a frontend plugin (extbase) and a backend module (no extbase) to demonstrate how simple the setup and usage is. Let's look at the frontend plugin with extbase first.

Top

Extbase

Extbase controller are covered by the autoconfigure: true config from the _defaults part of our Services.yaml, so nothing really to do here. The inject methods are still working like before as is the constructor injection. Let's look at an example for both:

public function __construct(
    protected readonly VeryGoodService $service,
    protected readonly FrontendUserRepository $frontendUserRepository)
{
}

protected FunctionalityInterface $functionality;

public function injectFunctionalityInterface(FunctionalityInterface $functionality): void
{
    $this->functionality = $functionality;
}

However, the @TYPO3\CMS\Extbase\Annotation\Inject annotations are not working but should be avoided anyway.

In the injectFunctionalityInterface() you see the TypeHint is an interface. In many cases it is a good practice to code against interfaces, so you might already have an interface dependency in your extbase controller because extbase supported this as well. However, there are differences on how extbases' dependency injection handles interfaces and how the symfony component does it. Let me quote the Feature RST here:

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).

So what does this mean? Let's take an example: If the ObjectManagerInterface was injected via extbase the default (meaning: nothing else was configured) implementation was concidered the interface name minus the Interface suffix. So in the example the default would have been resolved to the class ObjectManager (in the same namespace as the interface). Extbase did this regardless of how many other classes implemented the ObjectManagerInterface.

Symfony on the other hand resolves an interface automatically only in the case that just a single implementation exists regardless of the class name or namespace. As soon as there are two or more different classes implementing the same interface the container can't decide automatically what implementation to inject. Additional configuration is required. One way to do this, is to define a default implementation and then bind different implementations to specific variable names. As this is quite fascinating, let's have a look.

Imagine we have a class A and a class B both implementing the same FunctionalityInterface. With the following configuration

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  Vendor\Ext\FunctionalityInterface: '@Vendor\Ext\Functionality\A'
  Vendor\Ext\FunctionalityInterface $alternativeFunctionality: '@Vendor\Ext\Functionality\B'

This means that only if the interface is injected into a variable $alternativeFunctionality it is resolved to B, in all other cases A. To make this clear let's look at this in action (it is also included in the showcase extension):

public function __construct(
    /** This will be A */
    protected readonly FunctionalityInterface $functionality,
    /** This will be B */
    protected readonly FunctionalityInterface $alternativeFunctionality
) {
}

This pretty much concludes the dependency injection usage in an extbase context. Most important takeaway: Do not use the inject annotations anymore! Switch to inject methods or constructor injection.

Now, let's move away from extbase.

Top

Everywhere else

Here is the thing: it's pretty much the same everywhere else now. We can use constructor injection and even inject<Dependency>() methods in every class (that is included in a Services.yaml).

As an example let's register a backend module with an index action in a controller as the entry point. Just basic stuff. Up to TYPO3 v11 the registration in ext_tables.php looked like this:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModule(
    'site',
    'di',
    '',
    '',
    [
        'routeTarget' => \DanielGoerz\DiExamples\Controller\DependencyInjectionController::class . '::indexAction',
     	[...]
);

With TYPO3 v12 the registration of backend modules moved to Configuration/Backend/Modules.php. (Feature RST)

The important thing is that - when the module is clicked - the controller is instantiated by the core via GeneralUtility::makeInstance(). The dependency injection container is consulted in makeInstance() for registered classes but this depends on $container->get() and as mentioned above this only works for classes marked as public in the container configuration. Therefore we need to add the following to the Services.yaml:

services:
  [...]
  DanielGoerz\DiExamples\Controller\DependencyInjectionController:
    public: true

This overrides the public configuration just for the class DependencyInjectionController. Since this is necessary for every controller of every (non-extbase) backend module, the TYPO3 core provides a compiler pass (see "Advanced Functionality" below) to make every class tagged with backend.controller automaticaly public. So the above configuration could also be written as:

services:
  [...]
  DanielGoerz\DiExamples\Controller\DependencyInjectionController:
    tags: ['backend.controller']

If we configure our class this way it is more clear why it has been made public and it also would benefit from further magic done in the future to the tag backend.controller.

Since TYPO3 12 backend controllers should no longer be tagged. Instead every controller class should get the PHP attribute #[Controller]. Its doing the same thing but more elegant!

One way or the other, we can now use dependency injection in the controller. In the showcase extension I injected different classes, some being part of the showcase extension others being core classes. This is possible because every core system extension provides a Services.yaml file and include its classes.

Let's look at the complete showcase example where everything is used at once:

use TYPO3\CMS\Backend\Attribute\Controller;

#[Controller]
class DependencyInjectionController
{
    protected EvenBetterService $evenBetterService;
    protected VeryGoodService $veryGoodService;

    public function __construct(
        protected readonly FunctionalityInterface $functionality,
        protected readonly FunctionalityInterface $alternativeFunctionality,
        protected readonly ModuleTemplateFactory $moduleTemplateFactory
    ) {
    }

    /** @required */
    public function setEvenBetterService(EvenBetterService $service): void
    {
        $this->evenBetterService = $service;
    }

    public function injectModuleTemplate(VeryGoodService $service): void
    {
        $this->veryGoodService = $service;
    }
}

You probably noticed the method setEvenBetterService() already. With autowiring enabled the container will call every method marked with a @required annotation. So this opens up another option for automated injection.

The most important takeaway is that there is no longer a difference between extbase and non-extbase classes once you switched your extbase extension to the new dependency injection which should be very easy. If you already got rid of the inject annotations all you have to do is to create the most basic Services.yaml and you should be fine. If not then take the time, remove the annotations now and switch to the symfony dependency injection as the extbase dependency injection stuff will be gone for good at some point in time.

Generally speaking you should always prefer injecting dependencies over instantiating them yourself via GeneralUtility::makeInstance().

Please check out the whole showcase extension on GitHub to have a look at the full picture.

This whole example section was written under the assumption that you set autoconfigure: true as well as autowire: true for your extension (and convenience). Of course, you can manually wire depenencies, configure method calls, use aliases and much more. Please refer to the official documentation of the component as nearly all of this is now possible in TYPO3 as well.

Finally we gonna look at some advanced fuctionality regarding the dependency injection container.

Top

Advanced Functionality

As already mentioned above instead or in addition to a Services.yaml you could also register a Services.php file. This file is expected to return a callback function which will be executed upon container compilation to adjust, enhance or add to the configuration. Let's look at an example from the TYPO3 core as quite some things are solved with this:

<?php
namespace TYPO3\CMS\Core;

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

return function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
    $containerBuilder->registerForAutoconfiguration(SingletonInterface::class)->addTag('typo3.singleton');
    $containerBuilder->addCompilerPass(new DependencyInjection\SingletonPass('typo3.singleton'));
};

This is just a small piece from EXT:core/Configuration/Service.php to demonstrate how it works. First every class that implements the SingletonInterface is tagged with typo3.singleton. Tags can be used in other places of the configuration as well. Next we see the registration of a SingletonPass class. Compiler Passes are executed during the compilation process and offer the possibility to modify the compilation itself. It also allows to access all registered classes during compile time which is used in the SingletonPass example:

final class SingletonPass implements CompilerPassInterface
{
    private string $tagName;

    public function __construct(private readonly string $tagName)
    {
    }

    public function process(ContainerBuilder $container): void
    {
        foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
            $definition = $container->findDefinition($id);
            if (!$definition->isAutoconfigured() || $definition->isAbstract()) {
                continue;
            }

            // Singletons need to be shared (that's symfony's configuration for singletons)
            // They need to be public to be available for legacy makeInstance usage.
            $definition->setShared(true)->setPublic(true);
        }
    }
}

The process() method iterates over all classes tagged as typo3.singleton and marks them as shared and public. If autoconfiguration was turned off for such a class these steps are omitted. We can see some things we learned earlier come together here. In addition, the configuration option shared which defaults to false is set. This is done so the symfony container also treads those classes as singletons (that is what shared: true is for) and the classes can be retrieved via GeneralUtility::makeInstance() which is still used a lot in core.

Compiler passes are a flexible way to hook into the compilation of the container. If you want to find out more about this, please refer to the Feature RST where this is shown with a different example as well as to the official symfony documentation.

This concludes our journey through the new system-wide dependency injection. Big thanks again to everyone involved in getting this into the TYPO3 core!

Happy injecting!

Top

Further Information