Skip to content

🏗️ Gold Standard Architecture

A deep dive into the architectural decisions and patterns used in the Gold Standard module.

XOOPS 2026 Reference Architecture

The Gold Standard module demonstrates the target architecture for XOOPS 2026. It uses PHP 8.2+, PSR-11 containers, and Clean Architecture principles.

Aspect XOOPS 2.5.x Gold Standard (2026)
PHP Version 7.4+ 8.2+
DI Container Manual wiring PSR-11 autowiring
Architecture Handler pattern Clean Architecture
Data Access XoopsPersistableObjectHandler Repository + Mapper

Learning value for 2.5.x developers: Study the patterns and adapt them to your PHP version. The concepts (separation of concerns, dependency inversion) apply regardless of PHP version.


Architecture Philosophy

The Gold Standard module follows Clean Architecture principles, ensuring that business logic is isolated from framework concerns. This creates a maintainable, testable, and portable codebase.

Core Principles

flowchart LR
    subgraph Principles["Architectural Principles"]
        direction TB
        A[🎯 Domain-Centric]
        B[📦 Framework Independence]
        C[🧪 Testability]
        D[🔌 Dependency Inversion]
    end

    A --> E[Business logic at the core]
    B --> F[XOOPS as a plugin]
    C --> G[Unit test everything]
    D --> H[Abstractions over concretions]

Layer Architecture

The Dependency Rule

Dependencies only point inward. Outer layers depend on inner layers, never the reverse.

flowchart TB
    subgraph External["External Layer"]
        UI[UI/Web]
        DB[(Database)]
        EXT[External APIs]
    end

    subgraph Infrastructure["Infrastructure Layer"]
        CTRL[Controllers]
        REPO_IMPL[Repository Implementations]
        HTTP[HTTP Clients]
    end

    subgraph Application["Application Layer"]
        UC[Use Cases]
        APP_SVC[Application Services]
        DTO[DTOs]
    end

    subgraph Domain["Domain Layer (Core)"]
        ENT[Entities]
        VO[Value Objects]
        REPO_INT[Repository Interfaces]
        DOM_SVC[Domain Services]
        EVT[Domain Events]
    end

    External --> Infrastructure
    Infrastructure --> Application
    Application --> Domain

    style Domain fill:#e1f5fe
    style Application fill:#fff3e0
    style Infrastructure fill:#f3e5f5
    style External fill:#ffebee

Domain Layer

The heart of the module. Contains all business logic with zero external dependencies.

Entities

Entities have identity and lifecycle. They encapsulate business rules.

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Entity;

final class Article
{
    private function __construct(
        public readonly ArticleId $id,
        private ArticleTitle $title,
        private ArticleContent $content,
        private AuthorId $authorId,
        private ArticleStatus $status,
        private readonly \DateTimeImmutable $createdAt,
        private ?\DateTimeImmutable $updatedAt = null,
        private ?\DateTimeImmutable $publishedAt = null,
    ) {}

    public static function create(
        ArticleId $id,
        ArticleTitle $title,
        ArticleContent $content,
        AuthorId $authorId,
    ): self {
        return new self(
            id: $id,
            title: $title,
            content: $content,
            authorId: $authorId,
            status: ArticleStatus::Draft,
            createdAt: new \DateTimeImmutable(),
        );
    }

    public function update(ArticleTitle $title, ArticleContent $content): void
    {
        $this->title = $title;
        $this->content = $content;
        $this->updatedAt = new \DateTimeImmutable();
    }

    public function publish(): void
    {
        $this->guardAgainstInvalidStatusTransition(ArticleStatus::Published);
        $this->status = ArticleStatus::Published;
        $this->publishedAt = new \DateTimeImmutable();
    }

    public function archive(): void
    {
        $this->guardAgainstInvalidStatusTransition(ArticleStatus::Archived);
        $this->status = ArticleStatus::Archived;
    }

    private function guardAgainstInvalidStatusTransition(ArticleStatus $to): void
    {
        if (!$this->status->canTransitionTo($to)) {
            throw new InvalidStatusTransition($this->status, $to);
        }
    }
}

