Как лучше построить модуль Транзакции в symfony?

Доброго времени суток, уважаемы IT-кудесники!
Я разрабатываю модуль(bundle) Транзакции в symfony. Хотел бы узнать ваши советы по поводу компонентов на которые можно разбить модуль и связи между компонентами.
В моем ТЗ счет(account) юзера разбивается на несколько типов счетов(1. FundAccount - счет который юзер может пополнять и тратить деньги с этого счета на услуги внутри системы, но не может снимать, 2. IncomeAccount - счет который юзер накапливает/зарабатывает за выполнение разных проектов - может снимать) и т.д. Переводить деньги со второго счета на первый можно ( + изымается комиссия), а со второго на первый перевод денег запрещен.
Возможно будет добавлены еще типы счетов. Это делается чтобы соответсвовать бизнес-модели - счета имеют разные комиссии, разные плюшки.
Так вот, как видите, различных условий достаточно. Тем более я хотел бы разработать гибкую и выразительную структуру.
Вот как я представляю решение, но все же жду от вас критики и советов!

Я хочу чтобы все запросы связанные с переводами денег(транзакциями) приходили в один контроллер. Примерно так будет выглядеть контроллер.
public function transactionAction(Request $request)
{
    //Получаю необходимые данные
    $sender = $this->get('security.token_storage')->getToken()->getUser();
    $senderAccountType =  $request->request->get('sender_account_type');
    $recipient = $request->request->get('recipient');
    $recipientAccountType =  $request->request->get('recipient_account_type');
    $amount =  $request->request->get('amount');

    //Передаю данные сервису TransactionChainService
    try{
        $this->get('acme.transaction_bundle.transaction_chain')->transfer(User $sender, $senderAccountType,   User $recipient, $recipientAccountType, $amount);
        return new JsonRespone(array('success' => true));
    }catch(TransactionFailedException $e){
        var_dump($e->getMessage()); die();
    }catch(Exception $e){
        var_dump($e->getMessage()); die();
    }
}


В свою очередь TransactionChainService хранит менеджеров всех счетов юзера. В моем случае - пока только FundAccountManager и IncomeAccountManager. Все менеджеры обязаны реализовать метод transfer().

TransactionChainService
class TransactionChainServie
{
      private $accountManagers = array();      

      public function transfer(User $sender, $senderAccountType,   User $recipient, $recipientAccountType, $amount)
      {
            foreach($accountManagers as $manager){
                   $manager->transfer(User $sender, $senderAccountType,   User $recipient, $recipientAccountType, $amount);
            }
      }

      public function addManager($manager){
            $this->accountManagers[] = $manager;
      }
}


FundAccountManager
class FundTransactionManager
{
    protected $em;
 
   public function __construct( EntityManager $em)
    {
        $this->em = $em;
    }
   
    public function transfer(User $sender, $senderAccountType,   User $recipient, $recipientAccountType, $amount)
    {
         if($senderAccountType == "Fund")
         {
              //Перевод денег учитываю %комиссии для данного типа
              $em = $this->em;
              $em->getConnection()->beginTransaction();
              $transaction = new Transaction($sender, $recipient, $amount);
              $sender->setFond($sender->getFund() - $amount);
              call_user_func_array($recipient['set'.$recipientAccountType], $recipient['get'.$recipientAccountType] + $amount * 0.1);  //умножаем на комиссию
              $em->persist($sender);
              $em->persist($recipient);
              $em->persist($transaction);
              $em->flush();
              $em->getConnection()->commit();
         }
    }
}


IncomeAccountManager
class IncomeTransactionManager
{
    protected $em;
 
   public function __construct( EntityManager $em)
    {
        $this->em = $em;
    }
   
