Persistence (Eloquent)
Eloquent is the default Laravel persistence driver. It belongs only in Infrastructure repositories (and related models/factories). Application and Domain speak Entities. Examples use greenfield Ordering.
Folders
Ordering/Infrastructure/Persistence/Eloquent/
├── Models/OrderModel.php
├── Repositories/OrderRepository.php
├── Factories/… # optional
└── Migrations/… # see Migrations page
Domain port:
Ordering/Domain/Ports/Order/OrderRepositoryInterface.php
Layer responsibilities
| Component | Layer | Purpose |
|---|---|---|
| Port (interface) | Domain | Contract (findById, save, …) |
| Implementation | Infrastructure | Eloquent / SQL |
| Entity mapping | Infrastructure | Model ↔ Entity |
Domain port
namespace App\Modules\Ordering\Domain\Ports\Order;
use App\Modules\Ordering\Domain\Entities\OrderEntity;
interface OrderRepositoryInterface
{
public function findById(int $id): ?OrderEntity;
public function save(OrderEntity $entity): void;
}
Infrastructure adapter
namespace App\Modules\Ordering\Infrastructure\Persistence\Eloquent\Repositories;
use App\Modules\Ordering\Domain\Entities\OrderEntity;
use App\Modules\Ordering\Domain\Ports\Order\OrderRepositoryInterface;
use App\Modules\Ordering\Infrastructure\Persistence\Eloquent\Models\OrderModel;
class OrderRepository implements OrderRepositoryInterface
{
public function findById(int $id): ?OrderEntity
{
$model = OrderModel::find($id);
if (! $model) {
return null;
}
return new OrderEntity(
id: $model->getId(),
status: $model->getStatus(),
amount: $model->getAmount(),
);
}
public function save(OrderEntity $entity): void
{
OrderModel::updateOrCreate(
['id' => $entity->id],
[
'status' => $entity->status,
'amount' => $entity->amount,
],
);
}
}
Read models via getters; write via setters / explicit attribute arrays. Direct $model->attribute access on Eloquent models is forbidden in host standards that enforce encapsulation.
Eloquent access is repository-only
The Repository (and its base class) is the only place allowed to touch Eloquent models directly. Use Cases, Domain Services, and other Infrastructure services must obtain data through a Repository.
// ❌ WRONG — Infrastructure helper reaching for the model statically
$model = OrderModel::find($orderId);
// ✅ CORRECT — go through the Repository (Domain Port from Application)
$entity = $this->orders->findById($orderId);
- Domain / Application always depend on the Port and receive Entities.
- Infrastructure services that genuinely need the persistence model (search index, reporting) may depend on the concrete Repository and a dedicated
findModel()-style method kept off the Domain Port. - Forbidden outside the Repository:
Model::find(),Model::where(),Model::create(),Model::query(),Model::updateOrCreate(), and similar.
Why manual mapping is required
| Reason | Effect |
|---|---|
| Schema decoupling | Rename a column → touch Repository only |
| Strict business rules | Use Cases operate on Entities, not rows |
| Encapsulation | Sensitive columns never leak via Entity |
Model pattern (Laravel host)
Eloquent models live under Infrastructure/Persistence/Eloquent/Models/. They are not Domain Entities. Prefer:
- Getters / setters for every persisted field used by mapping
- No business policy methods that belong on Entities
- Factories colocated for tests
Anti-patterns
| Anti-pattern | Fix |
|---|---|
OrderEntity extends Model | Plain Entity + Infrastructure Model |
Use Case calls OrderModel::query() | Inject OrderRepositoryInterface |
| Repository returns paginator of Models | Map items to Entities |
| Cross-module Eloquent relations for business joins | ACL / Events |
Golden line
UI → UseCase → Port → Eloquent Repository → Domain Entity
Core reference: Ports & persistence.