Breaking: #110578 - SingletonInterface removed from core services 

See forge#110578

Description 

\TYPO3\CMS\Core\SingletonInterface 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 GeneralUtility::makeInstance() 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 SingletonInterface anymore:

  • \TYPO3\CMS\Adminpanel\Service\ConfigurationService
  • \TYPO3\CMS\Adminpanel\Service\ProcessedImageCollector
  • \TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection
  • \TYPO3\CMS\Core\Cache\CacheManager
  • \TYPO3\CMS\Core\Console\CommandRegistry
  • \TYPO3\CMS\Core\Error\AbstractExceptionHandler
  • \TYPO3\CMS\Core\Localization\Locales
  • \TYPO3\CMS\Core\Messaging\FlashMessageService
  • \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry
  • \TYPO3\CMS\Core\PageTitle\PageTitleProviderManager
  • \TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry
  • \TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry
  • \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry
  • \TYPO3\CMS\Core\Resource\TextExtraction\TextExtractorRegistry
  • \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
  • \TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter
  • \TYPO3\CMS\Extbase\Service\ExtensionService
  • \TYPO3\CMS\Extbase\Validation\ValidatorResolver
  • \TYPO3\CMS\Install\Service\SessionService
  • \TYPO3\CMS\Scheduler\Scheduler
  • \TYPO3\CMS\Workspaces\Service\Dependency\CollectionService

GeneralUtility::setSingletonInstance() and GeneralUtility::removeSingletonInstance() only accept SingletonInterface instances and raise a TypeError for these classes. This mostly affects tests that substitute one of them.

Every type converter extending AbstractTypeConverter is affected as well, since the marker was inherited. Converters registered with the extbase.type_converter 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 GeneralUtility::makeInstance() or $container->get() fails with an ArgumentCountError or a ServiceNotFoundException . 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 GeneralUtility::makeInstance() or the container, or that replace one of them in tests via GeneralUtility::setSingletonInstance() .

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:

EXT:my_extension/Classes/Service/MyService.php
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');
    }
}
Copied!

The same applies to tests that replaced one of these classes globally with GeneralUtility::setSingletonInstance() . 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.

SingletonInterface 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:

EXT:my_extension/Classes/Service/MyLegacyService.php
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true)]
final class MyLegacyService
{
}
Copied!