Gaming Videos Project

December 03, 2025 09:27pm

Technical Deep Dive: Implementing the Game Module with Hexagonal Architecture. A technical overview of the Game module's architectural overhaul, demonstrating clear separation of concerns, enhanced testability, and domain independence using the Ports and Adapters pattern.

💡 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 Game and Video. The Game entity validates that the ID is positive and the simulador (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: GameRepositoryInterface defines methods for retrieval, such as findGamesWithVideos (paginated list of games with associated videos), findById, and methods to count games or find videos by ID (findVideosByGameId).
    • Write Port (Command Port): GameWriteRepositoryInterface defines mutation operations like createGame, updateGame, createVideo, and the crucial transactional method deleteGameAndVideos.
  • 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: GetGamesUseCase retrieves paginated game lists. GetGameUseCase retrieves a single game by ID and throws GameNotFoundException if the result is null. GetVideosUseCase handles retrieval and sorting of videos for a specific game ID.
    • Write Examples (Admin Commands): CreateGameUseCase, UpdateGameUseCase, and DeleteGameUseCase utilize the GameWriteRepositoryInterface to perform their respective duties.
  • 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 Domain Game or Video entities using fromArray methods.
    • 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: GameRenderer is the presentation adapter responsible for converting DTOs into HTML output. It contains methods like displayGamesList and displayGame.

🖥️ 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 GameRenderer handles 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 (GetGamesUseCase takes limit and offset). 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 GameRenderer includes a private sanitize method using htmlspecialchars to 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 PdoGameWriteRepository to 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:

  1. Testability: Use cases, such as CreateGameUseCase or GetGamesUseCase, are now independent of database logic, requiring only a mock implementation of the Repository Interface during testing.
  2. 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.
  3. 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
        }
    }
}

go to games list