The five-layer model#
The foundation of the whole system. Without understanding it the rest of the documentation makes no sense — and above all the code becomes unnavigable, because the same thing has a different name and a different role in every layer.
The model#
Presenter thin; it only calls a manager
↓
Manager the domain's public interface, lifecycle hooks
↓
Service a thin layer, holds the counts, delegates
↓
Mapper turns raw data into entities
↓
Repository the ONLY place with SQL
| Layer | May | May not |
|---|---|---|
| Presenter | call a manager, fill the template | query the database, hold domain logic |
| Manager | domain logic, hooks, orchestration; inject other managers | write SQL |
| Service | delegate, hold totalCount |
make domain decisions, inject other services |
| Mapper | hydrate data into entities | write SQL |
| Repository | SQL and the QueryBuilder | domain logic |
A shortcut across a layer always comes back
A query from a presenter straight into a repository works — until somebody needs the same thing from another section. Then the logic is duplicated and one of the copies stops being fixed.
The manager is the ONLY layer allowed to inject other managers
Service, Mapper and Repository are closed: a service holds only its mapper, a mapper
only its repository. An XxxRunner or XxxResolver in Models/Services/ that injects
a manager is a violation — it belongs in the manager tier next to the entity manager.
The front end does not touch the database#
The most important consequence of the model. Neither the front end nor the
administration has database access. They request data from the Api section through the
bridge (app/Core/Bridge/ApiBridge.php).
Front Manager → Service → Mapper → Repository → ApiBridge
↓
an internal call (no HTTP) or an HTTP request
↓
Api presenter
The same module has several sets of entities
App\UI\Front\Eshop\Models\Entities\Product and
App\UI\Api\Eshop\Models\Entities\Product are two different classes. The Api
entity is a Doctrine entity over the table, the Front entity is the result of hydrating
a response. Mixing the two up is the most common beginner's mistake in this system —
and it surfaces as "a TypeError somewhere entirely different".
What flows between sections is an ARRAY, not an object
An array goes across the bridge. The object on the other side only comes into being
through hydration — which is why there are two hydrators,
one for each side.
A front manager can only read#
The base front manager has read traits only — GetTrait, GetByTrait, GetAllTrait,
TotalCountTrait. Writing has to be asked for explicitly:
use App\Core\Traits\Shared\Models\Managers\DeleteTrait;
use App\Core\Traits\Shared\Models\Managers\SaveTrait;
class AdvertFollowerManager extends BaseManager
{
use SaveTrait;
use DeleteTrait;
}
25 out of 111 front managers do so. The Admin and Api base classes have writing built in.
It is a deliberate brake, not an oversight
A write from the public site is an exception that should be visible in the code — not something that "just works".
Base classes#
Every layer has an ancestor in app/Core/Base/, separately for each section:
app/Core/Base/
Api/ Front/ Admin/ Cron/ Script/ Shared/
Models/Managers/Manager.php
Models/Services/…
Models/Mappers/…
Models/Repositories/…
The shared implementations live in app/Core/Traits/<Section>/Models/.
The mapper has two shapes#
| Where | Folder | What it does |
|---|---|---|
| an Api module | Mappers/Db/ |
maps over the database |
| a Front/Admin module | Mappers/Api/ |
hydrates the API response |
It is the same layer in two different roles — which is why it has the same name.
Procedure: adding reads for a new entity#
- The Api side: entity → repository (SQL) → mapper → service → manager →
get*Data()in the presenter with a permission guard. - A permission key in the database, shaped
:Api:<Module>:<Entity>:<action>. - The Front/Admin side: a variant of the entity with hydration attributes → a
repository with an
apiEndpoint→ mapper → service → manager. - The presenter injects the manager only and calls
findAll(…)->execute(). - Verify both paths — through a page and directly through the bridge.
In detail: Calling the API and Code conventions.
Why it is like this#
The administration, the front end and cron all take the same Api path. Domain logic in a manager therefore holds for every entry point at once — it does not have to be written three times and it cannot drift apart.
A side effect: the API is finished first
Because the front end uses the Api itself, the API is functional for third parties from the start rather than as an afterthought. See the API manual.
The price is one extra bottleneck
Every read goes through get*Data(), and therefore through the authorisation guard and
through clearDoctrine(). The latter is the
system's most serious landmine.
Follow-up chapters: Project structure · Calling the API · Hydrators · Identity map · Code conventions