Перейти к содержимому

Паттерн Unit of Work

Паттерн Unit of Work поддерживает список объектов, затронутых бизнес-транзакцией, и координирует запись изменений. Он гарантирует, что все связанные изменения фиксируются вместе или откатываются при ошибке.

  1. Transaction Management - Группируйте связанные операции
  2. Change Tracking - Отслеживайте модифицированные сущности
  3. Batch Operations - Оптимизируйте записи в базу данных
  4. Consistency - Обеспечивайте целостность данных
<?php
declare(strict_types=1);
namespace XoopsModules\MyModule\Infrastructure;
interface UnitOfWorkInterface
{
public function begin(): void;
public function commit(): void;
public function rollback(): void;
public function registerNew(object $entity): void;
public function registerDirty(object $entity): void;
public function registerDeleted(object $entity): void;
}
<?php
declare(strict_types=1);
namespace XoopsModules\MyModule\Infrastructure;
final class UnitOfWork implements UnitOfWorkInterface
{
private array $newEntities = [];
private array $dirtyEntities = [];
private array $deletedEntities = [];
private bool $inTransaction = false;
public function __construct(
private readonly \XoopsDatabase $db,
private readonly EntityMapperRegistry $mappers
) {}
public function begin(): void
{
if ($this->inTransaction) {
throw new \RuntimeException('Transaction already started');
}
$this->db->query('START TRANSACTION');
$this->inTransaction = true;
}
public function commit(): void
{
if (!$this->inTransaction) {
throw new \RuntimeException('No transaction in progress');
}
try {
$this->insertNew();
$this->updateDirty();
$this->deleteRemoved();
$this->db->query('COMMIT');
$this->clear();
} catch (\Exception $e) {
$this->rollback();
throw $e;
}
}
public function rollback(): void
{
if ($this->inTransaction) {
$this->db->query('ROLLBACK');
$this->clear();
}
}
public function registerNew(object $entity): void
{
$id = spl_object_id($entity);
$this->newEntities[$id] = $entity;
}
public function registerDirty(object $entity): void
{
$id = spl_object_id($entity);
if (!isset($this->newEntities[$id])) {
$this->dirtyEntities[$id] = $entity;
}
}
public function registerDeleted(object $entity): void
{
$id = spl_object_id($entity);
unset($this->newEntities[$id], $this->dirtyEntities[$id]);
$this->deletedEntities[$id] = $entity;
}
private function insertNew(): void
{
foreach ($this->newEntities as $entity) {
$mapper = $this->mappers->getMapper($entity::class);
$mapper->insert($entity);
}
}
private function updateDirty(): void
{
foreach ($this->dirtyEntities as $entity) {
$mapper = $this->mappers->getMapper($entity::class);
$mapper->update($entity);
}
}
private function deleteRemoved(): void
{
foreach ($this->deletedEntities as $entity) {
$mapper = $this->mappers->getMapper($entity::class);
$mapper->delete($entity);
}
}
private function clear(): void
{
$this->newEntities = [];
$this->dirtyEntities = [];
$this->deletedEntities = [];
$this->inTransaction = false;
}
}
final class ArticleService
{
public function __construct(
private readonly UnitOfWorkInterface $unitOfWork,
private readonly ArticleRepository $articles,
private readonly CommentRepository $comments
) {}
public function publishWithComments(
Article $article,
array $comments
): void {
$this->unitOfWork->begin();
try {
// Mark article as modified
$article->publish();
$this->unitOfWork->registerDirty($article);
// Add new comments
foreach ($comments as $comment) {
$this->unitOfWork->registerNew($comment);
}
// Commit all changes together
$this->unitOfWork->commit();
} catch (\Exception $e) {
$this->unitOfWork->rollback();
throw $e;
}
}
}
final class ArticleRepository implements ArticleRepositoryInterface
{
public function __construct(
private readonly UnitOfWorkInterface $unitOfWork,
private readonly ArticleMapper $mapper
) {}
public function add(Article $article): void
{
$this->unitOfWork->registerNew($article);
}
public function update(Article $article): void
{
$this->unitOfWork->registerDirty($article);
}
public function remove(Article $article): void
{
$this->unitOfWork->registerDeleted($article);
}
public function findById(ArticleId $id): ?Article
{
return $this->mapper->findById($id);
}
}
  1. Short Transactions - Держите транзакции короткими
  2. Single Responsibility - Один unit of work per бизнес-операция
  3. Clear Boundaries - Четко определяйте область транзакции
  4. Error Handling - Всегда обрабатывайте откат при ошибке
  5. Avoid Nested - Не вкладывайте units of work
  • Repository-Layer - Repository pattern
  • Service-Layer - Service pattern
  • ../Database/Database-Schema - Database operations
  • Domain-Model - Domain entities