Separating business logic from PHP Doctrine 2
Asked Answered
R

2

7

I use symfony 2.3 and php doctrine 2.

The program has the following models:

  • entity Order - a typical customer order
  • entity BadOrderEntry(fields: id, order - unidirectional one-to-one relationship with Order, createdAt)
  • factory BadOrderEntryFactory for creation entity BadOrderEntry
  • repository BadOrderEntryRepository for search methods of entity BadOrderEntry
  • manager BadOrderEntryManager for save/edit/delete methods of entity BadOrderEntry
  • AND MAIN CLASS BadOrderList - list of bad orders, code of this class:

    private $factory;
    private $repository;
    private $manager;
    
    public function __construct(
        BadOrderEntryFactory $f,
        BadOrderEntryRepository $r,
        BadOrderEntryManager $m
    ) {
        $this->factory = $f;
        $this->repository = $r;
        $this->manager = $m;
    }
    
    public function has(Order $order)
    {
        return $this->repository->existsByOrder($order);
    }
    
    public function add(Order $order)
    {
        if (! $this->has($order)) {
            $entry = $this->factory->create($order);
            $this->manager->save($entry);
        }
    }
    
    public function remove(Order $order)
    {
        $entry = $this->repository->findOneByOrder($order);
        if ($entry !== null) {
            $this->manager->delete($entry);
        }
    }
    

I really like the design of this class. I thought a lot about it. Everything is wonderful. BUT! There is one problem: operations in methods add and remove must be performed in the transactions.

Transaction code in PHP Docrine 2 looks like this:

<?php
$em->getConnection()->beginTransaction();
try {
    //... do some work
    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->getConnection()->rollback();
    throw $e;
}

But how can I call this code inside BadOrderList?

I spent a lot of time and removed depending on the database(and correspondingly PHP Doctrine 2), and again to create it? Is now dependence is hidden in classes BadOrderEntryRepository and BadOrderEntryManager.

How to hide the dependence on the transaction mechanism in class BadOrderList?

Righthand answered 4/1, 2015 at 14:25 Comment(6)
Add transaction management to your Manager::add and deleteI also suggest you to rethink your design. It's not really nice. Make your model persistent independent.Annikaanniken
@Annikaanniken How can I add transaction management to Manager::add(or delete)? What design problems? Manager is simply an additional layer of abstraction over doctrine object manager. It's not bad and it's not good. But gives more control.Righthand
You can do it in the same way as you mentioned in your example. doctrine-orm.readthedocs.org/en/latest/reference/… . As for design problems - why do you think your list is THE MAIN object. Main for what part of you architecture? Have you thought about method and class names? Can you test whole your model without the doctrine?Annikaanniken
1) Do you offer an injection of connection(doctrine.dbal.default_connection)? This action creates a dependency on the class \Doctrine\DBAL\Connection. My question is, how to avoid it. 2) THE MAIN only in the context of this issue of course. I created a topic on only one issue, and showed only the code related to it.Righthand
1) You can make ORMManager, that extends your Manager, you can use some abstract UnitOfWork/Persister/Storage/etc or its interface with final ORM class. Anyways you need an adapter to ORM somewhere outside the model. Your question was How to hide the dependence on the transaction mechanism in class BadOrderList? 2) why? :)Annikaanniken
I do not want to expand the capabilities of ORM. I want to hide the ORM. Indeed, needed here some adapter hiding the transaction mechanism of PHP Doctrine. Now I think I need a class, for example, BusinessTransastion implements methods beginTransaction, commit, rollback. The most interesting method BusinessTransastion::beginTransastion, it must guess what the EntityManager use: without arguments - use default, or if call BusinessTransastion::beginTransastion(get_class($order)) detect automatically most suitable. What do you think about this?Righthand
A
4

After our discussion I have an answer to your question. The question is really not "How to hide the dependence on the transaction mechanism in class BadOrderList?", but How to decouple a model from a persistence layer? (Doctrine2 in that particular case).

I tried illustrate my suggestion with some code

class BadOrderEntry
// Bad - is too bad word to describe an order here. Why is it bad? Is it Declined? Cancelled?
{
   private $order;
   // some code
}
class BadOrderEntryFactory 
{ 
   // If there is not to much processing to build BadOrderEntry better use factory method like BadOrderEntry::fromOrder($order); 
}
class BadOrderEntryRepository 
{ 
   // here is some read model 
}
class BadOrderEntryManager  
// ITS a part of our model and shouldn't be coupled to ORM
{
  public function save(BadEntry $be) 
  {
    // some model events, model actions
    $this->doSave($be); // here we should hide our storage manipulation
  }

  protected function doSave($be) // it can be abstract, but may contain some basic storage actions  
  { 
  }

  // similar code for delete/remove and other model code
}
class ORMBadOrderEntryManager extends BadOrderEntryManager 
// IT'S NOT the part of your model. There is no business logic. There is only persistent logic and transaction manipulation
{ 
  protected $entityManager;

  // some constructor to inject doctrine entitymanager

  protected doSave($be)
  {
    $em = $this->entityManager;
    $em->getConnection()->beginTransaction(); // suspend auto-commit
    try {
      $em->persist($be);
      $em->flush();
      $em->getConnection()->commit();
    } catch (Exception $e) {
      $em->getConnection()->rollback();
      throw $e;
    }
  }
}
// You can also implement ODMBadOrderEntryManager, MemcacheBadOrderEntryManager etc.

So if we talk about directory structure, all your model can be moved out of bundle and used anywhere. Your Bundle structure will be like:

BadEntryBundle
|
+ Entity
| |
| --- BadOrderEntryEntity.php
|
+ ORM
| |
| --- ORMBadOrderEntryManager.php 

And then you'll just inject ORMBadOrderEntryManager to your BadOrderEntryList

Annikaanniken answered 4/1, 2015 at 17:57 Comment(1)
This is really cool solution! I have seen a similar solution in JMSPaymentCoreBundle. But did not think of it... You opened my eyes! Thank you so much!Righthand
R
1

You can transform your class as a service and call it whatever you want after injecting your service container inside your class. you can find more information here about dependency injection :

$injectedContainerOfService->get("id_of_your_service")
Remonaremonetize answered 4/1, 2015 at 16:9 Comment(4)
Thx. It's very simple and obvious solution. But not practical/testable/maintainable one.Annikaanniken
its practicale and testable but for that you should decouple your controller and transform it as a service if you want to test itRemonaremonetize
where do you see a controller?Annikaanniken
Why its not testable?i want to say if your class such a controller or else is not testable perhaps its a design issueRemonaremonetize

© 2022 - 2024 — McMap. All rights reserved.