friends-of-ddd/transaction-manager

DDD 的简单事务管理抽象

0.1.1 2024-03-01 04:42 UTC

This package is auto-updated.

Last update: 2024-08-30 05:01:11 UTC


README

该库引入了用于数据库持久化的解耦接口。考虑到领域驱动设计(DDD)

  • 事务管理器:闭包中的所有内容要么提交,要么回滚。回调函数内的任何异常都会触发回滚。

  • Flusher:是一种减少每个请求中 flush 数量的机制。

:exclamation: 请参阅 friends-of-ddd/transaction-manager-doctrine 了解该库的 doctrine 实现。

  1. 目的
  2. 安装
  3. 事务管理器
  4. Flusher
  5. 支持的 PHP 版本

目的

  • 纯事务抽象。
  • 框架和库无关。
  • 易于测试。
  • 作为 doctrine 的 EntityManagerInterface 的替代品,后者被各种数据库操作污染。(违反了 SOLID 的接口隔离原则)
  • 支持流行的 PHP ORM(Doctrine,Eloquent),具有活跃和 beta 版本的测试覆盖率

安装

Doctrine

composer require friends-of-ddd/transaction-manager-doctrine

事务管理器

示例


use FriendsOfDdd\TransactionManager\Domain\TransactionManagerInterface;

final readonly class MoneyTransferService
{
    public function __construct(
        private AccountBalanceRepositoryInterface $accountBalanceRepository,
        private TransactionManagerInterface $transactionManager,
    ) {
    }

    public function transfer(Money $amount, AccountBalance $fromAccount, AccountBalance $toAccount): void
    {
        $fromAccount->withdraw($amount);
        $toAccount->topUp($amount);

        $this->transactionManager->wrapInTransaction(
            function () use ($fromAccount, $toAccount) {
                $this->accountBalanceRepository->save($fromAccount);
                $this->accountBalanceRepository->save($toAccount);
            }
        );
    }
}

测试的模拟实现

您可以使用 MockedTransactionManager 类在测试中模拟事务管理器。


use PHPUnit\Framework\TestCase;
use FriendsOfDdd\TransactionManager\Infrastructure\MockedTransactionManager;

final class MoneyTransferServiceTest extends TestCase
{
    private MoneyTransferService $moneyTransferService;
    private InMemoryAccountBalanceRepository $accountBalanceRepository;
    private MockedTransactionManager $transactionManager;

    protected function setUp(): void 
    {
        $this->accountBalanceRepository = new InMemoryAccountBalanceRepository();
        $this->transactionManager = new MockedTransactionManager();
        $this->moneyTransferService = new MoneyTransferService(
            $this->accountBalanceRepository,
            $this->transactionManager,
        );
    }

    public function testTransactionDoesNotFail(): void 
    {
        // Arrange
        $fromAccount = AccountBalanceMother::createWithAmount(Amount::fromInt(100));
        $this->accountBalanceRepository->save($fromAccount);

        $toAccount = AccountBalanceMother::createWithAmount(Amount::fromInt(0));
        $this->accountBalanceRepository->save($toAccount);
    
        // Act
        $this->moneyTransferService->transfer(
            Amount::fromFloat(19.99),
            $fromAccount,
            $toAccount,
        );
        
        // Assert
        self::assertEquals(
            Amount::fromFloat(80.01),
            $this->accountBalanceRepository->getById($fromAccount->getId())->getBalance()
        );
        self::assertEquals(
            Amount::fromFloat(19.99),
            $this->accountBalanceRepository->getById($toAccount->getId())->getBalance()
        );
    }

    public function testTransactionFails(): void 
    {
        // Arrange
        $fromAccount = AccountBalanceMother::createWithAmount(Amount::fromInt(100));
        $this->accountBalanceRepository->save($fromAccount);

        $toAccount = AccountBalanceMother::createWithAmount(Amount::fromInt(0));
        $this->accountBalanceRepository->save($toAccount);
        
        $expectedException = new RuntimeException();
        $this->transactionManager->expectedException = $expectedException;
        
        // Assert
        $this->expectExceptionObject($expectedException);
    
        // Act
        $this->moneyTransferService->transfer(
            Amount::fromFloat(19.99),
            $fromAccount,
            $toAccount,
        );
    }
}

逻辑终止

有些情况下,事务终止不是由于系统故障,而是根据预定义的逻辑。在这种情况下,您可以抛出 LogicTerminationInterfaceLogicTerminationException

例如,在 doctrine 中,任何从回调抛出的异常如果不是 LogicTerminationInterface 的实例,都会关闭数据库连接,因此连接变得不可写。

Flusher

ORM 如 doctrine 会将持久化实体存储在内存中,直到它们在真实的数据库中刷新。为了控制刷新(不要对每个对象都进行刷新),您可以使用 FlusherInterface

懒加载 Flush

LazyFlusherDecoraror 只会在闭包函数中包含的所有代码执行一次时刷新。


use FriendsOfDdd\TransactionManager\Application\FlusherInterface;

final readonly class AccountStatementGenerator
{
    public function __construct(
        private AccountRepositoryInterface $accountRepository,
        private AccountStatementRepositoryInterface $accountStatementRepository,
        private AccountStatementFactory $accountStatementFactory,
        private FlusherInterface $flusher,
    ) {
    }

    public function generateStatementsByUserId(UserId $userId): void
    {
        $userAccounts = $this->accountRepository->getAllByUserId($userId);
        
        $this->flusher->flushOnComplete(
            function () use ($userAccounts) {
                foreach ($userAccounts as $userAccount) {
                    $accountStatement = $this->accountStatementFactory->create($userAccount->getId());
                    $this->accountStatementRepository->save($accountStatement);
                }
            }
        );
    }
}


use Doctrine\ORM\EntityManagerInterface;
use FriendsOfDdd\TransactionManager\Application\FlusherInterface;

final readonly class DoctrineAccountStatementRepository implements AccountStatementRepositoryInterface
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private FlusherInterface $flusher,
    ) {
    }
    
    public function save(AccountStatement $accountStatement): void 
    {
        $this->flusher->flushOnComplete(
            function () {
                $this->entityManager->persist();
            }
        );
    }
}

EntityManagerInterace::flush() 不会在每次 save() 调用时被调用,而只会在结束时调用一次。

您可以通过在懒加载 Flush 构造函数参数中配置 $maxBufferSize 来指定在完成一定数量的回调后进行刷新。

空 Flush

出于测试目的(内存存储库),当您不需要刷新时,可以使用 VoidOrmSessionFlusher 实现。

支持的 PHP 版本

  • 8.0.*
  • 8.1.*
  • 8.2.*
  • 8.3.*