💡 Overview
The Game module represents a critical component within the custom CMS, responsible for managing game records and their associated video content. This project documents the refactoring effort to implement the module using Hexagonal Architecture (Ports and Adapters) principles.
The goal of this architectural change was to isolate the core business logic—the Domain—from external dependencies, such as the database (Persistence) and the frontend templating (Presentation). This approach provides a clear separation of concerns, leading to greater flexibility and maintainability.
⚙️ Technical Stack
| Area | Technologies |
|---|---|
| Architecture | Hexagonal Architecture (Ports and Adapters) |
| Backend Core | PHP 8 (with strict types) |
| Data Access | PDO, MySQL |
| Dependencies | Dependency Injection (managed by GameServiceFactory) |
| Security Principle | Read/Write Repository separation |
🧭 Architecture & Design: The Hexagonal Model
The Game module adheres strictly to the three main layers defined by the Hexagonal Architecture:
1. Domain Layer (The Core)
This layer contains the application's true business rules and data structure. It defines what the system does.
- Entities: The core data structures are encapsulated in domain entities, such as
GameandVideo. TheGameentity validates that the ID is positive and thesimulador(game name/title) is not empty upon construction. - Ports (Interfaces): The domain defines interfaces (ports) for all external interactions, ensuring it remains independent of implementation details.
- Read Port:
GameRepositoryInterfacedefines methods for retrieval, such asfindGamesWithVideos(paginated list of games with associated videos),findById, and methods to count games or find videos by ID (findVideosByGameId). - Write Port (Command Port):
GameWriteRepositoryInterfacedefines mutation operations likecreateGame,updateGame,createVideo, and the crucial transactional methoddeleteGameAndVideos.
- Read Port:
- Exceptions: Domain-specific errors, like
GameNotFoundException, are defined here.
2. Application Layer (The Use Cases)
The Application Layer orchestrates domain entities to fulfill specific business tasks, known as Use Cases. It dictates how the domain is used.
- Use Cases: Each business action is an independent use case:
- Read Examples:
GetGamesUseCaseretrieves paginated game lists.GetGameUseCaseretrieves a single game by ID and throwsGameNotFoundExceptionif the result is null.GetVideosUseCasehandles retrieval and sorting of videos for a specific game ID. - Write Examples (Admin Commands):
CreateGameUseCase,UpdateGameUseCase, andDeleteGameUseCaseutilize theGameWriteRepositoryInterfaceto perform their respective duties.
- Read Examples:
- Data Transfer Objects (DTOs): DTOs (
GameDTO,VideoDTO,PaginationDTO) are used exclusively to transfer data between layers (e.g., from Use Case to Presentation) without exposing the internal Domain entities.
3. Infrastructure Layer (The Adapters)
This layer holds the external technical details and implements the ports defined by the Domain. It manages where data is stored and how it is displayed.
- Persistence Adapters:
PdoGameRepository: Implements the read port (GameRepositoryInterface) using PDO and MySQL. It maps raw database results back into DomainGameorVideoentities usingfromArraymethods.PdoGameWriteRepository: Implements the write port (GameWriteRepositoryInterface). This adapter is responsible for CUD operations, including the atomic deletion of a game and its videos within a database transaction.
- Presentation Adapter:
GameRendereris the presentation adapter responsible for converting DTOs into HTML output. It contains methods likedisplayGamesListanddisplayGame.
🖥️ Key Functionalities
The module provides robust management and display features:
- Game Management (Admin): Use cases allow for creating (
CreateGameUseCase), updating (UpdateGameUseCase), and deleting (DeleteGameUseCase) game records, ensuring that deletion also removes associated videos (deleteGameAndVideos). - Video Management: Videos, identified by a unique ID and
gameId, can be created, updated, and deleted using dedicated use cases (CreateVideoUseCase,UpdateVideoUseCase,DeleteVideoUseCase), interacting with the write repository. - Content Display: The
GameRendererhandles generating HTML for game lists, detailed single-game views, and videos associated with a game. It retrieves necessary parameters (like game ID) from request parameters (e.g.,$_GET['game']). - Pagination and SEO: Retrieval use cases are paginated (
GetGamesUseCasetakeslimitandoffset). The renderer implements logic to generate SEO-friendly URL slugs combining the game ID and the game title (simulador).
🧠 Challenges & Solutions
| Challenge | Solution (Hexagonal Context) |
|---|---|
| Separating Read/Write Concerns | Defined separate ports (GameRepositoryInterface and GameWriteRepositoryInterface) and corresponding adapters (PdoGameRepository and PdoGameWriteRepository). This reinforces the Least Privilege principle, aligning with the site's two-connection model (read-only vs. admin access). |
| Backward Compatibility | Maintained deprecated classes (GameRepo and GameRender) which now delegate calls to the new Use Cases and Adapters, facilitating a seamless migration from the old architecture. |
| Wiring and Configuration | The GameServiceFactory was introduced in the Infrastructure layer to handle the creation and wiring of Use Cases with their appropriate PDO-based Repository Adapters, centralizing Dependency Injection. |
🔍 Security & Best Practices
- Data Integrity: Domain entities (
Game,Video) perform input validation (e.g., ID constraints, non-empty fields). - Database Security: All persistence adapters (
PdoGameRepository,PdoGameWriteRepository) rely on prepared statements (bindValue/execute) to prevent SQL injection. - Presentation Security: The
GameRendererincludes a privatesanitizemethod usinghtmlspecialcharsto convert special characters to HTML entities, effectively preventing XSS attacks in the rendered output. - Atomic Operations: Deleting a game and its associated videos is performed within a database transaction by the
PdoGameWriteRepositoryto ensure atomicity; if one deletion fails, the transaction is rolled back.
🚀 Results & Learnings
The Hexagonal refactoring of the Game module successfully delivered on the core benefits promised by the architecture:
- Testability: Use cases, such as
CreateGameUseCaseorGetGamesUseCase, are now independent of database logic, requiring only a mock implementation of the Repository Interface during testing. - Flexibility: The system can easily swap out the current PDO implementation for a different persistence framework (e.g., Doctrine) simply by writing a new Adapter for the defined Repository Ports.
- Independence: The central Domain layer remains pristine, uncontaminated by infrastructure details like HTTP requests or database specifics.
This architecture acts like a power converter for data: the Use Cases define the standard current (business logic), the Repository Ports define the sockets (interfaces), and the Persistence Adapters (like PDO) act as the plugs, converting the standard current into the correct form needed by the external power source (the database).
🧩 Code Snippets
A. Domain Entity Example (Game) The core Game entity encapsulates data and validation.
declare(strict_types=1);
namespace Domain\Game;
class Game
{
public function __construct(
private readonly int $id,
private readonly string $simulador,
// ... other properties
) {
if ($id <= 0) {
throw new \InvalidArgumentException('Game ID must be a positive integer');
}
if (empty(trim($simulador))) {
throw new \InvalidArgumentException('Game name (simulador) cannot be empty');
}
}
// ... Getters
}
B. Application Use Case Example (CreateGameUseCase) The Use Case relies solely on the write repository port.
declare(strict_types=1);
namespace Application\Game\UseCase;
use Domain\Game\GameWriteRepositoryInterface;
class CreateGameUseCase
{
public function __construct(
private readonly GameWriteRepositoryInterface $repository
) {
}
public function execute(
string $simulador,
int $igdb,
// ... other parameters
): int {
return $this->repository->createGame(
$simulador,
$igdb,
// ... arguments
);
}
}
C. Infrastructure Adapter Example (Read Operation) The PdoGameRepository implements the read port using parameterized SQL queries.
declare(strict_types=1);
namespace Infrastructure\Persistence;
use Domain\Game\Game;
use Domain\Game\GameRepositoryInterface;
// ... (PDO imports)
class PdoGameRepository implements GameRepositoryInterface
{
// ... constructor
public function findGamesWithVideos(int $limit, int $offset): array
{
$sql = "SELECT * FROM `game`
WHERE id IN (SELECT DISTINCT game FROM gaming_video)
ORDER BY simulador
LIMIT :limit OFFSET :offset";
try {
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_map(
fn(array $row) => Game::fromArray($row),
$results
);
} catch (PDOException $e) {
// ... exception handling
}
}
}





