K
K
Kerim Berdimyradov2015-12-16 00:15:02
symfony
Kerim Berdimyradov, 2015-12-16 00:15:02

What is the best way to build the Transactions module in symfony?

Good day, dear IT wizards!
I am developing a Transactions bundle in symfony. I would like to know your advice about the components into which the module can be divided and the connections between the components.
In my TK, the user's account is divided into several types of accounts (1. FundAccount - an account that the user can replenish and spend money from this account on services within the system, but cannot withdraw, 2. IncomeAccount - an account that the user accumulates / earns for implementation of various projects - can shoot), etc. It is possible to transfer money from the second account to the first one (+ a commission is withdrawn), and from the second to the first, transferring money is prohibited.
More types of accounts may be added. This is done to match the business model - accounts have different commissions, different benefits.
So, as you can see, there are enough different conditions. Moreover, I would like to develop a flexible and expressive structure.
This is how I present the solution, but still I look forward to your criticism and advice!
I want all requests related to money transfers (transactions) to come to one controller. This is what the controller will look like.

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

In turn, TransactionChainService stores the managers of all user accounts. In my case, so far only FundAccountManager and IncomeAccountManager. All managers are required to implement the transfer() method.
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();
         }
    }
}

I wriggle in advance for errors (the code is written in order to convey an idea) and if I described something incomprehensibly. I'm waiting for your criticism, advice and suggestions about the transformation of the structure!

Answer the question

In order to leave comments, you need to log in

1 answer(s)
S
Sergey, 2015-12-16
@berdimuradov

0) No TransactionBundle. You will not be able to reuse this logic, which means there is no point in making a bundle. Read symfony best practice. You should have one AppBundle and that's it, nothing else. You can try to move some parts of the infrastructure that are not tied to the business logic into separate bundles for subsequent reuse, but the business logic of the application will not work.
1) read about event sourcing. This method of storing data is ideal for payment transactions, actually in banks, etc. this approach has been used for decades, and even the same database keeps a log of transactions.
2) remove the flush of their service and take it to the controller. flush commits the transaction to the database, and we need to do this when we have completed work with them and not "somewhere in between".
3) wrapping this goodness into another transaction is stupid, because ... the doctrine will make the transaction anyway. In any case, for good it should be done in the decorator.
4) call_user_func_array in your case is an example of a bad decision.
5) by default, you need to use persist only for those entities that we just created (in our case, a transaction), or those that we explicitly took out of the unit of work (and we do not have a $em->detach call).
6) EntityManager should be used exclusively in the repository and should not go outside. Everything about doctrine should be isolated from your logic. This is the biggest plus of the doctrine (abstraction from the repository) and for some reason few people use this plus, then the doctrine is useless ....
7) service managers suck. Name the services properly.
8) instead of a bunch of services, you can enter different transaction objects. For example FundTransaction, IncomTransaction, etc. In your services, almost all the code is duplicated. And so it would be possible to add up all the logic with these operations directly in essence.
9) NO DIE! even for debugging.

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

Further, it is not logical for me to understand why you have one transaction for two people, for any reason, the sender will have one type of transaction and the receiver will have another. You can remember to whom we passed what and nothing more.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question