Skip to content

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, FormPanelData

Dependency 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) ​

NamespaceDirectory
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 exceptions

Entities 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) ​

AdapterResponsibility
PostTypeCPT registration (middag_{domain}), custom caps
DatabaseAbstraction over wp_posts + wp_postmeta, CCT toggle
HookHookInterface with register(), middag/ prefix
RestREST route registration with permission_callback
CronWP-Cron thin handlers delegating to domain services
EmailTheme-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.

php
// 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;
}
php
// 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.

php
add_action('plugins_loaded', function (): void {
    $kernel = new Kernel();
    $kernel->boot();
});

PHP 8.4+ Features ​

  • Native backed enums for all status types
  • readonly classes and properties on all entities
  • Match expressions for status transitions
  • Named arguments throughout
  • Strict typing enforced in every file