Appearance
<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, orHAVINGclauses) 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). ABaseRepositoryprovides common functionality likeget(),match(), and the core relationship loading logic.
b. The Specification Pattern
This pattern is the key to flexible and complex query building.
- Responsibility: A
Specificationobject encapsulates a single piece of a query (e.g., aWHEREclause, anORDER BYclause, pagination). - Implementation: Instead of passing messy arrays to services, we now pass a series of
Specificationobjects. We have created a library of reusable specifications, including:Filter(forWHEREandWHERE INclauses)Sort(forORDER BY)Pagination(forLIMITandOFFSET)WithRelations(to explicitly request eager loading)- Custom Specifications for complex logic, such as
FilterByTagorFilterByCategorySlug, 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
RelationConfiguratorclass has one job: to define and return the array ofRelationobjects for a specific repository. - Implementation: The
BaseRepositorynow requires aRelationConfiguratorin 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.