Darstellung
Ü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üssel | Wert |
|---|---|
| Name | AD Form |
| Version | 0.11.0 |
| Slug / Textdomain | ad-form |
| PHP-Namespace | Dingfelder\AdForm |
| REST-Namespace | ad-form/v1 |
| Menü-Slug | ad-form |
| Mindestanforderungen | PHP 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-URLPluginInfo::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:
- Direktzugriff blocken –
if ( ! defined( 'ABSPATH' ) ) { exit; } - Konstanten definieren –
AD_FORM_VERSION,AD_FORM_FILE,AD_FORM_DIR,AD_FORM_URL,AD_FORM_BASENAME - PHP-Version prüfen – unter 8.2 wird nur ein Admin-Hinweis registriert und die Datei per
returnverlassen; es wird nichts geladen - Autoloader registrieren –
Dingfelder\AdForm\Autoloader::register()bevorzugtvendor/autoload.phpund fällt sonst auf einen eigenen PSR-4-Loader zurück - Aktivierung / Deaktivierung registrieren –
Core\Activator::activate()undCore\Deactivator::deactivate() - Boot einhängen – auf
plugins_loadedmit Priorität5
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:
- Container erzeugen (im privaten Konstruktor). Der Container registriert sich selbst und die
Plugin-Instanz alsinstance()-Bindings, sodass beide auflösbar sind. - Textdomain laden –
load_plugin_textdomain( 'ad-form', false, …/languages ). - Provider-Liste bauen und durch den Filter
ad_form_service_providersschicken. Alles, was danach keinServiceProviderInterfaceist, wird herausgefiltert. register()über alle Provider – hier werden ausschließlich Bindings gesetzt, nichts aufgelöst und keine WordPress-Hooks registriert.ad_form_register_servicesfeuert mit dem Container als Parameter. Der letzte Zeitpunkt, um eigene Services zu binden oder Kern-Bindings zu ersetzen.boot()über alle Provider – jetzt werden Services aufgelöst undadd_action()/add_filter()aufgerufen.ad_form_loadedfeuert mit derPlugin-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:
| # | Provider | Registriert unter anderem |
|---|---|---|
| 1 | Core\CoreServiceProvider | OptionsInterface (WpOptions), LoggerInterface (Logger); bootet Settings-Registrierung auf init |
| 2 | Database\DatabaseServiceProvider | DatabaseInterface (WpdbAdapter), TableNames, Schema, SchemaInstallerInterface, Migrator, Stats; startet Migrator::upgrade() auf init mit Priorität 1 |
| 3 | Licensing\LicensingServiceProvider | Lizenz- und Entitlement-Stack (LicenseManager, LicenseCache, FeatureRegistry, EntitlementManager, FeatureGate, ModuleGate, …); bootet Feature-Registry, Modul-Gate und Scheduler |
| 4 | Forms\FormsServiceProvider | FormDefinitionValidator, FormRepositoryInterface (FormRepository), FormService |
| 5 | Fields\FieldsServiceProvider | FieldRegistry; bootet die Registry |
| 6 | Conditions\ConditionsServiceProvider | ConditionEngineInterface (ConditionEngine) |
| 7 | Calculations\CalculationsServiceProvider | CalculationEngineInterface (CalculationEngine) |
| 8 | Actions\ActionsServiceProvider | MailerInterface (WpMailer), MergeTagEngine, ActionRegistry (mit EmailAction), ActionRunner; hängt den Runner an ad_form_after_submission |
| 9 | Admin\AdminServiceProvider | View, FormsPage, EntriesPage, Menu, Assets; bootet nur wenn is_admin() |
| 10 | REST\RestServiceProvider | acht Controller; registriert die Routen auf rest_api_init und feuert danach ad_form_rest_routes |
| 11 | Frontend\FrontendServiceProvider | Assets, FormGate, SubmitState, SubmissionRepository, SubmitService, SubmissionService, FormRenderer, FormRendererInterface, Shortcode, SubmitListener |
| 12 | Elementor\ElementorServiceProvider | ElementorIntegration (nur aktiv, wenn Elementor geladen ist) |
| 13 | Gutenberg\GutenbergServiceProvider | GutenbergIntegration (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 ContainerExceptionDer 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.