Skip to content

Überblick und Boot-Reihenfolge

AD Form besteht aus einem schlanken Service Container, einer festen Liste von Service Providern und stabilen Hook-Namen. Alles, was ein Add-on braucht, hängt an genau diesen drei Bausteinen.

Identität

SchlüsselWert
NameAD Form
Version0.11.0
Slug / Textdomainad-form
PHP-NamespaceDingfelder\AdForm
REST-Namespacead-form/v1
Menü-Slugad-form
MindestanforderungenPHP 8.2, WordPress 6.4

Diese Werte liegen als Konstanten in Dingfelder\AdForm\Core\PluginInfo und sollten von dort gelesen werden, nicht hart kodiert:

php
use Dingfelder\AdForm\Core\PluginInfo;

PluginInfo::VERSION;         // '0.11.0'
PluginInfo::REST_NAMESPACE;  // 'ad-form/v1'
PluginInfo::TEXT_DOMAIN;     // 'ad-form'
PluginInfo::dir();           // absoluter Plugin-Pfad mit Trailing Slash
PluginInfo::url();           // Plugin-URL

PluginInfo::OPTION_SETTINGS (ad_form_settings), OPTION_DB_VERSION und OPTION_VERSION benennen die verwendeten WordPress-Optionen.

Bootstrap: ad-form.php

Die Haupt-Datei bleibt bewusst dünn. Sie enthält keinerlei Domänenlogik:

  1. Direktzugriff blockenif ( ! defined( 'ABSPATH' ) ) { exit; }
  2. Konstanten definierenAD_FORM_VERSION, AD_FORM_FILE, AD_FORM_DIR, AD_FORM_URL, AD_FORM_BASENAME
  3. PHP-Version prüfen – unter 8.2 wird nur ein Admin-Hinweis registriert und die Datei per return verlassen; es wird nichts geladen
  4. Autoloader registrierenDingfelder\AdForm\Autoloader::register() bevorzugt vendor/autoload.php und fällt sonst auf einen eigenen PSR-4-Loader zurück
  5. Aktivierung / Deaktivierung registrierenCore\Activator::activate() und Core\Deactivator::deactivate()
  6. Boot einhängen – auf plugins_loaded mit Priorität 5
php
add_action(
	'plugins_loaded',
	static function (): void {
		Dingfelder\AdForm\Plugin::instance()->boot();
	},
	5
);

Die frühe Priorität 5 sorgt dafür, dass Add-ons, die sich auf plugins_loaded mit Standard-Priorität 10 einhängen, den Container bereits vorfinden.

Boot-Reihenfolge in Plugin::boot()

Plugin ist ein Singleton (Plugin::instance()) und bootet nur einmal pro Request; ein booted-Flag schützt gegen Doppelaufrufe.

php
public function boot(): void {
	if ( $this->booted ) {
		return;
	}

	$this->booted = true;

	$this->load_textdomain();
	$this->register_providers();

	foreach ( $this->providers as $provider ) {
		$provider->register( $this->container );
	}

	do_action( Hooks::REGISTER_SERVICES, $this->container );

	foreach ( $this->providers as $provider ) {
		$provider->boot( $this->container );
	}

	do_action( Hooks::LOADED, $this );
}

Der Ablauf im Detail:

  1. Container erzeugen (im privaten Konstruktor). Der Container registriert sich selbst und die Plugin-Instanz als instance()-Bindings, sodass beide auflösbar sind.
  2. Textdomain ladenload_plugin_textdomain( 'ad-form', false, …/languages ).
  3. Provider-Liste bauen und durch den Filter ad_form_service_providers schicken. Alles, was danach kein ServiceProviderInterface ist, wird herausgefiltert.
  4. register() über alle Provider – hier werden ausschließlich Bindings gesetzt, nichts aufgelöst und keine WordPress-Hooks registriert.
  5. ad_form_register_services feuert mit dem Container als Parameter. Der letzte Zeitpunkt, um eigene Services zu binden oder Kern-Bindings zu ersetzen.
  6. boot() über alle Provider – jetzt werden Services aufgelöst und add_action() / add_filter() aufgerufen.
  7. ad_form_loaded feuert mit der Plugin-Instanz. Ab hier ist alles verfügbar.

register() vor boot() – für alle Provider

Erst durchläuft die gesamte Liste register(), dann die gesamte Liste boot(). Ein Provider darf in register() deshalb keinen Service aus einem anderen Provider auflösen: Die Reihenfolge der Bindings ist innerhalb der Phase nicht garantiert. Auflösen ist erst in boot() oder – noch besser – erst innerhalb der Factory-Closure erlaubt.

Service Provider in Registrierungsreihenfolge

Plugin::register_providers() legt diese Reihenfolge fest:

