friends-of-ddd / transaction-manager-doctrine
Doctrine 的简单事务管理实现
Requires
- php: >=8.0
- doctrine/orm: ^2.19.3|^3.1.1|^4.0
- friends-of-ddd/transaction-manager: ^0.1.1
Requires (Dev)
- ext-pdo_pgsql: *
- friendsofphp/php-cs-fixer: ^3.41
- phpunit/phpunit: ^9.6
- ramsey/uuid: ^4.7
- ramsey/uuid-doctrine: ^2.0
- symfony/cache: ^5.4
README
friends-of-ddd/transaction-manager
库的 doctrine 实现。
事务管理器:闭包中的所有操作要么提交,要么回滚。回调函数中的任何异常都会导致回滚。
Flusher:是一种减少每个请求中 flush 次数的机制。
目的
- 纯事务抽象。
- 易于测试。
- 是 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,
);
}
}
逻辑终止
有些情况下,事务的终止不是由于系统错误,而是根据预定义的逻辑。在这种情况下,您可以抛出 LogicTerminationInterface
或 LogicTerminationException
。
例如,在 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+