🏗️ 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
🔗 Related¶
- Domain-Model - Detailed domain model documentation
- Repository-Layer - Repository pattern implementation
- Service-Layer - Application services
- Gold Standard Module - Main module page