Value Objects

Immutable objects defined by their attributes, not identity.

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\ValueObject;

final readonly class ArticleTitle
{
    private const MIN_LENGTH = 3;
    private const MAX_LENGTH = 255;

    private function __construct(
        public string $value,
    ) {}

    public static function fromString(string $title): self
    {
        $title = trim($title);

        if (mb_strlen($title) < self::MIN_LENGTH) {
            throw new InvalidArticleTitle(
                "Title must be at least " . self::MIN_LENGTH . " characters"
            );
        }

        if (mb_strlen($title) > self::MAX_LENGTH) {
            throw new InvalidArticleTitle(
                "Title must not exceed " . self::MAX_LENGTH . " characters"
            );
        }

        return new self($title);
    }

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }
}

Enums for Status

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Entity;

enum ArticleStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function canTransitionTo(self $to): bool
    {
        return match($this) {
            self::Draft => in_array($to, [self::Published, self::Archived]),
            self::Published => $to === self::Archived,
            self::Archived => false,
        };
    }

    /** @return list<self> */
    public function allowedTransitions(): array
    {
        return match($this) {
            self::Draft => [self::Published, self::Archived],
            self::Published => [self::Archived],
            self::Archived => [],
        };
    }
}

State Machine Visualization

stateDiagram-v2
    [*] --> Draft: create()
    Draft --> Published: publish()
    Draft --> Archived: archive()
    Published --> Archived: archive()
    Archived --> [*]

    note right of Draft
        Initial state for all articles
        Can be edited freely
    end note

    note right of Published
        Visible to public
        Can only be archived
    end note

Application Layer

Orchestrates domain logic to fulfill use cases.

Command Pattern

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Application\Command;

final readonly class CreateArticle
{
    public function __construct(
        public string $title,
        public string $content,
        public int $authorId,
    ) {}
}

Command Handler

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Application\Command;

use Xoops\GoldStandard\Domain\Entity\Article;
use Xoops\GoldStandard\Domain\Repository\ArticleRepository;
use Xoops\GoldStandard\Domain\ValueObject\{ArticleTitle, ArticleContent, AuthorId};

final readonly class CreateArticleHandler
{
    public function __construct(
        private ArticleRepository $articles,
        private EventDispatcher $events,
    ) {}

    public function __invoke(CreateArticle $command): ArticleId
    {
        $article = Article::create(
            id: $this->articles->nextIdentity(),
            title: ArticleTitle::fromString($command->title),
            content: ArticleContent::fromString($command->content),
            authorId: AuthorId::fromInt($command->authorId),
        );

        $this->articles->save($article);

        foreach ($article->pullDomainEvents() as $event) {
            $this->events->dispatch($event);
        }

        return $article->id;
    }
}

Flow Diagram

sequenceDiagram
    participant C as Controller
    participant H as Handler
    participant A as Article
    participant R as Repository
    participant E as Events

    C->>H: CreateArticle(title, content, author)
    H->>R: nextIdentity()
    R-->>H: ArticleId
    H->>A: Article::create(...)
    A-->>H: Article
    H->>R: save(article)
    R-->>H: void
    H->>A: pullDomainEvents()
    A-->>H: [ArticleCreated]
    H->>E: dispatch(ArticleCreated)
    H-->>C: ArticleId

Infrastructure Layer

Concrete implementations of domain interfaces.

Repository Implementation

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Infrastructure\Persistence;

use Xoops\GoldStandard\Domain\Entity\Article;
use Xoops\GoldStandard\Domain\Repository\ArticleRepository;
use Xoops\GoldStandard\Domain\ValueObject\ArticleId;
use XoopsDatabase;

final class XoopsArticleRepository implements ArticleRepository
{
    private const TABLE = 'goldstandard_articles';

    public function __construct(
        private readonly XoopsDatabase $db,
        private readonly ArticleMapper $mapper,
    ) {}

    public function nextIdentity(): ArticleId
    {
        return ArticleId::generate();
    }

