Skip to content

REF-102-01: Persistence & WordPress Adapter Spec

ADR: ADR-102 — Architecture FoundationEscopo: CPT slugs, wp_posts mapping, repository pattern implementation, WordPress adapter sub-namespaces, dependency direction matrix, naming conventions, PSR-4


1. Persistence — wp_posts + Custom Post Types

All domain entities are stored as WordPress Custom Post Types in wp_posts + wp_postmeta (EAV pattern). CPTs use the slug convention middag_{domain} and are registered with 'show_ui' => false (admin UI provided by Inertia.js, not the WordPress editor).

wp_posts field mapping:

wp_posts ColumnDomain Meaning
post_titleDisplay name of the entity
post_statusEntity status (publish=active, draft=inactive)
post_authorOwner (WP user_id)
post_dateCreation date
post_modifiedLast modification date
post_parentParent entity (for hierarchies)
All other fieldsStored in wp_postmeta

Registered CPTs:

Domainpost_type slug
Organizationmiddag_organization
Collaboratormiddag_collaborator
Invoicemiddag_invoice
TaxInvoicemiddag_taxinvoice
Quotemiddag_quote
Contractmiddag_contract
Documentmiddag_document
Entitlementmiddag_entitlement
Environmentmiddag_environment
Servicemiddag_service
ServiceRequestmiddag_service_request
Licensemiddag_license

Performance optimizations:

  • update_post_meta_cache() for batch meta loading in list views
  • register_meta() with show_in_rest for REST API exposure
  • Indexed meta_query keys for high-traffic queries
  • WordPress object cache (wp_cache_get/wp_cache_set) for hot entities
  • Pattern: single meta_query + post__in instead of multiple JOINs

2. Repository Pattern — Implementation

Domain code defines repository interfaces. WordPress adapter layer provides concrete implementations using QueryBuilder and MetaRepository. The DI Container binds interface to implementation.

php
// Domain/Organization/OrganizationRepository.php (INTERFACE)
namespace Middag\Account\Domain\Organization;

interface OrganizationRepository {
    public function findById(int $id): ?OrganizationEntity;
    public function findByUserId(int $userId): array;
    public function save(OrganizationEntity $entity): int;
    public function delete(int $id): bool;
}

// WordPress/Repository/WpOrganizationRepository.php (IMPLEMENTATION)
namespace Middag\Account\WordPress\Repository;

use Middag\Account\Domain\Organization\OrganizationRepository;
use Middag\Account\Domain\Organization\OrganizationEntity;

class WpOrganizationRepository implements OrganizationRepository {
    public function __construct(
        private QueryBuilder $query,
        private MetaRepository $meta,
    ) {}

    public function findById(int $id): ?OrganizationEntity {
        $post = $this->query->postType('middag_organization')->find($id);
        if (!$post) return null;
        return OrganizationEntity::fromPost($post, $this->meta->getAllForPost($id));
    }
}

No domain code calls WP_Query, get_post_meta(), or any WordPress function directly.

3. WordPress Adapter Sub-Namespaces

Beyond the 6 core adapters, the WordPress abstraction layer includes additional sub-namespaces:

Sub-NamespaceKey ClassesResponsibility
WordPress\PostType\PostTypeRegistrarRegisters all CPTs on init hook, centralised
WordPress\Database\QueryBuilder, MetaRepository, MigrationServiceFluent WP_Query wrapper, batch meta ops, CCT migration
WordPress\Hook\HookRegistrar, HookInterface, domain-specific *HooksDeclarative hook registration, auto-discovery of hook classes
WordPress\Rest\RouteRegistrar, RestResponseREST route registration, standardised JSON responses
WordPress\Cron\CronRegistrar, CronHandlerCron scheduling, callback dispatch to domain services
WordPress\Email\EmailSender, EmailTemplateTransactional email via wp_mail(), template resolution
WordPress\Admin\AdminRegistrar, InertiaAdapterWP Admin menu/assets, Inertia.js bridge for React admin UI
WordPress\WooCommerce\OrderAdapter, SubscriptionAdapter, GatewayAdapter, ProductAdapterWC order/subscription/product abstraction for domain use
WordPress\User\UserRepository, UserMetaWP_User queries, user meta operations
WordPress\Repository\Wp{Domain}Repository for each domainConcrete implementations of domain repository interfaces

What this layer is NOT:

  • Not a framework — no extension system, no plugin architecture, no custom hooks
  • Not a full ORM — QueryBuilder is a thin WP_Query wrapper, not Eloquent or Doctrine
  • Not a WordPress replacement — uses WP APIs integrally, just encapsulates calls to isolate the domain

4. Dependency Direction Matrix

NamespaceCan AccessCannot Access
Core/Everything (bootstrap)
Domain/Only Core/ base classesWordPress/, Api/, Integration/, UI/
Integration/Core/, Domain/ (entities/DTOs)WordPress/, Api/, UI/
Api/Core/, Domain/, Integration/WordPress/, UI/
WordPress/Core/, Domain/, Integration/UI/
UI/Core/, Domain/, WordPress/Api/, Integration/

Any violation of these rules is an architectural defect that must be fixed before merge.

5. Naming Conventions

ElementConventionExample
DomainsPascalCase singularOrganization, Invoice
Services{Domain}Service.phpOrganizationService.php
Repositories{Domain}Repository.phpOrganizationRepository.php
Entities{Domain}Entity.phpOrganizationEntity.php
API Controllers{Domain}Controller.phpOrderController.php
Integrations{Provider}Client.phpStripeClient.php
WP Abstractions{Context}{Function}.phpPostTypeRegistrar.php
Middleware{Role}Middleware.phpAuthMiddleware.php
Page Controllers{Domain}PageController.phpQuotePageController.php

6. PSR-4 Namespace

ProjetoNamespacePSR-4 Root
Plugin middag-accountMiddag\Account\src/