friends-of-ddd/transaction-manager-doctrine 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

friends-of-ddd/transaction-manager-doctrine

Composer 安装命令:

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

包简介

Simple transaction manager implementation for Doctrine

README 文档

README

A doctrine implementation of friends-of-ddd/transaction-manager library.

  • Transaction manager: everything in a closure either gets committed or rolled back. Any exception inside callback causes rollback.

  • Flusher: is a mechanism to reduce the amount of flushes per request.

  1. Purpose
  2. Installation
  3. Transaction manager
  4. Flusher
  5. Supported PHP versions
  6. Supported Doctrine versions

Purpose

  • Pure abstraction for transactions.
  • Easily testable.
  • An alternative to doctrine's EntityManagerInterface which is polluted with all sorts of actions on database. (SOLID Interface segregation principle violation)
  • Support Doctrine with active and beta versions test coverage

Installation

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

Transaction Manager

Configuration

Symfony:

Initialization without automatic clearing:

# config/services.yaml:

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

Initialization with automatic clearing:

# config/services.yaml:

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

Example


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

Automatic clearing

Transaction manager can be initialized with automatic clearing. (See Configuration)

After any transaction commit or roll back all entity objects will be cleared/detatched from Doctrine cache automatically.

Mock implementation for testing

You can mock transaction manager in your tests with MockedTransactionManager class.


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

Logic Termination

There are cases when transaction is terminated not due to a system fault but according to a predefined logic. You can throw LogicTerminationInterface or LogicTerminationException for such cases.

For example, in doctrine any exception that is thrown from callback and is not instance of LogicTerminationInterface closes database connection, so connection becomes not writable.

Flusher

Doctrine stores persisted entities in memory until they are flushed in real DB. In order to control flushing (don't do it for every object) you can use FlusherInterface.

Configuration

Symfony:

Initialization with both lazy and clearing features via factory class:

# config/services.yaml:

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

Initialization with both lazy feature only via factory class:

# config/services.yaml:

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

Manual initialization via services decorators:

# 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.

Lazy Flusher

LazyFlusherDecoraror can flush only once for all code enclosed in a closure function.


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() won't be called with every save() call, but only once at the end.

You can configure $maxBufferSize in lazy flusher constructor argument in order to flush after the number callbacks completed.

Clearing flusher

Clearing is used for memory usage optimization. Once data gets flushed it is recommended to clear (detach) the entity objects from doctrine cache.

DoctrineClearingFlusherDecorator runs \Doctrine\ORM\EntityManager::clear() after each flush invoked by flusher interface.

If you use it combined with LazyFlusherDecoraror, clearing is made only after actual flash is called, e.g. after reaching max buffer limit.

Void flusher

For test purposes (InMemory repositories), when you don't need to flush you can use VoidOrmSessionFlusher implementation.

Supported PHP versions

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

Supported Doctrine versions

  • 2.20.3+
  • 3.3.3+

friends-of-ddd/transaction-manager-doctrine 适用场景与选型建议

friends-of-ddd/transaction-manager-doctrine 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 4 次下载、GitHub Stars 达 1, 最近一次更新时间为 2024 年 02 月 21 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 friends-of-ddd/transaction-manager-doctrine 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 friends-of-ddd/transaction-manager-doctrine 我们能提供哪些服务?
定制开发 / 二次开发

基于 friends-of-ddd/transaction-manager-doctrine 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 4
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 2
  • 点击次数: 12
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2024-02-21