Skip to content

Domain · Application · Adapter

Repositories

Keep query shapes in the Domain, transaction intent in the Application, and database work in an adapter.

Application code requests one atomic operation without depending on an ORM, framework connection, or database driver.

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.

composer require johnnickell/fight-common

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

  1. Pagination
  2. ResultSet
  3. TransactionalUnitOfWork Interface
  4. DoctrineTransactionalUnitOfWork
  5. CodeIgniterTransactionalUnitOfWork
  6. Laravel and Yii transactional adapters
  7. Usage in a Repository Interface
  8. 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();