Showing posts with label zend-framework2. Show all posts
Showing posts with label zend-framework2. Show all posts

Friday, February 16, 2018

How to Unit Test the FilePostRedirectGet plugin?

Leave a Comment

TLDR:

How to write an integration test for a FilePostRedirectGet form submission request?


In my application I have a multi-page form with a file element in it. Originally the controller code was (psuedo code - all multi-page stuff removed):

$form = new MyForm(); if($this->getRequest()->isPost()) {   if($form->isValid($this->getRequest()->getPost()) {     //move file to new home and save post to database.     return $this->redirect()->toRoute('admin/cms/edit');   } } return new ViewModel(['form' => $form]); 

Very basically, if the form was valid it would save the form details and perform a redirect (typically the edit version of the page). If the form was not valid, it would just show the form again with the appropriate error messages.

The problem with this approach, is if one of the form elements fails validation, the file input will lose its value and the user will need to re-upload their file. To get around this, I changed to the FPRG approach:

$form = new MyForm(); /* @var $prg FilePostRedirectGet */ $prg = $this->filePostRedirectGet($form, $this->url()->fromRoute($this->routeMatch, ['action' => $this->action, 'id' => $id], ['query' => ['page' => $page]]), true); if ($prg instanceof Response) {   return $prg; // Return PRG redirect response } elseif ($prg !== false) {     //move file to new home and save post to database.     return $this->redirect()->toRoute('admin/cms/edit'); } return new ViewModel(['form' => $form]); 

The FilePostRedirectGet plugin saves the post data in a session, redirects to the same page, validates the form and, if valid, redirects to the success/edit page - otherwise just show the form with errors. The benefits of this approach is that the file input retains its value regardless of any other failed elements.

The problem is, how to write an integration test for this request?

Originally, there was only one redirect (on success) so I could test for that (model mocking stuff removed for brevity):

/**  * Redirect to next page after form submission.  */ public function testAddActionRedirectsAfterValidPost() {      $postData = $this->_postData();      $this->dispatch('http://localhost/admin/cms/add', 'POST', $postData);     $this->assertResponseStatusCode(302);     $this->assertRedirectTo('http://localhost/admin/cms/edit/13');      return $postData; } 

However, due to the multiple redirects, I cannot use this method of testing if the request was successful or not.

How to write an integration test for this request?

0 Answers

Read More

Saturday, July 2, 2016

How to read Zend2 session in Symfony3 application

Leave a Comment

I have two applications. One the legacy one is written in Zend Framework and a new one in Symfony 3.1 should share session with an old one.

In old application native file storage is used so when I go to app_dev.php and I write session_start(); var_dump($_SESSION); I see '__ZF' key in the session and I need to get access to it in symfony3 application.

Obviously code above was only to check if session is shared within domain.

In symfony3 app I've tried to subcribe the event KernelEvents::REQUEST and there I wanted get raw session from the request and create a bag with parameters that come from Zend2.

class SessionSubscriber implements EventSubscriberInterface {     public function onKernelRequest(GetResponseEvent $event)     {          if ($event->isMasterRequest() && true == $event->getRequest()->hasSession()) {             var_dump($event->getRequest());             $event->getRequest()->getSession()->registerBag(new ZendSessionBag());             var_dump($event->getRequest()->getSession()->getBag('zf'));             exit;             return;         }     }     public static function getSubscribedEvents()     {         return array(             KernelEvents::REQUEST => (array('onKernelRequest', 127))         );     } } 

But I don't have access to raw session data from $event->getRequest();

This is how my bag looks so far. From what I understand I should have access to raw session data in initialize() method

class ZendSessionBag implements SessionBagInterface {     private $storageKey;     private $sessionData;       public function __construct($storageKey = '__ZF')     {         $this->storageKey = $storageKey;     }      public function getName()     {         return 'zf';     }      public function initialize(array &$array)     {         var_dump($array); // here should be raw $_SESSION data         $this->sessionData = &$array;     }      public function getStorageKey()     {         return $this->storageKey;     }      public function clear()     {         $this->sessionData = [];     } } 

So when I get the session and then get bag named "zf" I will have access to the data.

This is also my config regarding sessions:

session:         storage_id: session.storage.php_bridge         handler_id:  session.handler.native_file         save_path:   "/var/lib/php5/sessions" 

Any help would be appreciated.

1 Answers

Answers 1

I've managed to make it work.

Firstly I've changed configuration:

I've changed save path and remove native handler:

session:         save_path:   "/var/lib/php5/sessions" 

Then I've changed EventSubscriber:

class SessionSubscriber implements EventSubscriberInterface {     /**      * @param GetResponseEvent $event      */     public function onKernelRequest(GetResponseEvent $event)     {         $bag = null;         $session = $event->getRequest()->getSession();          try         {             $bag = $session->getBag('zf');         }         catch (\InvalidArgumentException $e)         {             $bag = new NamespacedAttributeBag("__ZF");             $bag->setName("zf");             $session->registerBag($bag);              $session->start();         }         $bag->set('userId', isset($_SESSION['Zend_Auth']->storage) ? $_SESSION['Zend_Auth']->storage : null);     }      /**      * @return array      */     public static function getSubscribedEvents()     {         return array(             KernelEvents::REQUEST => (array('onKernelRequest', 127))         );     } } 

I've imported class from Zend Zend\Stdlib\ArrayObject after that I have access to variable I want in bag zf.

Read More

Monday, May 2, 2016

ZF2 project stops working when is cloned to local server

Leave a Comment

I would like to know why when I clone my ZF2 project to a local machine to do some testing it completly stops working.

In my local machine I have two subfolders, one with a cakePHP project and the other with the ZF2 I've cloned.

The cakePHP project is working fine since it was there first, but the ZF2, when I try to access to the public folder it prints me:

{"error":"Something went wrong"}   

A really generic error... I have no clue about what is going on.

I've tried some general debug attemps like

ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); 

with no success at all, I've also checked the .htaccess RewriteBase directive to match my subfolder and the DB configuration is done too.

I have researched a bit in the project and the file which displays the error is module/RestfulV2_2/Module.php (Reading the README.md I've discovered is part of ZF2 Restful Module Skeleton):

/** * @param MvcEvent $e * @return null|\Zend\Http\PhpEnvironment\Response */  public function errorProcess(MvcEvent $e)     {         /** @var \Zend\Di\Di $di */         $di = $e->getApplication()->getServiceManager()->get('di');          $eventParams = $e->getParams();          /** @var array $configuration */         $configuration = $e->getApplication()->getConfig();          $vars = array();         if (isset($eventParams['exception'])) {             /** @var \Exception $exception */             $exception = $eventParams['exception'];              if ($configuration['errors']['show_exceptions']['message']) {                 $vars['error-message'] = $exception->getMessage();             }             if ($configuration['errors']['show_exceptions']['trace']) {                 $vars['error-trace'] = $exception->getTrace();             }         }          if (empty($vars)) {             $vars['error'] = 'Something went wrong';         }          /** @var PostProcessor\AbstractPostProcessor $postProcessor */         $postProcessor = $di->get(             $configuration['errors']['post_processor'],             array('vars' => $vars, 'response' => $e->getResponse())         );          $postProcessor->process();          if (             $eventParams['error'] === \Zend\Mvc\Application::ERROR_CONTROLLER_NOT_FOUND ||             $eventParams['error'] === \Zend\Mvc\Application::ERROR_ROUTER_NO_MATCH         ) {             $e->getResponse()->setStatusCode(\Zend\Http\PhpEnvironment\Response::STATUS_CODE_501);         } else {             $e->getResponse()->setStatusCode(\Zend\Http\PhpEnvironment\Response::STATUS_CODE_500);         }          $e->stopPropagation();          return $postProcessor->getResponse();     }  

The line which is calling the error in my index.php is:

Zend\Mvc\Application::init(require 'config/application.config.php')-  run();   

And the only line I found where the error function is called some way is this one in my modele.php :

 $sharedEvents->attach('Zend\Mvc\Application', MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'errorProcess'), 999); 

Can you help me to solve this? I'm inexperienced with ZF2 but I know that with cakePHP to make it work you need to clear the cache folder. Is there any similar process in ZF2? Should I virtualize two servers to avoid conflics?

Thank you in advance.

EDIT : I've already made virtual hosts to avoid any possible conflict between my two frameworks but the error output is still the same.

EDIT2 : Here is my application.config.php file:

return array(     // This should be an array of module namespaces used in the application.     'modules' => array(         'Restful',         'MvlabsSnappy',         'Qrcode',         'Application',         'RestfulV2',         'RestfulV2_2'     ),      // These are various options for the listeners attached to the ModuleManager     'module_listener_options' => array(         // This should be an array of paths in which modules reside.         // If a string key is provided, the listener will consider that a module         // namespace, the value of that key the specific path to that module's         // Module class.         'module_paths' => array(             './module',             './vendor',         ),          // An array of paths from which to glob configuration files after         // modules are loaded. These effectively override configuration         // provided by modules themselves. Paths may use GLOB_BRACE notation.         'config_glob_paths' => array(             'config/autoload/{,*.}{global,local}.php',         ),          // Whether or not to enable a configuration cache.         // If enabled, the merged configuration will be cached and used in         // subsequent requests.         //'config_cache_enabled' => $booleanValue,          // The key used to create the configuration cache file name.         //'config_cache_key' => $stringKey,          // Whether or not to enable a module class map cache.         // If enabled, creates a module class map cache which will be used         // by in future requests, to reduce the autoloading process.         //'module_map_cache_enabled' => $booleanValue,          // The key used to create the class map cache file name.         //'module_map_cache_key' => $stringKey,          // The path in which to cache merged configuration.         //'cache_dir' => $stringPath,          // Whether or not to enable modules dependency checking.         // Enabled by default, prevents usage of modules that depend on other modules         // that weren't loaded.         // 'check_dependencies' => true,     ),      // Used to create an own service manager. May contain one or more child arrays.     //'service_listener_options' => array(     //     array(     //         'service_manager' => $stringServiceManagerName,     //         'config_key'      => $stringConfigKey,     //         'interface'       => $stringOptionalInterface,     //         'method'          => $stringRequiredMethodName,     //     ),     // )     // Initial configuration with which to seed the ServiceManager.    // Should be compatible with Zend\ServiceManager\Config.    // 'service_manager' => array(), ); 

2 Answers

Answers 1

First I would open index.php or whatever used as initial file (DirectoryIndex) and temporarily completely replace whole its content with something very base and simple, for example just these two lines:

<?php   phpinfo(); 

And then make sure that it started to work after that - with that simple code which just displays your php configuration. So we'll find out that there is no error in server configurations, permissions and etc. and nothing prevents your script from run.

Then I would do the same at your old project location just to get phpinfo() from that place too and waste some time trying to compare them. Maybe you missed something important and you'll now see it.

If no - next step I would check your DB connectivity if your project uses any DB... also with some very simple commands like connect, bind, ....

And finally I'd try to restore original project content step by step from its begin, and look at which step it will fail. It doesn't matter that there maybe no any output - you may put echo __LINE__ . ' works!<br/>'; between blocks, so your index.php will look like:

<?php  // original code block 1    echo __LINE__ . ' works!<br/>';  // original code block 2    echo __LINE__ . ' works!<br/>'; 

And you'll see in browser where it fails.

This is a very base description of my debug principals, but hope it will help.

Answers 2

The error could be anything. However, assuming the posted code is executed, it will suppress an error message without the correct configuration.

Try adding the following config to local.config.php.

return [     'errors'=> [         'show_exceptions' => [             'message' => true,             'trace'   => true         ],     ], ]; 

If an exception is being thrown and that listener is catching it, then the $eventParams is something you should debug.

Read More

Wednesday, April 13, 2016

Zend framework 2 / Doctrine 2 / Bulk operations and events triggering

Leave a Comment

For a huge project, with a lot of entities, I wrote a save() common method.

This method is stored in an abstract service and is used in all the project to save entities state.

AbstractService::save() looks like this :

public function save($entity) {     $transactionStarted = $this->beginTransaction();      try     {         $action = $entity->getId() ? self::UPDATE : self::CREATION;          $this->getEventManager()->trigger('save.pre', $entity, ['action' => $action]);          $this->getEntityManager()->persist($entity);         $this->getEntityManager()->flush();          $this->getEventManager()->trigger('save.post', $entity, ['action' => $action]);          if ($transactionStarted)         {             $this->commitTransaction();         }     } catch (\Exception $e)     {         if ($transactionStarted)         {             $this->rollbackTransaction();         }          throw new Exception('Unable to save entity', $e);     }      return true; }  public function beginTransaction() {     if (!$this->getEntityManager()->getConnection()->isTransactionActive())     {         $this->getEntityManager()->getConnection()->beginTransaction();          return true;     }      return false; }  public function commitTransaction() {     $this->getEntityManager()->getConnection()->commit();      return $this; }  public function rollbackTransaction() {     $this->getEntityManager()->getConnection()->rollBack();      return $this; } 

In my case, when a member is inserted (new Member entity) when calling the Member service (extended AbstractService), an email is sent (e.g) through the save.post event. Or another action related to another service calling save method too can be proceed.

Example of the "child" MemberService::save() method

MemberService  public function save(Member $member) {     // some stuff, e.g set a property     $member->setFirstName('John');      return parent::save($member); } 

Example of triggered event

$sharedEventManager->attach(MemberService::class, 'save.post', [$this, 'onMembersCreation']);  public function onMembersCreation(EventInterface $event) {     // send an email      // anything else ... update another entity ... (call AnotherService::save() too)  } 

That's great for a simple saving process.

But now, I want to massively import a lot of members, with creations, updates, ... And to achieve that, I read the Doctrine doc related to bulk imports. Doc here

But how to update my code properly to handle "bulk saving" and "single saving" ? And keep transactions security and events ?

1 Answers

Answers 1

Basically I suggest you implement the Doctrine\Common\Collections\Collection interface, maybe extending ArrayCollection, and create a method save that will do what the doc told you to.

<?php  class MyDirtyCollection extends \Doctrine\Common\Collections\ArrayCollection {      public function __construct(AbstractService $abstractService)     {         $this->service = $abstractService;     }      public function save()     {         foreach ($this as $entity) {             $this->service->save($entity);         }     } }  class MyCollection extends \Doctrine\Common\Collections\ArrayCollection {      public $bulkSize = 500;      protected $eventManager;     protected $entityManager;      public function __construct(EntityManager $entityManager, EventManager $eventManager)     {         $this->entityManager = $entityManager;         $this->eventManager = $eventManager;     }      public function getEventManager()     {         return $this->eventManager;     }      public function getEntityManager()     {         return $this->entityManager;     }      public function setBulkSize(int $bulkSize)     {         $this->bulkSize = $bulkSize;     }      public function save()     {         $transactionStarted = $this->getEntityManager()->getConnection()->beginTransaction();          try {             foreach ($this as $entity) {                 $action = $entity->getId() ? self::UPDATE : self::CREATION;                 $this->getEventManager()->trigger('save.pre', $entity, ['action' => $action]);             }              $i = 0;             foreach ($this as $entity) {                 $i++;                  $this->getEntityManager()->persist($entity);                  if (($i % $this->bulkSize) === 0) {                     $this->getEntityManager()->flush();                     $this->getEntityManager()->clear();                 }             }              $this->getEntityManager()->flush();             $this->getEntityManager()->clear();              foreach ($this as $entity) {                 $action = $entity->getId() ? self::UPDATE : self::CREATION;                 $this->getEventManager()->trigger('save.post', $entity, ['action' => $action]);             }              if ($transactionStarted) {                 $this->getEntityManager()->getConnection()->commitTransaction();             }          } catch (Exception $e) {             $this->getEntityManager()->rollbackTransaction();         }     } } 

Something like that ;) When you fetch your data you hydrate your collection, then you deal with your entity and finally call $collection->save();

EDIT : Add insert class and use case below :

The performance here will be low, but still better than commit by commit. Yet you should think about using Doctrine DBAL instead of the ORM if you are looking for hgih performance. Here I share with you my DBAL class for bulk Insert :

<?php  namespace JTH\Doctrine\DBAL;  use Doctrine\DBAL\Query\QueryBuilder; use Exception; use InvalidArgumentException; use Traversable; use UnderflowException;  class Insert extends QueryBuilder {     const CALLBACK_FAILURE_SKIP = 0;     const CALLBACK_FAILURE_BREAK = 1;      protected $callbackFailureStrategy = self::CALLBACK_FAILURE_BREAK;      public static $defaultBulkSize = 500;      public $ignore = false;     public $onDuplicate = null;      public function values(array $values)     {         $this->resetQueryPart('values');         $this->addValues($values);     }      public function addValues(array $values)     {         $this->add('values', $values, true);     }      public function setCallbackFailureStrategy($strategy)     {         if ($strategy == static::CALLBACK_FAILURE_BREAK) {             $this->callbackFailureStrategy = static::CALLBACK_FAILURE_BREAK;         } elseif ($strategy == static::CALLBACK_FAILURE_SKIP) {             $this->callbackFailureStrategy = static::CALLBACK_FAILURE_SKIP;         } else {             $class = self::class;             throw new InvalidArgumentException(                 "Invalid failure behaviour. See $class::CALLBACK_FAILURE_SKIP and $class::CALLBACK_FAILURE_BREAK"             );         }     }      public function getCallbackFailureStrategy()     {         return $this->callbackFailureStrategy;     }      public function execute()     {         return $this->getConnection()->executeUpdate(             $this->getSQLForInsert(),             $this->getParameters(),             $this->getParameterTypes()         );     }      /**      * Converts this instance into an INSERT string in SQL.      * @return string      * @throws \Exception      */     private function getSQLForInsert()     {         $count = sizeof($this->getQueryPart('values'));          if ($count == 0) {             throw new UnderflowException("No values ready for INSERT");         }          $values = current($this->getQueryPart('values'));         $ignore = $this->ignore ? 'IGNORE' : '' ;         $sql = "INSERT $ignore INTO " . $this->getQueryPart('from')['table'] .             ' (' . implode(', ', array_keys($values)) . ')' . ' VALUES ';          foreach ($this->getQueryPart('values') as $values) {             $sql .= '(' ;              foreach ($values as $value) {                 if (is_array($value)) {                     if ($value['raw']) {                         $sql .= $value['value'] . ',';                     } else {                         $sql .= $this->expr()->literal($value['value'], $value['type']) . ',';                     }                 } else {                     $sql .= $this->expr()->literal($value) . ',';                 }             }              $sql = substr($sql, 0, -1);             $sql .= '),';         }          $sql = substr($sql, 0, -1);          if (!is_null($this->onDuplicate)) {             $sql .= ' ON DUPLICATE KEY UPDATE ' . $this->onDuplicate . ' ';         }          return $sql;     }      /**      * @param $loopable array | Traversable An array or object to loop over      * @param $callable Callable A callable that will be called before actually insert the row.      * two parameters will be passed :      * - the key of the current row      * - the row values (Array)      * An array of rows to insert must be returned      * @param $bulkSize int How many rows will be inserted at once      * @param bool $transactionnal      * @throws \Doctrine\DBAL\ConnectionException      * @throws \Exception      */     public function bulk($loopable, callable $callable, $bulkSize = null, $transactionnal = true)     {         if (!is_array($loopable) and !($loopable instanceof Traversable)) {             throw new InvalidArgumentException("\$loppable must be either an array or a traversable object");         }          $bulkSize = $bulkSize ?? static::$defaultBulkSize;          $this->getConnection()->getConfiguration()->setSQLLogger(null); // Avoid MonoLog memory overload          if ($transactionnal) {             $this->getConnection()->beginTransaction();         }          $this->resetQueryPart('values');          foreach ($loopable as $key => $values) {             try {                 $callbackedValues = $callable($key, $values);                  if (sizeof($callbackedValues) > 0) {                     foreach ($callbackedValues as $callbackedValuesRow) {                         $this->addValues($callbackedValuesRow);                     }                 }             } catch (Exception $e) {                 /*                  * If a callback exception must break the transaction, then throw the exception to the call stack                  * Else, skip the row insertion                  */                 if ($this->callbackFailureStrategy == static::CALLBACK_FAILURE_BREAK) {                     throw $e;                 } else {                     continue;                 }             }              $count = count($this->getQueryPart('values'));              if ($count >= $bulkSize) {                 $this->execute();                 $this->resetQueryPart('values');             }         }          $count = count($this->getQueryPart('values'));          if ($count > 0) {             $this->execute();         }          $this->resetQueryPart('values');          if ($transactionnal) {             $this->getConnection()->commit();         }     }      /**      * @return boolean      */     public function isIgnore()     {         return $this->ignore;     }      /**      * @param boolean $ignore      */     public function setIgnore(bool $ignore)     {         $this->ignore = $ignore;     }      /**      * @return null|string      */     public function getOnDuplicate() : string     {         return $this->onDuplicate;     }      /**      * @param null $onDuplicate      */     public function setOnDuplicate($onDuplicate)     {         $this->onDuplicate = $onDuplicate;         $this->ignore = false;     }   } 

Use case :

    try {         $i = new Insert($this->getDoctrine()->getConnection('myDB'));         $i->insert('myTable');         $i->setOnDuplicate('col1 = VALUES(col1), updated_last = NOW()');         $i->setCallbackFailureStrategy(Insert::CALLBACK_FAILURE_BREAK);         $i->bulk($myArrayOfRows, function ($key, $row) {              // Some pre-insert processing              $rowset[] = $row;              return $rowset;          }, 500, true);          $this->addFlash('success', 'Yay !');      } catch (DBALException $e) {         $this->addFlash('error', 'Damn, error : ' . $e->getMessage());     } 
Read More

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?

Read More