Ports and persistence
Purpose: persist and load Domain data without teaching Use Cases about an ORM.
Component map (one feature)
Typical folders
Application/DTO/{Capability}/CreateXDTO.php
Application/UseCases/{Capability}/CreateXUseCase.php
Domain/Entities/XEntity.php
Domain/Ports/{Capability}/XRepositoryInterface.php
Infrastructure/Persistence/.../XRecord.php # host-specific
Infrastructure/Persistence/.../XRepository.php # implements port
Core requires: manual mapping and no ORM types in Domain/Application.
Domain/Ports/{Capability}/XRepositoryInterface.php
Infrastructure/Persistence/.../XRepository.php # implements port
Infrastructure/Persistence/.../XRecord.php # host-specific row type
Map Entity ↔ record inside the adapter. Application never sees the record type. Persistence access is repository-only; Use Cases depend on the port and receive Domain Entities.
Illustrative host mappings — not Core MUST
Persistence quarantine (Infrastructure only)
| Host | Typical place |
|---|---|
| Laravel | Eloquent models / repositories under `…/Eloquent/` |
| Symfony | Doctrine entities / repositories under `…/Doctrine/` |
| Yii | Cycle (Yii3) or ActiveRecord (Yii2 brownfield) in Infra |
| CodeIgniter | CI Models / Query Builder only in Infra repositories |
| CakePHP | Table / Entity ORM types quarantined in Infra |
| Spiral | Cycle ORM via cycle-bridge in Infra |
| Slim | Bring-your-own ORM/SQL — still Infra-only |
| Mezzio | Bring-your-own (laminas-db, Doctrine, …) — Infra-only |
Full topic pages: Adapters overview
Port sketch
interface OrderRepositoryInterface
{
public function save(OrderEntity $order): OrderEntity;
public function findByCode(string $code): ?OrderEntity;
}
public function __construct(
private OrderRepositoryInterface $orders,
) {}
The composition root binds the port to a concrete adapter.
Mapping rules
| Direction | Where | Input → Output |
|---|---|---|
| Write | Repository adapter | XEntity → record attributes → DB |
| Read | Repository adapter | DB → record → XEntity |
| HTTP out | UI | Entity fields → JSON / resource |
Never return an ORM model (or query builder) from the repository to the Use Case.
Sequence — create then return
Nested helpers still use ports
When PlaceOrderUseCase delegates to PrepareOrderLines:
- Helpers use the same Domain ports.
- Helpers must not become UI entry points.
- Peer calls (Warehouse, Directory) are ACL ports, not this module’s repositories — see cross-module ACL.
Anti-patterns
| Anti-pattern | Fix |
|---|---|
| Entity extends ORM model | Entity = plain PHP; record only in Infrastructure |
| Repository returns paginator of ORM models | Map items to Entities (or a Domain page DTO) |
| Use Case builds SQL strings | Belong in the repository adapter |
| Sharing ORM relations across modules | Use ACL / Events; no cross-module business joins via foreign models |
Next: cross-module ACL.