Unser Ziel ist es, ein Feature als TYPO3 Extension zu entwickeln, welches aber so unabhängig von TYO3 wie möglich sein sollte.
Es soll somit möglich sein das Feature zukünftig auch in “nicht-TYPO3” Umgebungen verwenden zu können ohne alles neu schreiben zu müssen.

Der Plan ist also, das Feature als Composer Package zu entwickeln und eine TYPO3 Extension zu erstellen, die als Art “Adapter” dient um die Funktionen in TYPO3 nutzen zu können.

Aber warum sollte man das machen?

Dafür kann es viele Gründe geben, mein initialer Use-Case war es eine Erweiterung für ein TYPO3 System zu entwickeln, welche in Zukunft einfach in ein eigenständiges Symfony Projekt übertragen werden soll.

Was ist die Idee?

Der Wunsch ist die folgende Ordner-Struktur:

<typo3-root>
├── packages
│   ├── typo3-extension
│   │   ├── Classes
│   │   ├── Configuration
│   │   │   └── Services.php           <- DI Konfiguration für "typo3-extension"
│   │   └── ext_emconf.php
│   └── feature
│       ├── src
│       │   ├── <Services and Classes>  <- Klassen des "Core" Features
│       │   └── Resources
│       │       └── config
│       │           └── service.xml     <- DI Konfiguration für "feature"
│       └── tests                       
├── public
├── copmoser.json
├── ...

Die Struktur des feature Packages erinnert hier an ein Symfony Bundle, dies ist aber nicht erforderlich, macht für meinen Use-Case hier aber Sinn da alle Entwickler an dem Projekt mit Symfony Bundles (außerhalb von TYPO3) vertraut sind.

Es sollen alle für das Feature relevanten Klassen (wie DTOs und Services) aus dem Feature “Bundle” kommen und die TYPO3 Extension lediglich die Adaptierung auf TYPO3 Frontend Plugins und API Routen implementieren.

Dependency Injection

Seit Version 10 hat TYPO3 Dependency Injection auf Basis von symfony/dependency-injection an Board.

Der Plan ist nun, die src/Resources/congig/services.xml Datei aus dem Bundle zu laden, sodass wir sowohl wir dependency injection sowohl innerhalb des Bundles als auch der TYPO3 Implementierung verwenden können.

Symfony Bundles machen dies mit einer sog. Extension für den Container Builder also schreiben wir auch eine.

Extensions liegen in Symfony Bundles in src/DependencyInjection, daher lege ich dieser auch dort an.

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Extension\Extension;

class FeatureExtension extends Extension implements CompilerPassInterface
{
    public function load(array $configs, ContainerBuilder $container): void
    {
        $this->process($container);
    }

    public function process(ContainerBuilder $container)
    {
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . "/../Resources/config"));

        $loader->load("services.xml");
    }
}

Wichtig ist hierbei, dass wir sowohl die Klasse Extension erweitern als auch das CompilerPassInterface implementieren.

Die Basis-Klasse Extension wird benötigt, sodass wir die Extension im Container Builder registrieren können. Die eigentliche Ausführung der Extension passiert dann aber als CompilerPass, hierfür wird das CompilerPassInterface benötigt.

Das alleine sorgt aber leider nicht magisch dafür, dass die Services im DI-Container verfügbar sind.
Wir müssen TYPO3 noch sagen, dass die Extension geladen werden soll, dies geht am einfachsten mit der Configuration/Services.php Datei.

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

return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void {
    $containerBuilder->registerExtension(new FeatureExtension());
};

Verwenden der registrieren Services

Nun, da wir die Services in der Dependency-Injection registriert haben, können wir diese tatsächlich genau so verwenden wie Services, die direkt aus einer TYPO3-Extension kommen.

Mehr darüber erfährst du in der TYPO3 Dokumentation zur Dependency-Injection