Skip to content

XOOPS 2026 Architecture

Overview

XOOPS 2026 introduces a modern, clean architecture that embraces Domain-Driven Design (DDD), CQRS patterns, and PSR standards while maintaining backward compatibility with existing modules.

Architecture Principles

flowchart TB
    subgraph "Presentation Layer"
        A[Controllers]
        B[API Endpoints]
        C[CLI Commands]
    end

    subgraph "Application Layer"
        D[Command Handlers]
        E[Query Handlers]
        F[Application Services]
    end

    subgraph "Domain Layer"
        G[Entities]
        H[Value Objects]
        I[Domain Services]
        J[Domain Events]
    end

    subgraph "Infrastructure Layer"
        K[Repositories]
        L[External Services]
        M[Persistence]
    end

    A --> D
    A --> E
    B --> D
    B --> E
    C --> D
    D --> F
    E --> F
    F --> G
    F --> H
    F --> I
    I --> J
    G --> K
    K --> M
    F --> L

Core Concepts

Clean Architecture Layers

Layer Purpose Dependencies
Presentation HTTP handling, CLI, templates Application Layer
Application Use cases, orchestration Domain Layer
Domain Business logic, entities None (pure PHP)
Infrastructure External concerns, persistence All layers

Domain Layer

The domain layer contains pure business logic with no external dependencies:

namespace Xoops\Modules\Article\Domain;

final class Article
{
    private function __construct(
        private ArticleId $id,
        private Title $title,
        private Content $content,
        private AuthorId $authorId,
        private ArticleStatus $status,
        private \DateTimeImmutable $createdAt,
        private ?\DateTimeImmutable $publishedAt
    ) {}

    public static function create(
        Title $title,
        Content $content,
        AuthorId $authorId
    ): self {
        return new self(
            id: ArticleId::generate(),
            title: $title,
            content: $content,
            authorId: $authorId,
            status: ArticleStatus::Draft,
            createdAt: new \DateTimeImmutable(),
            publishedAt: null
        );
    }

    public function publish(): void
    {
        if ($this->status === ArticleStatus::Published) {
            throw new ArticleAlreadyPublishedException($this->id);
        }

        $this->status = ArticleStatus::Published;
        $this->publishedAt = new \DateTimeImmutable();
    }
}

Application Layer

The application layer coordinates domain operations:

namespace Xoops\Modules\Article\Application\Commands;

final class PublishArticleCommand
{
    public function __construct(
        public readonly string $articleId
    ) {}
}

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

    public function handle(PublishArticleCommand $command): void
    {
        $article = $this->repository->findById(
            ArticleId::fromString($command->articleId)
        );

        if (!$article) {
            throw new ArticleNotFoundException($command->articleId);
        }

        $article->publish();

        $this->repository->save($article);
        $this->events->dispatch(new ArticlePublished($article->getId()));
    }
}

Key Components

ULID Identifiers

All entities use ULIDs for unique identification:

namespace Xoops\Modules\Xmf\Domain\ValueObjects;

final class EntityId
{
    private function __construct(
        private readonly string $value
    ) {}

    public static function generate(): self
    {
        return new self(Ulid::generate());
    }

    public static function fromString(string $value): self
    {
        if (!Ulid::isValid($value)) {
            throw new InvalidEntityIdException($value);
        }
        return new self($value);
    }
}

Repository Pattern

interface ArticleRepository
{
    public function findById(ArticleId $id): ?Article;
    public function save(Article $article): void;
    public function delete(Article $article): void;
    public function findByAuthor(AuthorId $authorId): array;
}

CQRS Pattern

Commands for writes, queries for reads:

// Command (write)
$commandBus->dispatch(new PublishArticleCommand($articleId));

// Query (read)
$articles = $queryBus->dispatch(new GetRecentArticlesQuery(limit: 10));

Integration Points

PSR Standards

  • PSR-4: Autoloading
  • PSR-7: HTTP Messages
  • PSR-11: Container Interface
  • PSR-15: HTTP Middleware
  • PSR-14: Event Dispatcher

Backward Compatibility

Legacy modules continue to work through adapters:

// Legacy handler adapted to new repository
class LegacyArticleHandlerAdapter implements ArticleRepository
{
    public function __construct(
        private readonly XoopsObjectHandler $legacyHandler
    ) {}

    public function findById(ArticleId $id): ?Article
    {
        $legacyObject = $this->legacyHandler->get($id->toInt());
        return $legacyObject ? ArticleMapper::toDomain($legacyObject) : null;
    }
}