Friday, April 1, 2016

How to handle different reference directions in database and ZF2 application?

Leave a Comment

Zend\Form\Fieldsets and Zend\Form\Collectionss can be nested and provide a very comfortable way to map complex object structures to them, in order to get a comlete object (ready to be saved) from the form input more or less automatically. The Form Collections tutorial provides a very good example.

The case I'm currently having is a bit more complex, since it contains a reference inversion. That means:

I have two entities -- MyA and MyB and while in the database the relationship between them is implemented as FOREIGN KEY from myb.mya_id to mya.id, the application is using an inverted referencing:

MyA has MyB 

Or with some code:

namespace My\DataObject;  class MyA {     /**      * @var integer      */     private $id;     /*      * @var text      */     private $foo;     /**      * @var MyB      */     private $myB; }  namespace My\DataObject;  class MyB {     /**      * @var integer      */     private $id;     /*      * @var text      */     private $bar;     /*     Actually it's even bidirectional, but it's not crucial for this issue.     For this problem it's not important,     wheter the class MyB has a property of type MyA.     We get the issue already,     when we define a property of type MyB in the class MyA.     Since the direction of the reference MyA.myB->MyB differes     from the direction of the reference my_b.my_a.id->my_a.id.     */      /**      * @var MyA      */     // private $myA; } 

My Mapper objects get DataObjects passed as argument: MyAMapper#save(MyA $object) and MyBMapper#save(MyB $object).

namespace My\Mapper; use ... class MyAMapper {     ...     public fuction save(MyA $object)     {         // save the plain MyA propertis a new entry in the my_a table         ...         $myMapperB->save($myA->getMyB());     } }  namespace My\Mapper; use ... class MyBMapper {     ...     public fuction save(MyB $object)     {         // save the plain MyB propertis a new entry in the my_b table         ...     } } 

That means, the MyAMapper#save(...) has evrything needed to save the MyA object to the my_a table. But in the MyBMapper the data for my_b.my_a_id will be missing.

And I also cannot create a fieldset MyAFieldset with a nested fieldset MyBFieldset and then nest the fieldset MyBFieldset into MyAFieldset in order to fill MyA#MyB#MyA (in order to pass the data for my_b.my_a_id to MyBMapper#save(...)):

class MyAFieldset {     $this->add([         'name' => 'my_b',         'type' => 'My\Form\Fieldset\MyBFieldset',         'options' => []     ]); }  class MyBFieldset {     $this->add([         'name' => 'my_a',         'type' => 'My\Form\Fieldset\MyAFieldset',         'options' => []     ]); } 

This would cause a recursive dependency and cannot work.

How to handle a case, when the reference direction on the application level differs from it's direction in the database? How to create though a fieldsets structure, that provides a complete ("ready to be saved") object?


Workaround 1

When the form is processed, a further MyA object can be created and added to the MyB object got from the form:

class MyConrtoller {     ...     public function myAction() {         $this->myForm->bind($this->myA);         $request = $this->getRequest();         $this->myForm->setData($request->getPost());         // here the hack #start#         $this->myB->setMyA($this->myA);         // here the hack #stop#         $this->myAService->saveMyA($this->myA);     } } 

Well, maybe not in the controller, the mapper might be a better place for that:

class MyAMapper {     ...     public function save(MyA $myA)     {         $data = [];         $data['foo'] = [$myA->getFoo()];         // common saving stuff #start#         $action = new Insert('my_a');         $action->values($data);         $sql = new Sql($this->dbAdapter);         $statement = $sql->prepareStatementForSqlObject($action);         $result = $statement->execute();         $newId = $result->getGeneratedValue()         // common saving stuff #stop#         ...         // hack #start#         if(! $myA->getB->getA()) {             $myA->getB->setA(new MyA());             $myA->getB->getA()->setId($newId);         }         // hack #stop#         // and only after all that we can save the MyB         $myB = $this->myBMapper->save($myB);         $myA->setMyB($myB);         ...     } } 

But anyway it's just a hack.

Workaround 2

The MyB class gets a property $myAId. But it's also not a clean way.

Workaround 3

The MyBFieldset gets a MyAFieldsetFake as sub-fieldset. This fieldset class is then just a "shallow" copy of the MyAFieldset, that contains only the ID for the MyA data object:

class MyAFieldset {     ...     public function init()     {         $this->add([             'type' => 'text',             'name' => 'id',             'options' => [...],         ]);         $this->add([             'type' => 'text',             'name' => 'foo',             'options' => [...],         ]);     } } class MyAFieldset {     ...     public function init()     {         $this->add([             'type' => 'text',             'name' => 'id',             'options' => [...],         ]);         $this->add([             'type' => 'text',             'name' => 'bar',             'options' => [...],         ]);         $this->add([             'type' => 'text',             'name' => 'foo',             'type' => 'My\Form\Fieldset\MyAFakeFieldset',             'options' => [...],         ]);     } } class MyAFieldset {     ...     public function init()     {         $this->add([             'type' => 'text',             'name' => 'id',             'options' => [...],         ]);     } } 

But fake objects are a bit dirty as well.

1 Answers

Answers 1

How about creating a new table to handle the mappings on their own. Then you can isolate that complexity away from the objects that take advantage of them.

So, you could have a new object AtoBMappings

namespace My\DataObject;  class MyA {     /**      * @var integer      */     private $id;     /*      * @var text      */     private $foo;     /**      * @var MyAtoB      */     private $myAtoB; }  namespace My\DataObject;  class MyB {     /**      * @var integer      */     private $id;      /**      * @var AtoBMapperID      */     private $myAtoB; }  class MyAtoBMapper {    /**     * @var myB     */    private $myB    /**     * @var myA    **    private $myA } 

Then, instead of hacking your Mapper method, you can simply make an assignment in MyA to MyB creation.

class MyAMapper {     ...     public function save(MyA $myA)     {          $myAtoB = new MyAtoBMapper();         //.... instert new myAtoB into DB           $data = [];         $data['foo'] = [$myA->getFoo()];         $data['myAtoB'] = $myAtoB->getId();         // common saving stuff #start#         $action = new Insert('my_a');         $action->values($data);         $sql = new Sql($this->dbAdapter);         $statement = $sql->prepareStatementForSqlObject($action);         $result = $statement->execute();         $newId = $result->getGeneratedValue();         $myA->setMyAtoB($newAtoB);         $myAtoBMapper->myA = $newId;         // common saving stuff #stop#         // and only after all that we can save the MyB         $myB = $this->myBMapper->save($myB);         $myB->setMyAtoB($newAtoB);         $myAtoBMapper->myB = $myB;         ...     } } 

Do you think this would work, or do you think this is too much of a hack?

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment