Skip to content

Service Provider

Ein Service Provider bündelt alles, was ein Modul braucht: die Container-Bindings und die WordPress-Hooks. AD Form registriert dreizehn Kern-Provider; Add-ons hängen ihre eigenen über einen Filter an.

Der Vertrag

php
<?php
declare(strict_types=1);

namespace Dingfelder\AdForm\Core;

interface ServiceProviderInterface {

	/**
	 * Bind services into the container.
	 */
	public function register( Container $container ): void;

	/**
	 * Hook into WordPress after all services are registered.
	 */
	public function boot( Container $container ): void;
}

Beide Methoden geben void zurück und bekommen denselben Container.

register() vs. boot()

Der Unterschied ist keine Stilfrage, sondern die Bedingung dafür, dass Provider sich gegenseitig sehen können. Plugin::boot() durchläuft erst alle register()-Methoden, danach alleboot()-Methoden.

register()boot()
ZweckBindings setzenAuf WordPress reagieren
Erlaubtsingleton(), bind(), instance()get(), add_action(), add_filter(), register_*()
Nicht erlaubtget() auf fremde Bindings, add_action()neue Bindings, die andere Provider erwarten
Zustand danachAlle Kern-Bindings stehenPlugin ist einsatzbereit

Kein get() in register()

In register() ist nicht garantiert, dass ein Binding aus einem anderen Provider bereits existiert – die Reihenfolge innerhalb der Phase ist Implementierungsdetail. Abhängigkeiten werden deshalb innerhalb der Factory-Closure aufgelöst, die erst beim späteren get() läuft.

Richtig:

php
public function register( Container $container ): void {
	$container->singleton(
		CrmSync::class,
		// Läuft erst beim get() – zu dem Zeitpunkt sind alle Bindings da.
		static fn ( Container $container ): CrmSync => new CrmSync(
			$container->get( LoggerInterface::class )
		)
	);
}

Falsch:

php
public function register( Container $container ): void {
	// Läuft sofort. LoggerInterface ist eventuell noch nicht gebunden.
	$logger = $container->get( LoggerInterface::class );

	$container->singleton( CrmSync::class, static fn (): CrmSync => new CrmSync( $logger ) );
}

Beispiele aus dem Kern

Der kleinste Provider bindet genau einen Service und tut in boot() nichts:

php
final class ConditionsServiceProvider implements ServiceProviderInterface {

	public function register( Container $container ): void {
		$container->singleton(
			ConditionEngineInterface::class,
			static fn (): ConditionEngineInterface => new ConditionEngine()
		);
	}

	public function boot( Container $container ): void {
		unset( $container );
	}
}

unset( $container ); ist dabei kein Zufall: Es macht den unbenutzten Parameter explizit und hält die Coding-Standards-Prüfung ruhig.

Ein Provider mit Hook-Registrierung – hier wird der ActionRunner an das Ende der Submission gehängt:

php
public function boot( Container $container ): void {
	$container->get( ActionRegistry::class )->boot();
	$runner = $container->get( ActionRunner::class );

	add_action(
		Hooks::AFTER_SUBMISSION,
		static function ( mixed $submission, mixed $form ) use ( $runner ): void {
			if ( ! $submission instanceof Submission || ! $form instanceof Form ) {
				return;
			}

			$runner->run( $form, $submission );
		},
		10,
		2
	);
}

Ein Provider, der nur im Backend arbeitet, steigt früh aus:

php
public function boot( Container $container ): void {
	if ( ! is_admin() ) {
		return;
	}

	$container->get( FormsPage::class )->boot();
	// …
}

Eigenen Provider registrieren

Der Filter ad_form_service_providers bekommt die Liste der Kern-Provider und muss eine Liste zurückgeben. Plugin filtert anschließend alles heraus, was kein ServiceProviderInterface ist – ein Rückgabefehler führt also nicht zum Fatal Error, sondern zum stillen Ignorieren.

php
add_filter(
	'ad_form_service_providers',
	static function ( array $providers ): array {
		$providers[] = new Acme\AdFormCrm\CrmServiceProvider();

		return $providers;
	}
);

Timing

Der Filter läuft in Plugin::boot() auf plugins_loaded mit Priorität 5. Der add_filter()- Aufruf muss also beim Laden der Add-on-Datei passieren, nicht erst in einem späteren Hook.

Anhängen statt Ersetzen: $providers[] = … erhält die Kern-Reihenfolge. Ein eigener Provider läuft dadurch immer nach allen Kern-Providern – in register() wie in boot().

Vollständiges Beispiel

php
<?php
/**
 * CRM add-on for AD Form.
 *
 * @package Acme\AdFormCrm
 */

declare(strict_types=1);

namespace Acme\AdFormCrm;

use Dingfelder\AdForm\Core\Container;
use Dingfelder\AdForm\Core\Hooks;
use Dingfelder\AdForm\Core\ServiceProviderInterface;
use Dingfelder\AdForm\Forms\Form;
use Dingfelder\AdForm\Logging\LoggerInterface;
use Dingfelder\AdForm\Submissions\Submission;

final class CrmServiceProvider implements ServiceProviderInterface {

	public function register( 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 )
			)
		);
	}

	public function boot( Container $container ): void {
		$sync = $container->get( CrmSync::class );

		add_action(
			Hooks::AFTER_SUBMISSION,
			static function ( mixed $submission, mixed $form ) use ( $sync ): void {
				if ( ! $submission instanceof Submission || ! $form instanceof Form ) {
					return;
				}

				$sync->push( $form, $submission );
			},
			10,
			2
		);
	}
}

Und die Registrierung in der Haupt-Datei des Add-ons:

php
<?php
declare(strict_types=1);

add_filter(
	'ad_form_service_providers',
	static function ( array $providers ): array {
		$providers[] = new Acme\AdFormCrm\CrmServiceProvider();

		return $providers;
	}
);

Wann ein Provider – und wann nicht

Ein eigener Provider lohnt sich, sobald mehr als ein Service beteiligt ist oder Bindings und Hook-Registrierung zusammengehören.

Für einen einzelnen Service reicht ad_form_register_services:

php
add_action(
	'ad_form_register_services',
	static function ( Container $container ): void {
		$container->singleton( MyService::class, static fn (): MyService => new MyService() );
	}
);

Für reines Verhalten ohne eigene Services genügen die normalen Hooks – siehe Hook-Referenz.

Provider-Liste auslesen

Zur Diagnose gibt Plugin::providers() die gefilterte Liste zurück:

php
add_action(
	'ad_form_loaded',
	static function ( \Dingfelder\AdForm\Plugin $plugin ): void {
		foreach ( $plugin->providers() as $provider ) {
			error_log( $provider::class );
		}
	}
);

Digitale Lösungen. Persönlich. Zukunftssicher.