Darstellung
AD Form Architecture
Identity
| Key | Value |
|---|---|
| Name | AD Form |
| Slug | ad-form |
| PHP namespace | Dingfelder\AdForm |
| Text domain | ad-form |
| REST namespace | ad-form/v1 |
| Vendor | Dingfelder Digital |
Decisions
Custom tables instead of CPTs
Forms and submissions live in dedicated tables. CPT overhead (revisions, term relationships, wp_posts scans) does not scale to hundreds of thousands of entries. Queryable field values are stored in submission_meta (value_text, value_num) plus a JSON payload snapshot on the submission row for full reconstruction.
Application-level relations, no InnoDB foreign keys
WordPress sites change prefixes, import dumps and run hosts that skip FK checks. Referential integrity is enforced in repositories. Indexes cover the same columns a FK would use.
REST instead of admin-ajax.php
Admin builder and frontend submit share /wp-json/ad-form/v1. Cookie + X-WP-Nonce for admin. Public submit uses a per-form nonce (ad_form_submit_{id}). Logged-in AJAX submits still send X-WP-Nonce (wp_rest) so WordPress cookie auth accepts the request.
Lightweight container, no Symfony
A small singleton/transient container keeps the plugin installable without Composer runtime packages. Add-ons bind via ad_form_service_providers and ad_form_register_services.
Optional modules stay optional
The Gutenberg provider registers ad-form/form only when register_block_type exists and compiled block assets are present. The Elementor provider registers widget ad-form only after elementor/loaded and Widget_Base exist. Frontend CSS/JS enqueue only after a form is marked present.
Boot flow
ad-form.phpdefines constants, checks PHP, registers the autoloader.- Activation runs
Migrator::upgrade()and grants capabilities to administrators. plugins_loadedbootsPlugin.- Providers
register()thenboot(). ad_form_loadedfires for add-ons.
Services
| Service | Responsibility |
|---|---|
Container | DI bindings |
Migrator | Versioned schema upgrades |
Schema | dbDelta-safe CREATE TABLE SQL |
WpOptions / Settings | Plugin settings |
Capabilities | Custom caps |
Logger | Channel logs with redaction |
Menu / View / Assets | Admin UI |
FormService / FormRepository | Form CRUD, slugs, optimistic locking |
FieldRegistry | Builder field types and inspector schema |
FormRenderer / Shortcode | Frontend HTML for [ad_form], Gutenberg and Elementor |
StepEngine | Multi-step grouping (≥ 2 steps) |
SubmitService | Public submit, validation, persistence |
ConditionEngine | Show/hide and action rules (client + server) |
CalculationEngine | Safe formula parser for calculation fields (no eval) |
ActionRegistry / ActionRunner | Post-submit actions (email + merge tags) |
SubmissionService | Entry list, status, spam/trash, search |
Frontend\Assets | Conditional frontend enqueue |
ElementorIntegration | Widget ad-form |
GutenbergIntegration | Block ad-form/form |
Add-on extension
php
add_filter( 'ad_form_service_providers', function( array $providers ) {
$providers[] = new MyAddon\ServiceProvider();
return $providers;
} );
add_action( 'ad_form_register_services', function( \Dingfelder\AdForm\Core\Container $container ) {
$container->singleton( MyService::class, fn() => new MyService() );
} );Core must not type-hint optional module classes.