    public function transfer(User $sender, $senderAccountType,   User $recipient, $recipientAccountType, $amount)
    {
         if($senderAccountType == "Income")
         {
              //Перевод денег учитываю %комиссии для данного типа
              $em = $this->em;
              $em->getConnection()->beginTransaction();
              $transaction = new Transaction($sender, $recipient, $amount);
              $sender->setIncome($sender->getIncome() - $amount);
              call_user_func_array($recipient['set'.$recipientAccountType], $recipient['get'.$recipientAccountType] + $amount * 0.05);  //умножаем на комиссию
              $em->persist($sender);
              $em->persist($recipient);
              $em->persist($transaction);
              $em->flush();
              $em->getConnection()->commit();
         }
    }
}


Заранее извиваюсь за ошибки(код написан с целью передать мысль) и если что то непонятно описал. Жду вашей критики, советов и предложений про преобразованию структуры!
  • Вопрос задан
  • 1496 просмотров
Решения вопроса 1
Fesor
@Fesor
Full-stack developer (Symfony, Angular)
0) Никаких TransactionBundle. Вы эту логику не сможете реюзать, а значит нет смысла делать бандл. Почитайте symfony best practice. У вас должен быть один AppBundle и все, больше ничего. Вы можете пытаться выносить какие-то части инфраструктуры, которая не привязана к бизнес логике в отдельные бандлы для последующего реюза, но бизнес логику приложения реюзать не выйдет.

1) почитайте про event sourcing. Этот способ хранения данных идеален для платежных транзакций, собственно в банках и т.д. этот подход и используют десятилетиями, да даже та же база данных хранит лог транзакций.

2) уберите flush их сервиса и вынесите его в контроллер. flush коммитит транзакцию в базу, и нам надо это делать когда мы завершили работу с оными а не "где-то посередине".

3) оборачивать это добро в еще одну транзакцию глупо, потому что... доктрина и так сделает транзакцию. В любом случае по хорошему это надо делать в декораторе.

4) call_user_func_array в вашем случае - пример плохого решения.

5) по умолчанию persist использовать нужно только для тех сущностей, которые мы только что создали (в нашем случае - транзакция), либо тех которые мы явно вынули из unit of work (а у нас нет вызова $em->detach).

6) EntityManager должен использоваться исключительно в репозитории и наружу гулять не должен. Все что касается доктрины должно быть изолировано от вашей логики. В этом самый большой плюс доктрины (абстракция от хранилища) и почему-то мало кто этим плюсом пользуется, толку тогда от доктрины....

7) сервисы менеджеры - отстой. Называйте сервисы нормально.

8) вместо кучи сервисов можно ввести разные объекты транзакций. Например FundTransaction, IncomTransaction и т.д. У вас же в сервисах почти весь код дублируется. А так можно было бы всю логику с этими операциями сложить прямо в сущности.

9) НИКАКИХ DIE! даже для дебага.

public function transactionAction(Request $request)
{
    $data = $request->request;
    $transactionDTO = new TransactionDTO(
         // вообще я бы тут просто ID пользователя возвращал... но я упорот по изоляции приложения от UI
         $this->get('security.token_storage')->getToken()->getUser(), 
         $data->get('sender_account_type'),
         $data->get('recipient_account_type'),
         $data->get('amount')
    );
    // с исключениями разберется фронт контроллер
    $this->get('app.transaction_processor')->process($transactionDTO);
    // вот теперь сохраняем изменения
    $this->get('doctrine.orm.entity_manager')->flush();

    return new Response(null, 201); // создали новую запись в журнале транзакций
}


class TransactionProcessor
{
      private $transactionsRepository;      

      public function __construct(TransactionRepository $repository)
      {
           $this->transactionsRepository = $repository;
      }

      public function process(TransactionDTO $dto)
      {
            // create это статический метод фабрика у абстрактного класса Transaction
            // читать шаблон проектирования "абстрактная фабрика".
            $transaction = Transaction::create($dto->getSender(), $dto->getRecipient(), $dto->getAmount());
            
            $this->transactionsRepository->add($transaction);
      }
}


дальше мне по логике не понятно, почему у вас одна транзакция на двух человек, полюбому у sender-а будет один тип транзакции а у ресивера другой. Можно запомнить кому мы чего передавали и только.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы