tangible / ddd
Domain-Driven Design framework for WordPress plugins
Requires
- php: >=8.1
- league/tactician: ^2.0-rc1
- makinacorpus/query-builder: ^1.6
- symfony/config: ^7.4
- symfony/dependency-injection: ^7.4
- woocommerce/action-scheduler: ^3.9
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.0|^11.0
- symfony/yaml: ^7.4
- tomasvotruba/unused-public: ^2.2
- wp-phpunit/wp-phpunit: ^7.0
- yoast/phpunit-polyfills: ^4.0
README
Tangible DDD is a Domain-Driven Design runtime for WordPress plugins. It gives each consumer plugin a command/query boundary, transactional event publication, durable asynchronous delivery, causal tracing, and long-running orchestration without making consumers share one database identity.
The current documentation describes the 0.6.x line. Check the installed package version and source before applying examples to an older consumer.
Requirements
- PHP 8.1 or newer
- WordPress
- Composer
- Action Scheduler
- A Symfony Dependency Injection container for each top-level consumer
Quick start
composer require tangible/ddd:^0.6.2
wp ddd init --prefix=acme_orders --namespace='Acme\Orders'
Run wp ddd init from the consumer plugin directory. It creates the supported
DI and table scaffolding without modifying the plugin entry file; the command
prints the small bootstrap snippet to add there. Existing generated files are
preserved unless --force is supplied.
See Wiring a consumer for the complete container, bootstrap, migration, and deployment contract.
Runtime model
- Commands enter the command bus. Its middleware owns correlation, audit, transaction, domain-event publication, and terminal handler execution.
- The stock wpdb transaction middleware opens a database transaction only for
commands implementing
ITransactionalCommand. Being on the command bus is not itself a transaction opt-in. - Queries use a read-only bus without the command transaction and audit bracket.
- Domain events are synchronous and remain inside the originating unit of work. Integration events cross a consistency boundary through the transactional outbox and Action Scheduler.
- Commands and queries may use separate handlers or explicitly opt into self-handling. Commands do not dispatch other commands.
BehaviourWorkflowruns configurable, repeatable behaviour routines over work items.LongProcessmodels developer-authored business lifecycles that can schedule, suspend, await integration events, resume, and compensate.- Long-process definitions are compiled into
LongProcessCatalog, so dumped production containers have the same process discovery as development containers. - Correlation and causation metadata survive cross-plugin handoffs. Trace discovery remains consumer-scoped, but opening an exact correlation gathers every registered consumer and stitches their recorded fragments without a shared write table.
- Declared aggregate touches create a rebuildable Biography read model without making the touches table a write-side authority.
Consumer ownership
Every plugin may bundle its own Composer copy. Each copy registers at
plugins_loaded:0; at priority 1, the loader initializes only the newest
registered version. Top-level consumers then boot against that winning copy and
retain their own namespace root, table prefix, container, and storage.
A consumer module is a strict namespace descendant that contributes commands, queries, listeners, and long processes through a separate compiled container while sharing its host consumer's runtime identity. A sidecar plugin is one way to package such a module. Modules do not become additional dashboard consumers and do not mutate the host container. See Consumer modules.
Consumer-scoped storage
For a configured prefix such as acme_orders, the framework maintains seven
WordPress tables:
acme_orders_integration_outboxacme_orders_integration_dlqacme_orders_long_processesacme_orders_command_auditacme_orders_touchesacme_orders_behaviour_workflowsacme_orders_behaviour_workflow_items
The real table names also include the site's WordPress table prefix. Retention and export policy can be chosen per consumer. Propagated correlation metadata allows the v2 unified trace to be assembled without a shared write table.
Dashboard
The winning framework copy registers Tangible DDD at the
tangible-dddash admin page. The dashboard discovers top-level consumers and
reads each consumer's own audit, outbox, process, workflow, touch, and trace
data. Exact traces are unified across participating consumers and retain
consumer provenance; Biography remains scoped to the selected aggregate owner.
Its live view uses WordPress Heartbeat to reveal new trace pieces as workers
finish.
Documentation
- Documentation map
- Wiring a consumer
- Consumer design interview
- Consumer modules
- Release and migration ledger
- Canonical agent skill
Historical specs and plans are retained for design provenance and are clearly classified in the documentation map. Current source and tests win whenever a historical record disagrees with the installed package.