Skip to content

<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/RefactorDomainsToRepositoryWithSpecifications.md for this page in Markdown format</div>

Architectural Refactoring: Data Access Layer

This document details the recent major refactoring of the application's data access layer. The legacy BaseModel has been completely removed and replaced with a modern, flexible, and more powerful architecture based on established design patterns.

1. Problem Statement

The previous data access layer, centered around a monolithic BaseModel, had several key issues:

  • High Complexity: It mixed data access, query building, and relationship logic in one large class, making it difficult to understand and maintain.
  • Inflexibility: Implementing custom or complex queries (e.g., queries with multiple JOINs, GROUP BY, or HAVING clauses) was difficult and often required modifying the base class, which is not sustainable.
  • Poor Testability: The tight coupling made it hard to unit test query logic in isolation.
  • Difficult Customization: Overriding or extending data logic for specific clients was challenging and error-prone.

2. The New Architecture

To solve these problems, we have implemented a new architecture based on three core design patterns:

a. The Repository Pattern

The Repository is now the primary gateway for accessing domain entities.

  • Responsibility: Its only job is data persistence (fetching and saving entities). It hides the underlying database logic.
  • Implementation: Every domain entity now has its own repository (e.g., PageRepository, ArticleRepository). A BaseRepository provides common functionality like get(), match(), and the core relationship loading logic.

b. The Specification Pattern

This pattern is the key to flexible and complex query building.

  • Responsibility: A Specification object encapsulates a single piece of a query (e.g., a WHERE clause, an ORDER BY clause, pagination).
  • Implementation: Instead of passing messy arrays to services, we now pass a series of Specification objects. We have created a library of reusable specifications, including:
    • Filter (for WHERE and WHERE IN clauses)
    • Sort (for ORDER BY)
    • Pagination (for LIMIT and OFFSET)
    • WithRelations (to explicitly request eager loading)
    • Custom Specifications for complex logic, such as FilterByTag or FilterByCategorySlug, which encapsulate complex subqueries and joins.

c. The Relation Configurator Pattern

This pattern decouples a repository from its own relationship definitions, which is the key to making the system customizable.

  • Responsibility: A RelationConfigurator class has one job: to define and return the array of Relation objects for a specific repository.
  • Implementation: The BaseRepository now requires a RelationConfigurator in its constructor. The DI container automatically creates the repository and injects the correct configurator.

3. How to Use the New System

Previously, a service call might have looked like this:

php
// OLD WAY
$this->model->getAll($filters, $sorts, $page, $perPage, $relations);

Now, the service builds and uses specifications, which is much more explicit and readable:

php
// NEW WAY - Inside a Service
$specifications = [
    new Filter('is_published', 1),
    new Sort('blog_date', 'desc'),
    new Pagination($page, $perPage),
    new WithRelations(['author', 'tags'])
];

$data = $this->repository->match(...$specifications);

4. How to Customize for a Client

This new architecture makes client-specific customizations clean and simple.

Example Goal: Add a new page relationship to the Location entity for a specific client.

Step 1: Create a Custom Configurator

In the custom/ folder, create a new configurator that extends the default one and adds the new relation.

php
// custom/Domains/Map/Location/Repository/CustomLocationRepositoryConfigurator.php
namespace Custom\Domains\Map\Location\Repository;

use Advisable\Domains\Cms\Page\Repository\PageRepository;
use Advisable\Domains\Map\Location\Repository\LocationRepositoryConfigurator;
use Advisable\Domains\Support\Repository\Relation;

class CustomLocationRepositoryConfigurator extends LocationRepositoryConfigurator
{
    public function getRelations(): array
    {
        // Get all the default relations
        $relations = parent::getRelations();

        // Add our new custom relation
        $relations['page'] = new Relation(
            Relation::BELONGS_TO,
            PageRepository::class,
            'page_id' // The custom foreign key on the 'location' table
        );

        return $relations;
    }
}

Step 2: Override in the Custom DI Container

In custom/Domains/Map/container.php, simply tell the DI container to use your new configurator instead of the default one.

php
// custom/Domains/Map/container.php
use Custom\Domains\Map\Location\Repository\CustomLocationRepositoryConfigurator;
use Advisable\Domains\Map\Location\Repository\LocationRepositoryConfigurator;

// ...

// Create an alias to override the default configurator with our custom one.
$services->alias(LocationRepositoryConfigurator::class, CustomLocationRepositoryConfigurator::class);

That's it. The LocationRepository will now be automatically built with the new page relationship for this client, without ever touching the core source code.

5. Conclusion

This refactoring provides significant benefits:

  • Decoupling: Business logic, query logic, and data access are now separate concerns.
  • Maintainability: Code is easier to read, understand, and debug.
  • Testability: Repositories and specifications can be tested in isolation.
  • Flexibility: The system is now built for extension and customization, paving the way for future features and client-specific requirements.