    public function find(ArticleId $id): ?Article
    {
        $sql = sprintf(
            "SELECT * FROM %s WHERE id = :id",
            $this->db->prefix(self::TABLE)
        );

        $row = $this->db->fetchRow($sql, ['id' => $id->toString()]);

        return $row ? $this->mapper->toDomain($row) : null;
    }

    public function findOrFail(ArticleId $id): Article
    {
        return $this->find($id)
            ?? throw new ArticleNotFound($id);
    }

    public function save(Article $article): void
    {
        $data = $this->mapper->toPersistence($article);

        $this->db->upsert(
            $this->db->prefix(self::TABLE),
            $data,
            ['id']
        );
    }

    public function remove(Article $article): void
    {
        $this->db->delete(
            $this->db->prefix(self::TABLE),
            ['id' => $article->id->toString()]
        );
    }
}

Dependency Injection

Service Configuration

<?php
// config/services.php

declare(strict_types=1);

use Xoops\GoldStandard\Domain\Repository\ArticleRepository;
use Xoops\GoldStandard\Infrastructure\Persistence\XoopsArticleRepository;

return static function (ContainerBuilder $container): void {
    // Domain - no config needed, plain PHP

    // Infrastructure
    $container->bind(ArticleRepository::class, XoopsArticleRepository::class);

    // Application
    $container->bind(CreateArticleHandler::class)
        ->constructor(
            ArticleRepository::class,
            EventDispatcher::class,
        );
};

Container Architecture

flowchart TB
    subgraph Container["PSR-11 Container"]
        direction TB
        REG[Service Registry]
        FAC[Factory Resolver]
        AUT[Auto-wiring]
    end

    subgraph Bindings["Service Bindings"]
        INT[Interfaces]
        IMP[Implementations]
    end

    subgraph Lifecycle["Lifecycle"]
        SIN[Singleton]
        TRA[Transient]
        SCO[Scoped]
    end

    INT --> REG
    IMP --> REG
    REG --> FAC
    FAC --> AUT
    AUT --> Lifecycle

Middleware Pipeline

Request processing follows PSR-15 middleware pattern:

flowchart LR
    REQ[Request] --> M1

    subgraph Pipeline["Middleware Stack"]
        M1[Error Handler] --> M2[Security]
        M2 --> M3[Session]
        M3 --> M4[Auth]
        M4 --> M5[Router]
        M5 --> M6[Module Loader]
        M6 --> CTRL[Controller]
    end

    CTRL --> RES[Response]

Event System

Domain Events

<?php

declare(strict_types=1);

namespace Xoops\GoldStandard\Domain\Event;

final readonly class ArticlePublished implements DomainEvent
{
    public function __construct(
        public ArticleId $articleId,
        public \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
    ) {}
}

Event Flow

flowchart LR
    subgraph Domain
        A[Article] -->|raises| E1[ArticlePublished]
    end

    subgraph Application
        E1 --> D[Dispatcher]
    end

    subgraph Listeners
        D --> L1[SendNotification]
        D --> L2[UpdateSearchIndex]
        D --> L3[InvalidateCache]
        D --> L4[LogActivity]
    end

Testing Architecture

Test Pyramid

flowchart TB
    subgraph Pyramid["Test Pyramid"]
        E2E["🔝 E2E Tests (5%)"]
        INT["⬆️ Integration Tests (25%)"]
        UNIT["📦 Unit Tests (70%)"]
    end

    E2E --> |"Slow, Expensive"| INT
    INT --> |"Medium"| UNIT
    UNIT --> |"Fast, Cheap"| BASE[Foundation]

Test Organization

tests/
├── Unit/
│   ├── Domain/
│   │   ├── Entity/
│   │   │   └── ArticleTest.php
│   │   └── ValueObject/
│   │       └── ArticleTitleTest.php
│   └── Application/
│       └── Command/
│           └── CreateArticleHandlerTest.php
├── Integration/
│   └── Infrastructure/
│       └── Persistence/
│           └── XoopsArticleRepositoryTest.php
└── Functional/
    └── Api/
        └── ArticleEndpointTest.php


architecture #clean-architecture #ddd #patterns #goldstandard