Architecture Overview
middag-account follows a DDD Light architecture with strict dependency direction. Domain logic lives in pure PHP with zero WordPress imports. WordPress acts as an infrastructure adapter.
Layers
src/
├── Core/ # DI Container, Kernel, ServiceProvider, Middleware
├── Domain/ # Pure PHP — ZERO WordPress dependencies
├── WordPress/ # CMS abstraction layer (6 adapters)
├── Api/V1/ # REST controllers (middag-account/v1)
├── Integration/ # HubSpot, Stripe, ISSNet, BancoInter, Cloudflare, SolidAffiliate
└── UI/ # Inertia page controllers, Router, FormPanelDataDependency Direction
WordPress/ ──→ Domain/
Api/ ──→ Domain/
Integration/ → Domain/
UI/ ──→ Domain/Domain never imports from WordPress, Api, Integration, or UI. This is enforced by PHPStan static analysis.
Symfony DI Container 7.4
All classes use constructor injection. Zero static calls to services.
Auto-discovery by suffix: classes ending in Service, Repository, Controller, Handler, Hooks, or Registrar are registered automatically. Adding a new class with the correct suffix is enough -- no manual registration required.
The container is compiled in production for performance.
Namespace Mapping (PSR-4)
| Namespace | Directory |
|---|---|
Middag\Account\Core\ | src/Core/ |
Middag\Account\Domain\{Name}\ | src/Domain/{Name} |
Middag\Account\WordPress\ | src/WordPress/ |
Middag\Account\Api\V1\ | src/Api/V1/ |
Middag\Account\Integration\{Name}\ | src/Integration/ |
Middag\Account\UI\ | src/UI/ |
Domain Layer
Each domain follows a standard structure:
Domain/{Name}/
├── {Name}Entity.php # Immutable (readonly), JsonSerializable
├── {Name}DTO.php # Data Transfer Object
├── {Name}RepositoryInterface.php # Contract (interface)
├── {Name}Service.php # Business logic, orchestration
├── {Name}Status.php # Status enum with transitions
└── {Name}Exception.php # Domain-specific exceptionsEntities use PHP 8.4 readonly properties, backed enums for statuses, and fromArray() factory methods that accept both API snake_case and CCT-prefixed keys.
WordPress Abstraction Layer (6 Adapters)
| Adapter | Responsibility |
|---|---|
| PostType | CPT registration (middag_{domain}), custom caps |
| Database | Abstraction over wp_posts + wp_postmeta, CCT toggle |
| Hook | HookInterface with register(), middag/ prefix |
| Rest | REST route registration with permission_callback |
| Cron | WP-Cron thin handlers delegating to domain services |
Theme-overridable templates via TemplateRenderer |
Repository Pattern
Domain code defines repository interfaces. WordPress adapter layer provides concrete implementations using QueryBuilder and MetaRepository. The DI container binds interface to implementation.
// Domain layer — pure PHP interface
namespace Middag\Account\Domain\Organization;
interface OrganizationRepositoryInterface
{
public function findById(int $id): ?OrganizationEntity;
public function findByOwnerId(int $ownerId): array;
public function save(OrganizationDTO $dto): int;
}// WordPress layer — concrete implementation
namespace Middag\Account\WordPress\Repository;
final class OrganizationRepository implements OrganizationRepositoryInterface
{
// Uses QueryBuilder + MetaRepository internally
}Dual-Repository Toggle
A middag_migration_complete option toggles between CCT repositories ($wpdb direct) and wp_posts-based repositories. This allows gradual migration without data loss.
Boot Sequence
The plugin boots on the plugins_loaded hook -- never immediately. The Kernel compiles the DI container, registers hooks, and loads auto-discovered services.
add_action('plugins_loaded', function (): void {
$kernel = new Kernel();
$kernel->boot();
});PHP 8.4+ Features
- Native backed enums for all status types
readonlyclasses and properties on all entities- Match expressions for status transitions
- Named arguments throughout
- Strict typing enforced in every file