Darstellung
Service Container
Dingfelder\AdForm\Core\Container ist ein bewusst kleiner, PSR-11-inspirierter Container. Er kennt kein Auto-Wiring und keine Reflection: Jede Abhängigkeit wird in einer Factory-Closure explizit aufgelöst. Dadurch bleibt das Plugin ohne Composer-Runtime-Pakete installierbar.
Interner Aufbau
Der Container hält vier Maps:
| Property | Inhalt |
|---|---|
$bindings | id → Factory-Closure |
$instances | id → bereits aufgelöstes Objekt |
$shared | id → ob das Ergebnis gecacht wird |
$resolving | id → aktuell in Auflösung (Zirkel-Erkennung) |
Als id wird durchgängig ein Klassen- oder Interface-Name über ::class verwendet. Freie Strings sind möglich, aber im Kern nicht gebräuchlich.
API
singleton()
php
public function singleton( string $id, callable $factory ): voidRegistriert eine geteilte Factory. Sie läuft beim ersten get(), danach wird immer dieselbe Instanz zurückgegeben.
php
$container->singleton(
MergeTagEngine::class,
static fn (): MergeTagEngine => new MergeTagEngine()
);Die Factory bekommt den Container als einzigen Parameter übergeben und kann darüber weitere Services auflösen:
php
$container->singleton(
ActionRunner::class,
static fn ( Container $container ): ActionRunner => new ActionRunner(
$container->get( ActionRegistry::class ),
$container->get( LoggerInterface::class ),
$container->get( ConditionEngineInterface::class )
)
);bind()
php
public function bind( string $id, callable $factory ): voidRegistriert eine transiente Factory: Jeder get()-Aufruf erzeugt ein neues Objekt. Ein zuvor gecachtes Ergebnis für dieselbe id wird dabei verworfen.
php
$container->bind(
ReportBuilder::class,
static fn (): ReportBuilder => new ReportBuilder()
);
$a = $container->get( ReportBuilder::class );
$b = $container->get( ReportBuilder::class );
// $a !== $bFür Services ist singleton() der Normalfall. bind() eignet sich für zustandsbehaftete Wegwerf-Objekte.
instance()
php
public function instance( string $id, mixed $instance ): voidHinterlegt ein bereits existierendes Objekt. Es wird sofort als geteilt markiert, es läuft keine Factory. Genau so registriert sich das Plugin selbst:
php
private function __construct() {
$this->container = new Container();
$this->container->instance( self::class, $this );
$this->container->instance( Container::class, $this->container );
}Praktisch auch, um im Test eine Fake-Implementierung einzusetzen:
php
$container->instance( MailerInterface::class, new CapturingMailer() );has()
php
public function has( string $id ): booltrue, wenn für die id eine Factory oder eine Instanz hinterlegt ist. has() löst nichts auf und hat keine Nebenwirkungen. Der richtige Weg, um optional vorhandene Services zu benutzen:
php
if ( $container->has( MyAddon\LicenseGuard::class ) ) {
$container->get( MyAddon\LicenseGuard::class )->check();
}get()
php
public function get( string $id ): mixedAuflösungsreihenfolge:
- Existiert ein Eintrag in
$instances, wird er direkt zurückgegeben. - Fehlt eine Factory, wirft
get()eineContainerException::not_found(). - Ist die
idbereits in Auflösung, wirftget()eineContainerException::circular(). - Die Factory läuft; das
resolving-Flag wird in einemfinally-Block wieder entfernt, auch wenn die Factory eine Exception wirft. - War das Binding geteilt, wird das Ergebnis in
$instancesgecacht.
php
try {
$service = $container->get( FormService::class );
} catch ( ContainerException $exception ) {
// Service ist nicht gebunden oder es liegt ein Zirkel vor.
}Fehlerbehandlung
Dingfelder\AdForm\Core\ContainerException erweitert RuntimeException und implementiert das Marker-Interface Dingfelder\AdForm\Exception\PluginException. Damit lassen sich alle Plugin-Fehler mit einem catch abfangen:
php
use Dingfelder\AdForm\Exception\PluginException;
try {
$container->get( SomeService::class );
} catch ( PluginException $exception ) {
error_log( $exception->getMessage() );
}Es gibt zwei Named Constructors:
| Aufruf | Nachricht |
|---|---|
ContainerException::not_found( $id ) | Service "…" is not bound in the AD Form container. |
ContainerException::circular( $id ) | Circular service dependency detected for "…". |
Zirkuläre Abhängigkeiten vermeiden
Ein Zirkel entsteht, wenn zwei Factories sich gegenseitig auflösen:
php
// Führt bei get( A::class ) zu ContainerException::circular().
$container->singleton( A::class, fn ( Container $c ) => new A( $c->get( B::class ) ) );
$container->singleton( B::class, fn ( Container $c ) => new B( $c->get( A::class ) ) );Lösung: den Container (oder eine Closure) statt der Instanz injizieren und erst zur Laufzeit auflösen – oder besser, die gemeinsame Verantwortung in einen dritten Service ziehen.
Zugriff auf den Container
Der Container ist über die Plugin-Instanz erreichbar:
php
use Dingfelder\AdForm\Plugin;
$container = Plugin::instance()->container();Innerhalb von Hooks kommt er meist direkt als Parameter an – bei ad_form_register_services und ad_form_rest_routes.
Eigene Services binden
ad_form_register_services feuert, nachdem alle Kern-Provider ihre Bindings gesetzt haben und bevor irgendein Provider bootet. Das ist der richtige Zeitpunkt für Add-on-Bindings.
php
<?php
declare(strict_types=1);
namespace Acme\AdFormCrm;
use Dingfelder\AdForm\Core\Container;
use Dingfelder\AdForm\Logging\LoggerInterface;
add_action(
'ad_form_register_services',
static function ( Container $container ): void {
$container->singleton(
CrmClient::class,
static fn (): CrmClient => new CrmClient(
(string) get_option( 'acme_crm_api_key', '' )
)
);
$container->singleton(
CrmSync::class,
static fn ( Container $container ): CrmSync => new CrmSync(
$container->get( CrmClient::class ),
$container->get( LoggerInterface::class )
)
);
}
);Später zugreifen:
php
use Dingfelder\AdForm\Forms\Form;
use Dingfelder\AdForm\Plugin;
use Dingfelder\AdForm\Submissions\Submission;
add_action(
'ad_form_after_submission',
static function ( Submission $submission, Form $form ): void {
Plugin::instance()->container()->get( CrmSync::class )->push( $submission, $form );
},
10,
2
);Kern-Bindings ersetzen
Weil singleton() ein bestehendes Binding überschreibt, lässt sich jede Interface-Bindung austauschen – solange das noch vor dem ersten get() passiert. ad_form_register_services ist genau dieses Zeitfenster:
php
use Dingfelder\AdForm\Core\Container;
use Dingfelder\AdForm\Notifications\MailerInterface;
add_action(
'ad_form_register_services',
static function ( Container $container ): void {
$container->singleton(
MailerInterface::class,
static fn (): MailerInterface => new Acme\PostmarkMailer()
);
},
20
);Nur Interfaces ersetzen
Sinnvoll austauschbar sind die Interface-Bindings: OptionsInterface, LoggerInterface, DatabaseInterface, SchemaInstallerInterface, MailerInterface, FormRepositoryInterface, ConditionEngineInterface, CalculationEngineInterface, FormRendererInterface. Konkrete Klassen zu ersetzen bricht die Typ-Hints der Kern-Services.
Was der Container nicht kann
- Kein Auto-Wiring.
get( Foo::class )ohne vorheriges Binding wirft, auch wenn die Klasse existiert. - Keine kontextabhängigen Bindings, keine Tags, keine Scopes über den Request hinaus.
- Kein Container-Reset. Ein aufgelöstes Singleton bleibt für den Request bestehen; nur
bind()verwirft eine gecachte Instanz.