#ProviderRegistriert unter anderem
1Core\CoreServiceProviderOptionsInterface (WpOptions), LoggerInterface (Logger); bootet Settings-Registrierung auf init
2Database\DatabaseServiceProviderDatabaseInterface (WpdbAdapter), TableNames, Schema, SchemaInstallerInterface, Migrator, Stats; startet Migrator::upgrade() auf init mit Priorität 1
3Licensing\LicensingServiceProviderLizenz- und Entitlement-Stack (LicenseManager, LicenseCache, FeatureRegistry, EntitlementManager, FeatureGate, ModuleGate, …); bootet Feature-Registry, Modul-Gate und Scheduler
4Forms\FormsServiceProviderFormDefinitionValidator, FormRepositoryInterface (FormRepository), FormService
5Fields\FieldsServiceProviderFieldRegistry; bootet die Registry
6Conditions\ConditionsServiceProviderConditionEngineInterface (ConditionEngine)
7Calculations\CalculationsServiceProviderCalculationEngineInterface (CalculationEngine)
8Actions\ActionsServiceProviderMailerInterface (WpMailer), MergeTagEngine, ActionRegistry (mit EmailAction), ActionRunner; hängt den Runner an ad_form_after_submission
9Admin\AdminServiceProviderView, FormsPage, EntriesPage, Menu, Assets; bootet nur wenn is_admin()
10REST\RestServiceProvideracht Controller; registriert die Routen auf rest_api_init und feuert danach ad_form_rest_routes
11Frontend\FrontendServiceProviderAssets, FormGate, SubmitState, SubmissionRepository, SubmitService, SubmissionService, FormRenderer, FormRendererInterface, Shortcode, SubmitListener
12Elementor\ElementorServiceProviderElementorIntegration (nur aktiv, wenn Elementor geladen ist)
13Gutenberg\GutenbergServiceProviderGutenbergIntegration (nur aktiv, wenn register_block_type und Block-Assets vorhanden sind)

Die Reihenfolge ist nicht beliebig: Core muss vor Database laufen, weil der Logger die DatabaseInterface-Bindung als Abhängigkeit hat; Licensing steht vor Fields, Actions, REST, Frontend und den optionalen Integrationen, damit jeder von ihnen beim Booten Entitlements auflösen kann; Fields muss vor Actions und Frontend kommen, weil beide auf die FieldRegistry zugreifen.

Eigene Provider werden über ad_form_service_providers angehängt und laufen danach – siehe Service Provider.

Container-API in Kürze

Der Container (Dingfelder\AdForm\Core\Container) bietet fünf Methoden:

php
use Dingfelder\AdForm\Core\Container;

$container->singleton( string $id, callable $factory ): void;  // einmal auflösen, Ergebnis merken
$container->bind( string $id, callable $factory ): void;       // bei jedem get() neu auflösen
$container->instance( string $id, mixed $instance ): void;     // fertiges Objekt hinterlegen
$container->has( string $id ): bool;
$container->get( string $id ): mixed;                          // wirft ContainerException

Der Container ist kein Auto-Wiring-Container: Abhängigkeiten werden in der Factory explizit aufgelöst. Das hält den Ablauf nachvollziehbar und verzichtet auf Reflection.

php
$container->singleton(
	FormService::class,
	static fn ( Container $container ): FormService => new FormService(
		$container->get( FormRepositoryInterface::class ),
		$container->get( FormDefinitionValidator::class )
	)
);

Zugriff aus einem Add-on:

php
use Dingfelder\AdForm\Forms\FormService;
use Dingfelder\AdForm\Plugin;

add_action(
	'ad_form_loaded',
	static function ( Plugin $plugin ): void {
		$forms = $plugin->container()->get( FormService::class );
		// …
	}
);

Alle Details – inklusive Fehlerfällen und Beispielen für eigene Bindings – stehen unter Service Container.

Architektur-Entscheidungen

Vier Entscheidungen prägen den Aufbau und sollten bei Erweiterungen respektiert werden:

Eigene Tabellen statt Custom Post Types. Formulare und Übermittlungen liegen in dedizierten Tabellen. Abfragbare Feldwerte landen in submission_meta (value_text, value_num), zusätzlich speichert die Submission-Zeile ein JSON-payload für die vollständige Rekonstruktion.

Relationen auf Anwendungsebene, keine InnoDB-Foreign-Keys. Präfixe ändern sich, Dumps werden importiert, Hoster überspringen FK-Checks. Die Repositories stellen Integrität sicher; Indizes decken dieselben Spalten ab, die ein FK nutzen würde.

REST statt admin-ajax.php. Builder und Frontend teilen sich /wp-json/ad-form/v1. Admin authentifiziert per Cookie und X-WP-Nonce, der öffentliche Submit über eine formularspezifische Nonce (ad_form_submit_{id}).

Optionale Module bleiben optional. Gutenberg und Elementor werden nur registriert, wenn die jeweilige Umgebung vorhanden ist. Kern-Code darf keine optionalen Modul-Klassen typisieren.

Die ausführliche Begründung steht im gespiegelten Referenzdokument Architektur.

Digitale Lösungen. Persönlich. Zukunftssicher.