Dependency injection
Overview of page contents
Abstract
This chapter explains "Dependency Injection (DI)" as used in TYPO3. Readers interested in the general concepts and principles may want to look at, for example, Dependency Injection in "PHP The Right Way" and What is dependency injection? by Fabien Potencier. Whenever a service has a service dependency to another class, the technique of dependency injection should be used to satisfy that need. TYPO3 uses a Symfony component for dependency injection. The component is PSR-11 compliant, and is used throughout core and extensions to standardize the process of obtaining service dependencies.
By default all API services shipped with the TYPO3 Core system extensions offer dependency injection. The recommended usage is constructor injection. Available as well are method injection and interface injection. To activate the Symfony component for dependency injection a few lines of configuration are necessary.
Introduction
The title of this chapter is "dependency injection" (DI), but the scope is a bit broader: In general, this chapter is about TYPO3 object lifecycle management and how to obtain services, with one sub-part of it being dependency injection.
The underlying interfaces are based on the PHP-FIG (PHP Framework Interop Group) standard PSR-11 Container interface, and the implementation is based on Symfony service container and Symfony dependency injection components, plus some TYPO3 specific sugar.
Background and history
Obtaining object instances in TYPO3 has always been straightforward: Call
General
and hand over
mandatory and optional __
arguments as additional arguments.
There are two quirks to that:
- First, a class instantiated through
General
can implementUtility:: make Instance () Singleton
. This empty interface definition tells makeInstance() to instantiate the object exactly once for this request, and if another makeInstance() call asks for it, the same object instance is returned - otherwise makeInstance() always creates a new object instance and returns it. Implementing SingletonInterface is nowadays considered old-fashioned, its usage should be reduced over time.Interface - Second,
General
allows "XCLASSing". This is a - rather dirty - way to substitute a class with a custom implementation. XCLASS'ing in general is brittle and seen as a last resort hack if no better solution is available. In connection with Symfony containers, XCLASSing services should be avoided in general and service overrides should be used instead. Replacing the XCLASS functionality in TYPO3 is still work in progress. In contrast, XCLASSing is still useful for data objects, and there is no good other solution yet.Utility:: make Instance ()
Using make
worked very well for a long time. It however lacked a feature
that has been added to the PHP world after make
had been invented: Dependency injection.
There are lots of articles about dependency injection on the net, so we won't go too deep
here but rather explain the main idea: The general issue appears when classes follow the
separation of concerns
principle.
One of the standard examples is logging. Let's say a class's responsibility is the creation of users - it checks everything and finally writes a row to database. Now, since this is an important operation, the class wants to log an information like "I just created a user". And this is where dependency injection enters the game: Logging is a huge topic, there are various error levels, information can be written to various destinations and so on. The little class does not want to deal with all those details, it just wants to tell the framework: "Please give me some logger I can use and that takes care of all details, I don't want to know about them". This separation is the heart of single responsibility and separation of concerns.
Dependency injection does two things for us here: First, it allows separating concerns, and second, it hands the task of finding an appropriate implementation of a dependency over to the framework, so the framework decides - based on configuration - which specific instance is given to the consumer. Note in our example, the logging instance itself may have dependencies again - the process of object creation and preparation may be nested.
In more abstract software engineering terms: Dependency injection is a pattern used to delegate the task of resolving class dependencies away from a consumer towards the underlying framework.
Back to history: After make
has been around for quite a while and lacked an
implementation of dependency injection, Extbase appeared in 2009. Extbase brought a first container
and dependency injection solution, it's main interface being the Extbase Object
.
The Extbase object manager has been widely used for a long time, but suffered from some
issues younger approaches don't face. One of the main drawbacks of Extbase object manager
is the fact that it's based on runtime reflection: Whenever an object is to be instantiated,
the object manager scans the class for needed injections and prepares dependencies to be
injected. This process is quite slow though mitigated by various caches. And these also
come with costs. In the end, these issues have been the main reason the object manager
was never established as a main core concept but only lived in Extbase scope.
The object lifecycle and dependency injection solution based on Symfony DI has been
added in TYPO3v10 and is a general core concept: Next to the native
dependency injection, it is also wired into make
as a long living
backwards compatibility solution, and it fully substitutes the Extbase object manager. In
contrast to the Extbase solution, Symfony based object management does not have the
overhead of
expensive runtime calculations. Instead it is an instance wide build-time solution: When
TYPO3 bootstraps, all object creation details of all classes are read from
a single cache file just once, and afterwards no expensive calculation is required
for actual creation.
Symfony based DI was implemented in TYPO3 v10 and usage of the Extbase ObjectManager was discouraged.
The Extbase ObjectManager has been removed with TYPO3 v12. Making use of
Symfony DI integration continues. There
are still various places in the core to be improved. Further streamlining is
done over time. For instance, the final fate of make
and the Singleton
has not fully been decided on. Various
tasks remain to be solved in younger TYPO3 development to further improve the
object lifecycle management provided by the core.
Build-time caches
To get a basic understanding of the core's lifecycle management it is helpful to get a rough insight on the main construct. As already mentioned, object lifecycle management is conceptualized as steps to take place at build-time. It is done very early and only once during the TYPO3 bootstrap process. All calculated information is written to a special cache that can not be reconfigured and is available early. On subsequent requests the cache file is loaded. Extensions can not mess with the construct if they adhere to the core API.
Besides being created early, the state of the container is independent and exactly the same in frontend, backend and CLI scope. The same container instance may even be used for multiple requests. This is becoming more and more important nowadays with the core being able to execute sub requests. The only exception to this is the Install Tool: It uses a more basic container that "cannot fail". This difference is not important for extension developers however since they can't hook into the Install Tool at those places.
The Symfony container implementation is usually configured to actively scan the
extension classes for needed injections. All it takes are just a couple
of lines within the Services.
file. This should be done within all extensions that
contain PHP classes and it is the fundamental setup we will outline in the following sections.
For developers, it is important to understand that dealing with Symfony DI is an early core bootstrap and cached thing. The system will fail upon misconfiguration, leading to unreachable frontend and backend.
Attention
Errors in the DI configuration may block frontend and backend!
The DI cache does not heal by itself but needs to be cleared manually!
With the container cache entry being a low level early bootstrap thing that is expensive to calculate when it has to be rebuild, there is a limited list of options to flush this cache:
- The container cache entry is not deleted when a backend user clicks "Flush all caches" in the backend top toolbar if the instance is configured as production application. For developer convenience, the container cache is flushed in development context, though.
- The container cache is flushed using "Admin tools" -> "Maintenance" -> "Flush Caches" of the Install Tool.
- The container cache is flushed using the CLI command
vendor/
. Usingbin/ typo3 cache: flush vendor/
afterwards will rebuild and cache the container.bin/ typo3 cache: warmup - The container cache is automatically flushed when using the Extension Manager to load or unload extensions in (non-Composer) classic mode.
- Another way to quickly drop this cache during development is to remove all
var/
files, which reside incache/ code/ di/* typo3temp/
in classic mode instances or elsewhere in composer mode instances (see Environment).
The main takeaway is: When a developer fiddles with container configuration,
the cache needs to be manually cleared. And if some configuration issue slipped in,
which made the container or DI calculation fail, the system does not heal itself and
needs both a fix of the Dependency injection
configuration plus probably a cache removal. The standalone Install
Tool however should always work, even if the backend breaks down, so the "Flush caches"
button is always reachable. Note that if the container calculation fails, the
var/
files contain the exception with backtrace!
Important terms
We will continue to use a couple of technical terms in this chapter, so let's quickly define them to align. Some of them are not precisely used in our world, for instance some Java devs may stumble upon "our" understanding of a prototype.
- Prototype
- The broad understanding of a prototype within the TYPO3 community is
that it's simply an object that is created anew every time. Basically the direct
opposite of a singleton. In fact, the prototype pattern describes a base object that
is created once, so
__
is called to set it up, after that it is cloned each time one wants to have a new instance of it. The community isn't well aware of that, and the core provides no "correct" prototype API, so the word prototype is often misused for an object that is always created anew when the framework is asked to create one.construct () - Singleton
- A singleton is an object that is instantiated exactly once within one request, with
the same instance being re-used when a class instance is requested. Symfony understands
such class instances as being "shared". TYPO3 can also declare a class as "shared" using the
Singleton
, but this is considered old-fashioned. Services are usually declared shared by default. This implies such classes should be stateless and there is trouble ahead when they are not.Interface - Service
- We use the understanding "What is a service?" from Symfony: In Symfony, everything
that is instantiated through the service container is a service. These are many things - for instance
controllers are services, as well as - non static - utilities, repositories and classes like mailers
and similar. To emphasize: Not only classes named with a
*Service
suffix are services but basically anything as long as it is not a data object. A class should represent either-or: A class is either a service that manipulates or does something with given data and does not hold it, or is a class that holds data. Sadly, this distinction is not always the case within TYPO3 core (yet), and there are many classes that blend service functionality and data characteristics. - Data object
- Data objects are the opposite of services. They are not available
through service containers. Calling
$container->has
returns() false
and they can not be injected. They should be instantiated usingnew
. Domain models and DTOs are a typical example of data objects. Data objects are not "service container aware" and do not support DI. Although the TYPO3 core does not strictly follow this rule in all cases until now, the ambition is to get this done over time.()
Using DI
The general idea is: Whenever your service class has a service dependency to another class, dependency injection should be used.
In some TYPO3 APIs dependency injection cannot be used yet. This applies to classes that need
specific data in their constructors or classes that are serialized and deserialized as, for
example, scheduler tasks. The TYPO3 core tries to refactor these cases over time. Such classes
need to fall back to old-school General
There are two ways proclaimed and natively supported by TYPO3 to obtain service dependencies:
Constructor injection using __
and method injection using
inject*
methods. Constructor injection is the way to go as long as a class
is not dealing with complex abstract inheritance chains. The symfony service container
can inject specific classes as well as instances of interfaces.
Constructor injection
Lets say we're writing a controller that renders a list of users. Those users are
found using a User
service, making the user repository service a
direct dependency of the controller service. A typical constructor dependency injection
to resolve the dependency by the framework looks like this:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Repository\UserRepository;
final class UserController
{
public function __construct(
private readonly UserRepository $userRepository,
) {}
}
The symfony container setup process will now see User
as a dependency
of User
when scanning its __
method. Since autowiring
is enabled by default (more on that below), an instance of the User
is
created and provided to __
when the controller is created. The instance
is set as a class property using constructor
property promotion
and the property is declared readonly
.
Method injection
A second way to get services injected is by using inject*
methods:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Repository\UserRepository;
final class UserController
{
private ?UserRepository $userRepository = null;
public function injectUserRepository(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
}
This ends up with basically the same result as above: The controller instance retrieves
an object of type User
in class property $user
. The service
container calls such inject*
methods directly after class instantiation, so after
__
has been executed, and before anything else.
The injection via methods was introduced by Extbase. TYPO3 core implemented it in addition to
the default Symfony constructor injection. Why did we do that, you may ask? Both
strategies have subtle differences: First, when using inject*
methods, the type
hinted class property needs to be nullable, otherwise PHP >= 7.4 throws a warning
since the instance is not set during __
. But that's just an
implementation detail. More important is an abstraction scenario. Consider this case:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Logger\Logger;
use MyVendor\MyExtension\Repository\UserRepository;
abstract class AbstractController
{
protected ?Logger $logger = null;
public function injectLogger(Logger $logger)
{
$this->logger = $logger;
}
}
final class UserController extends AbstractController
{
public function __construct(
private readonly UserRepository $userRepository,
) {}
}
We have an abstract controller service with a dependency plus a controller service that extends the abstract and has further dependencies.
Now assume the abstract class is provided by TYPO3 core and the consuming class
is provided by an extension. If the abstract class would use constructor injection,
the consuming class would need to know the dependencies of the abstract, add its
own dependencies to the constructor, and then call parent::__
to
satisfy the abstracts dependency. This would hardcode all dependencies
of the abstract into extending classes. If later the abstract is changed and
another dependency is added to the constructor, this would break consuming
classes.
Differently put: When core classes "pollute" __
with dependencies,
the core can not add dependencies without being breaking. This is the reason why
for example the extbase Abstract
uses inject*
methods for its
dependencies: Extending classes can then use constructor injection, do not need
to call parent::__
, and the core is free to change dependencies of
the abstract.
In general, when the core provides abstract classes that are expected to be
extended by extensions, the abstract class should use inject*
methods instead of
constructor injection. Extensions of course can follow this idea in similar
scenarios.
This construct has some further implications: Abstract classes should
think about making their dependency properties private
, so extending classes
can not rely on them. Furthermore, classes that should not be extended by extensions
are free to use constructor injection and should be marked final
, making
sure they can't be extended to allow internal changes.
Interface injection
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Logger\LoggerInterface;
final class UserController extends AbstractController
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
}
Notice the difference? The code requests the injection of an interface and not a class! This is permissible with both constructor and method injection. It compels the service container to determine which specific class is configured as the implementation of the interface and inject an instance of that class. A class can declare itself as the default implementation of such an interface. This is the essence of dependency injection: a consuming class no longer relies on a specific implementation but on an interface's signature.
The framework ensures that something fulfilling the interface is injected. The consuming class remains unaware of the specific implementation, focusing solely on the interface methods. An instance administrator can configure the framework to inject a different implementation, either globally or for specific classes. The consumer remains unconcerned, interacting only with the interface methods.
The example below has a couple of controller classes as service consumers. There
is a service interface with a default implementation. The default implementation
uses the symfony PHP attribute As
to register itself as default.
A Services.
file configures different service implementation for
some service consumers:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Service\MyServiceInterface;
class MyFirstController
{
public function __construct(
private readonly MyServiceInterface $myService,
) {}
}
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Service\MyServiceInterface;
class MySecondController
{
public function __construct(
private readonly MyServiceInterface $myService,
) {}
}
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Service\MyServiceInterface;
class MyThirdController
{
private MyServiceInterface $myService;
public function injectMyService(MyServiceInterface $myService): void
{
$this->myService = $myService;
}
}
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
interface MyServiceInterface
{
public function foo();
}
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(MyServiceInterface::class)]
class MyDefaultServiceImplementation implements MyServiceInterface
{
public function foo()
{
// do something
}
}
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
class MyOtherServiceImplementation implements MyServiceInterface
{
public function foo()
{
// do something
}
}
services:
_defaults:
autowire: true
autoconfigure: true
public: false
# Within MySecondController and MyThirdController different implementations
# than the default MyDefaultServiceImplementation of MyServiceInterface
# shall be injected.
# When working with constructor injection
MyVendor\MyExtension\Controller\MySecondController:
arguments:
$service: '@MyVendor\MyExtension\Service\MyOtherServiceImplementation'
# When working with method injection
MyVendor\MyExtension\Controller\MyThirdController:
calls:
- method: 'injectMyService'
arguments:
$service: '@MyVendor\MyExtension\Service\MyOtherServiceImplementation'
Configuration
Services.yaml
declaring service defaults
Extensions have to configure their classes to make use of
dependency injection. This can be done in Configuration/Services.yaml
.
Alternatively, Configuration/Services.php
can also be used, if
PHP syntax is required to apply conditional logic to definitions.
A basic Services.
file of an extension looks like the following.
Note
Whenever the service configuration or class dependencies change, the Core cache must be flushed, see above for details.
services:
_defaults:
autowire: true
autoconfigure: true
public: false
MyVendor\MyExtension\:
resource: '../Classes/*'
exclude: '../Classes/Domain/Model/*'
- autowire
-
autowire: true
instructs the dependency injection component to calculate the required dependencies from type declarations. The calculation generates service initialization code.An extension is not required to use autowiring. It can manually wire dependencies. However, opting out of autowiring is less convenient and is not further documented in this guide.
- autoconfigure
-
This directive instructs the dependency injection component to automatically add Symfony service tags based on implemented interfaces and base classes. For instance, autoconfiguration ensures that classes implementing
Singleton
are publicly available from the Symfony container and marked as shared (Interface shared: true
).TYPO3 dependency injection relies on this this for various default configurations. It is recommended to set
autoconfigure: true
. - public
public: false
is a performance optimization and should therefore be set in extensions. This settings controls which services are available through the dependency injection container used internally byGeneral
. See "What to make public?" for more information.Utility:: make Instance () - Model exclusion
- The path exclusion
exclude: '../
excludes your models from the dependency injection container: You cannot inject them nor inject dependencies into them. Models are not services but data objects and therefore should not require dependency injection. Also, these objects are usually created by the Extbase persistence layer, which does not support the DI container.Classes/ Domain/ Model/*'
Autoconfiguration using attributes and Services.yaml
Single service classes may need to change auto configuration
to be different than above declared defaults. This can be done using PHP attributes.
The most common use case is public: true
:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
/**
* This service is instantiated using GeneralUtility::makeInstance()
* in some cases, which requires 'public' being set to 'true'.
*/
#[Autoconfigure(public: true)]
readonly class MyServiceUsingAutoconfigurePublicTrue
{
public function __construct(
private SomeDependency $someDependency,
) {}
}
The above usage of the Autoconfigure
attribute declares this service as
public: true
which overrides a public: false
default from a
Services.
file for this specific class.
Similar with shared: false
:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
/**
* This service is stateful and configures the service container to
* inject new instances to consuming services when they are instantiated.
*/
#[Autoconfigure(shared: false)]
class MyServiceUsingAutoconfigureSharedFalse
{
private string $foo = 'foo';
public function __construct(
private readonly SomeDependency $someDependency,
) {}
public function setFoo(): void
{
$this->foo = 'bar';
}
}
It is possible to set both using #
.
The Autoconfigure
attribute is beneficial when an extension includes a
service class that is either stateful or instantiated using
General
. This attribute embeds the configuration
directly within the class file, eliminating the need for additional entries in
Services.
- the configuration is "in place".
To reconfigure "foreign" services - those not provided by the extension itself
but by another extension (such as a service class from ext:core) - the
Services.
file can be utilized. A common scenario is when a core
service is not declared public because all core extensions retrieve instances
via constructor or method injection, rather than
General
. If an extension must use
General
for a specific reason, it can declare
the "foreign" service as "public" in Services.
:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
MyVendor\MyExtension\:
resource: '../Classes/*'
# Declare a "foreign" service "public: true" since this extension needs
# to instantiate the service using GeneralUtility::makeInstance() and
# the service is configured "public: false" by the extension delivering
# that service.
TYPO3\CMS\Core\Some\Service:
public: true
Autowiring using attributes
Autowiring, particularly the Autowire
PHP attribute, is a powerful tool
for making dependency injection more convenient and transparent. TYPO3 core
includes default configurations that facilitate its use. Let’s explore some
examples.
Consider a service performing an expensive operation that caches the result
within the TYPO3 runtime cache to avoid repeating the operation within the same
request. The runtime cache, being a dependent service, should be injected. A
naive approach is to inject the core Cache
and retrieve the
runtime cache instance:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
class MyServiceUsingCacheManager
{
private FrontendInterface $runtimeCache;
public function __construct(
CacheManager $cacheManager,
) {
$this->runtimeCache = $cacheManager->getCache('runtime');
}
public function calculateSomethingExpensive()
{
// do something using runtime cache
}
}
This can be simplified, resulting in more streamlined code:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
class MyServiceGettingRuntimeCacheInjected
{
public function __construct(
#[Autowire(service: 'cache.runtime')]
private readonly FrontendInterface $runtimeCache,
) {}
public function calculateSomethingExpensive()
{
// do something using runtime cache
}
}
The "cache.runtime" service alias, configured by the TYPO3 core extension,
performs the Cache
operation behind the scenes.
Utilizing such shortcuts simplifies the consumers.
The autowire attribute also enables the execution of expressions and injection of the results, which is useful for "compile-time" state that remains constant during requests. For example, to inject a feature toggle status:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class MyServiceGettingFeatureToggleResultInjected
{
public function __construct(
#[Autowire(expression: 'service("features").isFeatureEnabled("myExtension.foo")')]
private readonly bool $fooEnabled,
) {}
}
Another example, including alias definition, is new in TYPO3 v13. It enables
injecting values from ext_
files using the
Extension
API.
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Core\Configuration;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias('extension-configuration', public: true)]
class ExtensionConfiguration
{
public function get(string $extension, string $path = ''): mixed
{
// implementation
}
}
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class MyServiceGettingExtensionConfigurationValueInjected
{
public function __construct(
#[Autowire(expression: 'service("extension-configuration").get("my_extension", "something.isEnabled")')]
private readonly bool $somethingIsEnabled,
) {}
}
This example demonstrates the combination of a service class with an alias and
a consumer utilizing this alias in an Autowire
attribute.
The TYPO3 core provides a couple such service aliases, with the above ones being the most important ones for extension developers. TYPO3 core does not arbitrarily add aliases.
Installation-wide configuration
A global service configuration for a project can be set up to be utilized across multiple project-specific extensions. This allows, for example, the aliasing of an interface with a concrete implementation that can be used in several extensions. Additionally, project-specific CLI commands can be registered without the need for a project-specific extension.
However, this is only possible - due to security restrictions - if TYPO3 is configured such that the project root is outside the document root, which is typically the case in Composer-based installations.
In Composer-based installations, the global service configuration files
services.
and services.
are located within the
config/
directory of a TYPO3 project.
Consider the following scenario: You want to use the PSR-20 clock interface as
a type hint for dependency injection in the service classes of your project's
various extensions. This setup allows the concrete implementation to change
without altering your code. In this example, we use lcobucci/
as the
concrete implementation.
The global files services.
and services.
are read before
files from extensions. The global files can provide defaults but can not override
service definitions from service configuration files loaded afterwards.
The concrete clock implementation is now injected when a type hint to the interface is given:
FAQ
What to make public?
Attention
In short: "Manually" instantiated services using
General
must be made
public.
The basic difference between public and private is well explained in the symfony documentation:
When defining a service, it can be made to be public or private. If a service is public, it means that you can access it directly from the container at runtime. For example, the doctrine service is a public service:
// only public services can be accessed in this way $doctrine = $container->get('doctrine');
Copied!But typically, services are accessed using dependency injection. And in this case, those services do not need to be public.
So unless you specifically need to access a service directly from the container via
$container->get
, the best-practice is to make your services private.()
The implementation of General
utilizes $container->get
.
As a result, services instantiated using make
must be declared public if they have
dependencies that need to be injected.
Services without dependencies can be instantiated using make
without
the service made public, as they are instantiated using new
without constructor
arguments.
Some services are automatically declared public by basic TYPO3 dependency injection
configuration since they are instantiated using make
by the core
framework. The most common ones are:
- Extbase controllers implementing
Controller
, usually by inheritingInterface Action
. They are additionally declaredController shared: false
. -
Backend controllers with
As
class attribute. They are additionally declaredController shared: false
:use TYPO3\CMS\Backend\Attribute\AsController; #[AsController] final readonly class MyBackendController { // implementation }
Copied! - Classes implementing
Singleton
Interface - Fluid view helpers implementing
\View
. They are additionally declaredHelper Interface shared: false
. - Fluid data processors tagged with data.processor.
Examples of classes that must be made public:
Services that use dependency injection and are not declared public typically error
out with typical messages when instantiated using make
They should
be declared public:
(1/1) ArgumentCountError
Too few arguments to function MyVendor\MyExtension\Namespace\Class::__construct(),
0 passed in typo3/sysext/core/Classes/Utility/GeneralUtility.php on line 3461 and exactly 1 expected
(1/1) Error
Call to a member function methodName() on null
When to use GeneralUtility::makeInstance()
?
Attention
In short: Use General
to obtain instances of stateful
services within otherwise stateless services.
Ideally, all services in a framework are stateless: They depend on other stateless services and are always retrieved using dependency injection.
TYPO3 core development is gradually transitioning more services to be stateless.
However, many historically stateful services still exist. The critical point is that
injecting a stateful service into a stateless service makes the consumer stateful as
well. This can create a chain of coupled stateful services, leading to unexpected
results when these services are reused multiple times within a single request. While
declaring a service shared: false
can mitigate the issue, it doesn't solve the
underlying problem. This scenario is a primary use case for
General
. Instead of injecting a stateful service at
service build time and reusing it frequently, the service can use
make
at runtime when it needs a service instance.
For instance, the Data
class should
create new instances for each use, as it becomes "tainted" after use and cannot reset
its state properly. Such "dirty-after-use" services should be instantiated anew with
make
when needed.
Some services are stateful but provide workarounds to be injectable. A good example
is the Extbase Uri
. It is
stateful due to its use of the method chaining pattern but includes a reset
method to reset its state. When used correctly, this service can be injected and
reused. Additionally, Uri
is declared shared: false
, so
consumers receive distinct instances, reducing the risk of bugs from improper use of
reset
.
Various solutions exist to make existing services stateless. For instance, the extbase
Uri
could deprecate its set
chaining methods and introduce a
Uri
data object, which would be passed to the service worker methods.
Implementing such changes in the TYPO3 core codebase is an ongoing process that requires
careful consideration.
Deciding whether to use make
instead of dependency injection
requires examining the dependency's behavior. Consider these factors:
- The service class is declared
readonly
and only declares stateless dependencies in__
.construct () - The service has no class properties.
- All
__
arguments are services and declaredconstruct () readonly
. __
requires no manual non-service arguments.construct ()
The last point is particularly relevant: Some TYPO3 core services expect state to be
passed to __
, making them stateful and unsuitable for injection,
as dependency injection cannot handle consumer state. These services must be
instantiated using make
until their constructors are updated to be
compatible with dependency injection.
When to use new
?
Attention
In short: Use new
to instantiate data objects, not services.
Services should be always retrieved using dependency injection. If that is not
feasible because the dependent service is stateful or because the class is created
using a "polluted" constructor with manual arguments, it should be created using
make
. While services without dependencies could be instantiated
with new
, this approach has drawbacks: It introduces risks if the service
is later modified to include dependencies and bypasses the XCLASS mechanism and
potential service overrides by configuration.
Only data objects - preferably using
public constructor property promotion -
should be instantiated using the PHP keyword new
.
Mix manual constructor arguments and service dependencies?
Attention
In short: No. For good reason.
A service can not mix manual constructor arguments with service dependencies
handled by dependency injection. Manual constructor arguments make services stateful.
When a service is instantiated with manual arguments, such as
$my
,
dependency injection is bypassed, and any other service dependencies in the
constructor are ignored. Mixing both blends the roles of services and data objects,
which is poor PHP architecture.
The extbase-based dependency injection solution using Object
allowed such mixtures, but this has been replaced by the Symfony-based
dependency injection solution, which does not support this practice.
What about user functions?
It is possible to use dependency injection when calling custom user functions,
for example .userFunc within TypoScript or
in (legacy) hooks, usually via
\TYPO3\
.
call
internally uses the dependency-injection-aware
helper General
, which can recognize and inject
services that are marked public.
What about injection in a XCLASS'ed class?
When extending an existing class (for example, an Extbase controller) using
XCLASS and injecting additional dependencies using constructor
injection, ensure that a reference to the extended class is added in the
Configuration/Services.yaml
file of the extending extension, as shown in
the example below:
TYPO3\CMS\Belog\Controller\BackendLogController: '@MyVendor\MyExtension\Controller\ExtendedBackendLogController'
Not yet exemplified
- This document does not currently elaborate on Symfony service providers, although the TYPO3 core uses them in various places. Use cases for these should be outlined.
- The concept and usage of "lazy" services are not discussed.
- Solutions to cyclic dependencies should be explored. Cyclic dependencies
occur when services depend on each other, forming a graph instead of a
tree, which Symfony's dependency injection cannot resolve. One solution
is to make one side lazy, although this is not the primary use of
"lazy." Another approach involves using a factory with an interface,
as demonstrated in
ext:
.styleguide
Further information
- Symfony dependency injection component
- Symfony service container
- Dependency Injection in TYPO3 blog article by Daniel Goerz
- Dependency Injection in "PHP The Right Way"
- What is dependency injection? by Fabien Potencier