friends-of-ddd / transaction-manager
DDD 的简单事务管理抽象
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.41
- phpunit/phpunit: ^9.6
README
该库引入了用于数据库持久化的解耦接口。考虑到领域驱动设计(DDD)
事务管理器:闭包中的所有内容要么提交,要么回滚。回调函数内的任何异常都会触发回滚。
Flusher:是一种减少每个请求中 flush 数量的机制。
:exclamation: 请参阅
friends-of-ddd/transaction-manager-doctrine
了解该库的 doctrine 实现。
目的
- 纯事务抽象。
- 框架和库无关。
- 易于测试。
- 作为 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,
);
}
}
逻辑终止
有些情况下,事务终止不是由于系统故障,而是根据预定义的逻辑。在这种情况下,您可以抛出 LogicTerminationInterface
或 LogicTerminationException
。
例如,在 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.*