friends-of-ddd/transaction-manager-doctrine

Doctrine 的简单事务管理实现

0.2.1 2024-03-27 05:11 UTC

This package is auto-updated.

Last update: 2024-09-25 07:41:45 UTC


README

friends-of-ddd/transaction-manager 库的 doctrine 实现。

  • 事务管理器:闭包中的所有操作要么提交,要么回滚。回调函数中的任何异常都会导致回滚。

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

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

目的

  • 纯事务抽象。
  • 易于测试。
  • 是 doctrine 的 EntityManagerInterface 的替代品,它被各种数据库操作污染。(违反 SOLID接口隔离原则
  • 支持 Doctrine 的活跃和测试版测试覆盖

安装

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

事务管理器

配置

Symfony

无自动清除的初始化

# config/services.yaml:

services:
  FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\DoctrineTransactionManager: ~
  
  FriendsOfDdd\TransactionManager\Domain\TransactionManagerInterface:
    '@FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\DoctrineTransactionManager'

带自动清除的初始化

# config/services.yaml:

services:
  FriendsOfDdd\TransactionManager\Domain\TransactionManagerInterface:
    class: FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\DoctrineClearingTransactionManagerDecorator
    factory: '@FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\Factory\ClearingTransactionManagerFactory'

示例


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);
            }
        );
    }
}

自动清除

事务管理器可以带自动清除初始化。(见 配置

在任何事务提交或回滚后,所有实体对象都会自动从 Doctrine 缓存中清除/分离。

测试的模拟实现

您可以使用 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

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

配置

Symfony

通过工厂类同时具有懒加载和清除功能进行初始化

# config/services.yaml:

services:
  FriendsOfDdd\TransactionManager\Application\FlusherInterface:
    class: FriendsOfDdd\TransactionManager\Infrastructure\Flusher\LazyFlusherDecorator
    factory: '@FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\Factory\LazyFlusherWithClearingFactory'
    arguments:
      $maxBufferSize: 100

仅通过工厂类具有懒加载功能的初始化

# config/services.yaml:

services:
  FriendsOfDdd\TransactionManager\Application\FlusherInterface:
    class: FriendsOfDdd\TransactionManager\Infrastructure\Flusher\LazyFlusherDecorator
    factory: '@FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\Factory\LazyFlusherFactory'
    arguments:
      $maxBufferSize: 100

通过服务装饰器手动初始化

# config/services.yaml:

services:
  FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\DoctrineFlusher: ~

  FriendsOfDdd\TransactionManager\Application\FlusherInterface:
    '@FriendsOfDdd\TransactionManager\Infrastructure\Doctrine\DoctrineFlusher'

  FriendsOfDdd\TransactionManager\Infrastructure\Flusher\DoctrineClearingFlusherDecorator:
    decorates: '@FriendsOfDdd\TransactionManager\Application\FlusherInterface'
    decoration_priority: 0
    arguments:
      $originalFlusher: '@FriendsOfDdd\TransactionManager\Infrastructure\Flusher\LazyFlusherDecorator.inner'

  FriendsOfDdd\TransactionManager\Infrastructure\Flusher\LazyFlusherDecorator:
    decorates: '@FriendsOfDdd\TransactionManager\Application\FlusherInterface'
    decoration_priority: 1
    arguments:
      $originalFlusher: '@FriendsOfDdd\TransactionManager\Infrastructure\Flusher\LazyFlusherDecorator.inner'
      $maxBufferSize: 100 # Optional: will flush after the amount of callbacks completed.

懒加载 Flusher

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() 调用时都调用,而是在最后只调用一次。

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

清除 Flusher

清除用于内存使用优化。一旦数据被刷新,建议从 doctrine 缓存中清除(分离)实体对象。

DoctrineClearingFlusherDecorator 在 flusher 接口调用刷新后运行 \Doctrine\ORM\EntityManager::clear()

如果您将其与 LazyFlusherDecoraror 结合使用,则清除仅在实际刷新后进行,例如在达到最大缓冲区限制后。

空 Flusher

对于测试目的(InMemory 存储库),如果您不需要刷新,可以使用 VoidOrmSessionFlusher 实现。

支持的 PHP 版本

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

支持的 Doctrine 版本

  • 2.19.7+
  • 3.2.2+