Breaking: #110578 - SingletonInterface removed from core services
See forge#110578
Description
\TYPO3\ predates dependency injection.
Today it has exactly one effect: it turns a class into a shared and
public service in the dependency injection container, so that the legacy
General can still reach it.
Both properties are already the default for injected services. Every service in the container is shared, so every class that receives a service through constructor injection gets the very same instance - that is what the marker used to guarantee. A service only needs to be public on top of that if it is pulled out of the container by hand.
The marker has therefore been removed from a number of Core services that are resolved through dependency injection anyway. Their lifetime does not change: they are still shared.
Impact
These classes do not implement
Singleton anymore:
\TYPO3\CMS\ Adminpanel\ Service\ Configuration Service \TYPO3\CMS\ Adminpanel\ Service\ Processed Image Collector \TYPO3\CMS\ Backend\ View\ Backend Layout\ Data Provider Collection \TYPO3\CMS\ Core\ Cache\ Cache Manager \TYPO3\CMS\ Core\ Console\ Command Registry \TYPO3\CMS\ Core\ Error\ Abstract Exception Handler \TYPO3\CMS\ Core\ Localization\ Locales \TYPO3\CMS\ Core\ Messaging\ Flash Message Service \TYPO3\CMS\ Core\ Meta Tag\ Meta Tag Manager Registry \TYPO3\CMS\ Core\ Page Title\ Page Title Provider Manager \TYPO3\CMS\ Core\ Resource\ Online Media\ Helpers\ Online Media Helper Registry \TYPO3\CMS\ Core\ Resource\ Processing\ Task Type Registry \TYPO3\CMS\ Core\ Resource\ Rendering\ Renderer Registry \TYPO3\CMS\ Core\ Resource\ Text Extraction\ Text Extractor Registry \TYPO3\CMS\ Extbase\ Persistence\ Generic\ Qom\ Query Object Model Factory \TYPO3\CMS\ Extbase\ Property\ Type Converter\ Abstract Type Converter \TYPO3\CMS\ Extbase\ Service\ Extension Service \TYPO3\CMS\ Extbase\ Validation\ Validator Resolver \TYPO3\CMS\ Install\ Service\ Session Service \TYPO3\CMS\ Scheduler\ Scheduler \TYPO3\CMS\ Workspaces\ Service\ Dependency\ Collection Service
General and
General only accept
Singleton instances and raise a
Type for these
classes. This mostly affects tests that substitute one of them.
Every type converter extending
Abstract is affected as
well, since the marker was inherited. Converters registered with the
extbase. tag stay public, because the tag makes them
public on its own.
Some of these services are not public anymore either, so fetching them
with
General or
$container->get
fails with an
Argument or a
Service. Whether a Core service is public is an
implementation detail and must not be relied upon.
Affected installations
Installations with extensions that fetch one of the listed classes through
General or the container, or that replace one
of them in tests via
General.
Migration
Inject the service instead of fetching it. Since services are shared, the injected instance is the same one the rest of the request uses:
use TYPO3\CMS\Core\Localization\Locales;
final readonly class MyService
{
public function __construct(
private Locales $locales,
) {}
public function doSomething(): void
{
// instead of GeneralUtility::makeInstance(Locales::class)
$this->locales->createLocale('de');
}
}
The same applies to tests that replaced one of these classes globally with
General. Hand the dependency to the
subject instead: a stub or a mock if the test only needs the collaborator
to be there, or a functional test if it should exercise the real one.
This is cleaner and framework-agnostic: the subject is built with plain PHP, and everything it works with is visible in the test itself, instead of being smuggled in through a global registry that the code under test happens to read from.
Singleton itself is not deprecated and keeps working for
custom classes. If a class of your own cannot use dependency injection -
typically because it is instantiated with constructor arguments, which
bypasses the container - it can also be made public explicitly, without
the marker:
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(public: true)]
final class MyLegacyService
{
}