Domain · Application · Adapter
Repositories
Keep query shapes in the Domain, transaction intent in the Application, and database work in an adapter.
Repositories keep a Domain-facing query shape and Application transaction boundary separate from
the database technology that fulfills them. Define a repository interface in your Domain, inject it
into the use case, and bind an implementation plus TransactionalUnitOfWork in the composition
root.
Ownership. Pagination and ResultSet are Domain DTOs. TransactionalUnitOfWork is an
Application port. Doctrine, Laravel, CodeIgniter, and Yii transaction implementations are Adapter code; a
consumer owns its repository implementation, connection selection, migrations, and outbox policy.
Dependencies. The portable DTOs and port require PHP 8.5+ and this package. The optional
Doctrine adapter requires doctrine/orm; Laravel requires laravel/framework; CodeIgniter requires
codeigniter4/framework; and Yii requires yiisoft/db (plus the consumer's Yii DI setup when its
provider is used).
Install.
Start with a portable application boundary.
use Fight\Common\Application\Repository\TransactionalUnitOfWork;
use Fight\Common\Domain\Repository\Pagination;
use Fight\Common\Domain\Repository\ResultSet;
final readonly class ListOrders
{
public function __construct(
private OrderRepository $orders,
private TransactionalUnitOfWork $unitOfWork,
) {}
public function handle(): ResultSet
{
return $this->unitOfWork->commitTransactional(
fn (): ResultSet => $this->orders->findAll(new Pagination(page: 1, perPage: 20)),
);
}
}
commitTransactional() propagates a failed callback and the chosen adapter rolls back its database
transaction. The supplied Doctrine, Laravel, CodeIgniter, and Yii adapters reject nested portable transactions
with LogicException; check isClosed() before reusing a unit of work after infrastructure
failure. Do not infer database validation or cross-system atomicity from the portable interface.
Reference¶
Standard DTOs for paginated repository queries (Pagination as input, ResultSet as output) and the narrow TransactionalUnitOfWork boundary for transaction management. Shipped adapters cover Doctrine, Laravel, CodeIgniter, and Yii.
Domain\Repository
├── Pagination — input: page, perPage, orderings
└── ResultSet — output: records + pagination metadata
Application\Repository
├── TransactionalUnitOfWork (canonical interface)
└── UnitOfWork (deprecated 1.x compatibility interface)
Adapter\Persistence
├── Doctrine\DoctrineTransactionalUnitOfWork
├── Laravel\LaravelTransactionalUnitOfWork
├── CodeIgniter\CodeIgniterTransactionalUnitOfWork
└── Yii\YiiTransactionalUnitOfWork
Adapter\Repository
└── DoctrineUnitOfWork (deprecated 1.x compatibility adapter)
Table of Contents¶
- Pagination
- ResultSet
- TransactionalUnitOfWork Interface
- DoctrineTransactionalUnitOfWork
- CodeIgniterTransactionalUnitOfWork
- Laravel and Yii transactional adapters
- Usage in a Repository Interface
- Deprecated 1.x Compatibility
Pagination¶
Fight\Common\Domain\Repository\Pagination
An immutable input DTO for paginated repository methods. Pre-computes offset and limit from page and perPage.
use Fight\Common\Domain\Repository\Pagination;
$pagination = new Pagination(
page: 2,
perPage: 20,
orderings: ['createdAt' => 'DESC', 'name' => 'ASC']
);
$pagination->page(); // 2
$pagination->perPage(); // 20
$pagination->offset(); // 20
$pagination->limit(); // 20
$pagination->orderings(); // ['createdAt' => 'DESC', 'name' => 'ASC']
| Method | Returns | Notes |
|---|---|---|
page() |
int |
Defaults to Pagination::DEFAULT_PAGE (1) |
perPage() |
int |
Defaults to Pagination::DEFAULT_PER_PAGE (100) |
offset() |
int |
Computed: (page - 1) * perPage |
limit() |
int |
Same as perPage |
orderings() |
array |
Values normalized to ASC / DESC |
Constants: Pagination::ASC, Pagination::DESC, Pagination::DEFAULT_PAGE, Pagination::DEFAULT_PER_PAGE.
ResultSet¶
Fight\Common\Domain\Repository\ResultSet
An output DTO wrapping a typed ArrayList of records together with pagination metadata. Implements Collection (Countable + IteratorAggregate), Arrayable, and JsonSerializable.
use Fight\Common\Domain\Repository\ResultSet;
use Fight\Common\Domain\Collection\ArrayList;
$records = ArrayList::of(User::class);
$records->add($user1);
$records->add($user2);
$result = new ResultSet(
page: 2,
perPage: 20,
totalRecords: 150,
records: $records
);
$result->page(); // 2
$result->perPage(); // 20
$result->totalPages(); // 8
$result->totalRecords(); // 150
$result->records(); // ArrayList<User>
$result->isEmpty(); // false
$result->count(); // 2
// Implements Collection — iterable
foreach ($result as $user) { /* ... */ }
// Serializable
$result->toArray();
// [
// 'page' => 2,
// 'per_page' => 20,
// 'total_pages' => 8,
// 'total_records' => 150,
// 'records' => [ ... ]
// ]
json_encode($result); // same structure
TransactionalUnitOfWork Interface¶
Fight\Common\Application\Repository\TransactionalUnitOfWork
Defines the canonical application boundary for running a complete operation atomically without coupling application services to a specific ORM.
interface TransactionalUnitOfWork
{
public function commitTransactional(callable $operation): mixed;
public function isClosed(): bool;
}
| Method | Purpose |
|---|---|
commitTransactional(callable) |
Wraps the operation in a transaction; returns the operation's result |
isClosed() |
Whether the unit of work is still usable (e.g. after a rollback) |
DoctrineTransactionalUnitOfWork¶
Fight\Common\Adapter\Persistence\Doctrine\DoctrineTransactionalUnitOfWork
The Doctrine ORM adapter wraps EntityManagerInterface and implements only TransactionalUnitOfWork.
use Fight\Common\Adapter\Persistence\Doctrine\DoctrineTransactionalUnitOfWork;
$unitOfWork = new DoctrineTransactionalUnitOfWork($entityManager);
$result = $unitOfWork->commitTransactional(function () use ($users, $command) {
$user = User::register($command->email, $command->name);
$users->save($user);
return $user->id();
});
| Method | Delegates to |
|---|---|
commitTransactional($operation) |
$entityManager->wrapInTransaction($operation) |
isClosed() |
!$entityManager->isOpen() |
CodeIgniterTransactionalUnitOfWork¶
Fight\Common\Adapter\Persistence\CodeIgniter\CodeIgniterTransactionalUnitOfWork adapts one explicitly
selected CodeIgniter database connection to TransactionalUnitOfWork. Register it only from the project-owned
Config\Services persistence capability delegate; selecting messaging does not bind it.
use Fight\Common\Adapter\ServiceContainer\CodeIgniter\PersistenceServices;
return PersistenceServices::transactionalUnitOfWork(db_connect());
The adapter begins, checks, commits, and rolls back the native transaction around one callback. It rejects nested portable transactions. Connection selection, transaction-exception policy, and migrations remain application configuration. Any outbox remains application configuration.
Laravel and Yii transactional adapters¶
Fight\Common\Adapter\Persistence\Laravel\LaravelTransactionalUnitOfWork wraps one Laravel
Illuminate\Database\Connection; the shipped Laravel PersistenceServiceProvider binds it to
TransactionalUnitOfWork using the application's db.connection. It requires laravel/framework.
Fight\Common\Adapter\Persistence\Yii\YiiTransactionalUnitOfWork wraps a Yii
Yiisoft\Db\Connection\ConnectionInterface; the shipped Yii PersistenceServiceProvider returns the
corresponding DI definition. It requires yiisoft/db and the consumer's Yii DI configuration.
Both adapters reject nested portable transactions. Yii also rejects execution on a connection it has observed as closed; consumers still own connection selection, migration, retry, and outbox policy.
Usage in a Repository Interface¶
The complete pattern for a repository interface using both DTOs and the canonical transaction boundary:
use Fight\Common\Domain\Repository\Pagination;
use Fight\Common\Domain\Repository\ResultSet;
use Fight\Common\Application\Repository\TransactionalUnitOfWork;
interface UserRepository
{
public function find(UserId $id): ?User;
public function findAll(Pagination $pagination): ResultSet;
public function save(User $user): void;
public function remove(UserId $id): void;
}
class RegisterUserService
{
public function __construct(
private UserRepository $users,
private TransactionalUnitOfWork $unitOfWork
) {}
public function execute(RegisterUserCommand $command): void
{
$this->unitOfWork->commitTransactional(function () use ($command): void {
$user = User::register($command->email, $command->name);
$this->users->save($user);
});
}
}
Deprecated 1.x compatibility¶
Fight\Common\Application\Repository\UnitOfWork and
Fight\Common\Adapter\Repository\DoctrineUnitOfWork remain functional throughout 1.x without runtime
deprecation notices. Their standalone UnitOfWork::commit() journey is deprecated 1.x compatibility, not the
path for new consumers. Migrate new and existing transaction boundaries to TransactionalUnitOfWork and
DoctrineTransactionalUnitOfWork:
use Fight\Common\Adapter\Repository\DoctrineUnitOfWork;
// Deprecated 1.x compatibility only.
$legacyUnitOfWork = new DoctrineUnitOfWork($entityManager);
$legacyUnitOfWork->commit();