Hi all, I'm currently trying to implement a relatively simple form with a collection. The model is a Diverts object which may have 0 or more Divert objects associated with it. I'm creating a form to allow me to edit a Diverts object. The model data comes from and is updated via a remote SOAP Web Service, though the details of how this happens are largely irrelevant for the purposes of this question.
Here is my form object, its pretty straight forward and is instantiated via the FormElementManager: ---------------------- <?php namespace Application\Form\Diverts; use Zend\Form\Form; use Zend\Stdlib\Hydrator\ClassMethods; class EditSubscriberDiverts extends Form { public function __construct() { parent::__construct('edit-subscriber-diverts'); $this->setAttribute('method','post') ->setAttribute('id','edit-subscriber-diverts') ->setHydrator(new ClassMethods(false)); $this->add([ 'type' => 'Application\Form\Diverts\DivertsFieldset', 'name' => 'divertsFieldset', 'options' => [ 'use_as_base_fieldset' => true, ], ]); $this->add([ 'name' => 'submit', 'attributes' => [ 'type' => 'submit', 'value' => 'Send', ], ]); } } -------------------------- Next we have the DivertsFieldset, this is created via the FormElementManager: Again, this is pretty simple, it just has a collection of Divert objects. -------------------------- <?php namespace Application\Form\Diverts; use ApplicationBase\Entity\SubscriberDiverts; use Zend\Form\Fieldset; use Zend\Stdlib\Hydrator\ClassMethods; class DivertsFieldset extends Fieldset { public function __construct() { parent::__construct('subscriber-diverts'); $this->setHydrator(new ClassMethods(false)) ->setObject(new \ApplicationBase\Entity\Diverts()) ->setLabel('Subscriber Diverts'); } public function init() { $this->add(array( 'type' => 'Zend\Form\Element\Collection', 'name' => 'diverts', 'options' => array( 'label' => 'Edit destinations', 'count' => 0, 'allow_add' => true, 'allow_remove' => true, 'target_element' => array( 'type' => 'Application\Form\Diverts\DivertFieldset', ), ), )); } } -------------------------- The DivertFieldset which is referenced in the above DivertsFieldset collection is: -------------------------- <?php namespace Application\Form\Diverts; use ApplicationBase\Entity\SubscriberDiverts; use Zend\Form\Fieldset; use Zend\Stdlib\Hydrator\ClassMethods; class DivertFieldset extends Fieldset { public function __construct() { parent::__construct('divert'); $this->setHydrator(new ClassMethods(false)) ->setObject(new \ApplicationBase\Entity\Divert()) ->setLabel('Divert'); $this->add([ 'name' => 'id', 'type' => 'hidden', ]); $this->add([ 'name' => 'destinationSetId', 'type' => 'select', 'options' => [ 'requried' => false, 'disable_inarray_validator' => true, ], ]); } } -------------------------- For reference, here is a snippet from my Module.php file where the above form and fieldsets are created: -------------------------- public function getFormElementConfig() { return [ 'invokables' => [ ....... 'EditSubscriberDiverts' => __NAMESPACE__ . '\Form\Diverts\EditSubscriberDiverts', 'DivertsFieldset' => __NAMESPACE__ . 'Form\Diverts\DivertsFieldset', 'DivertFieldset' => __NAMESPACE__ . 'Form\Diverts\DivertFieldset', ], ........ -------------------------- And now to the controller action which processes this form. There is a few things going on here so I'm going to splice the controller action with comment: --------------------------- public function editDivertAction() { // Check subscriber exists if (!$subscriber = $this->Subscriber()->getSubscriberFromRoute()) { $this->getResponse()->setStatusCode(404); return; } --------------------------- Firstly I'm using controller plugin ($this->Subscriber()) which takes a subscriber ID from the current route and calls a SOAP Web Service to get that subscriber. The Subscriber plugin is actually using a SubscriberService class I've created, which in actual fact has to make several SOAP calls to get all the required information for a subscriber, the details are largely irrelevant, but we should note that this isn't a 'cheap' operation. If the subscriber can't be found, I return a 404. --------------------------- // Get type from route. Route provides constraints, so no further checks required $type = $this->params()->fromRoute('type'); $diverts = $subscriber->getDiverts()->getDiverts($type); --------------------------- Next I get a type parameter from the route and from the subscriber object returned in the above step, I get the Diverts of that type. These are the diverts we want to edit in this action. --------------------------- $form = $form = $this->serviceLocator->get('FormElementManager')->get('EditSubscriberDiverts'); $form->bind($diverts); --------------------------- Then I get the form from the FormElementManager and bind my diverts object. --------------------------- $dsOptions = $subscriber->getDiverts()->getDestinationSetsOptionsList(); $collection = $form->get('divertsFieldset')->get('diverts'); foreach ($collection as $divert) { $divert->get('destinationSetId')->setValueOptions($dsOptions)->setDisableInArrayValidator(true); } --------------------------- Then it gets interesting, and this is where things start to fall apart. In the above DivertFieldset, there is a select input whose options are specific to the particular subscriber for which we are editing diverts. So, how do we set the options in each Divert object of the collection? The above solution is working (I think), but doesn't look right. Essentially, $dsOptions is a list of key values pairs that I pull from my subscriber object, these are the options I need to assign to the select list. Once I've bound the diverts object to the form, I'm pulling back the FormCollection and iterating over each item in the collection, setting the divert options. Now you might think / suggest that the DivertFieldset should be created from a factory which somehow supplies the list, or supplies a service of some kind from which the fieldset can pull back the options as its creates the select object. But the point is that these options are specific to the subscriber we've just pulled back from the Web Services. Even if I were to create the DivertFieldset with some kind of factory that provided a service to get the list of select options for a particular subscriber, how would the fieldset know 'which' subscriber to pull back the options for? The factory creating the fieldset would somehow have to be passed the current subscriber id?? Any thoughts on this particular issue would be much appreciated. --------------------------- if ($this->getRequest()->isPost()) { $this->logger->debug("post data is ".print_r($this->getRequest()->getPost(),1)); $form->setData($this->getRequest()->getPost()); if ($form->isValid()) { $this->logger->debug("Form is valid"); $this->logger->debug("data is ".print_r($diverts,1)); $this->logger->debug("Removed ids are ".print_r($diverts->getRemovedDivertIds(),1)); } else { $this->logger->debug("form is not valid" . print_r($form->getMessages(),1)); } } return new ViewModel([ 'form' => $form, 'destinationSetOptions' => $dsOptions, ]); } --------------------------- The remainder of the controller action is pretty usual stuff, process the request. Note that I'm not actually doing anything with the form submit at the moment, I haven't quite got that far because the form won't always validate. This email is already long enough (I thank you for getting this far), so I'm not going to put the view script in. But the final problem I am having is this, if I remove 1 item from the collection (essentially using query to remove the relevant form elements) the form will submit and validate. If i remove more than one element it does not. Here is some output from my logs which I get when I submit the form after removing more than one item. The debug messages fir show the posted data (from request->getPost()) and then the error messages from $form->getMessages(). Essentially there were 6 items in the collection, and I removed that last 2 (array indexes 4 and 5). The validator is complaining that the values for the select input in item 5 is empty, but that's because it has been removed. I have 'allow_remove' set to true on the collection. It only complains if I remove 2 items (as you see, its not complain that no value is set for array item 4 in the collection). Any thoughts on where I start looking for this issue? I love ZF2, and generally find the code pretty simple to dig into and find issues, but forms and validation are still a bit of a black box to me. I'm never convinced that forms should apply any validation unless specifically requested, via explicit required options or input filters. Is there any way I can pull the bound data back from the form without validating? I know that isn't a solution, but it could be a temporary work around. In actual fact, there isn't much validation that can be done here, the user selects options for which there is always a default value. ------------------------------- 2015-05-27T10:43:00+01:00 DEBUG (7): post data is Zend\Stdlib\Parameters Object ( [storage:ArrayObject:private] => Array ( [divertsFieldset] => Array ( [diverts] => Array ( [0] => Array ( [id] => 40 [destinationSetId] => 6 ) [1] => Array ( [id] => 41 [destinationSetId] => 9 ) [2] => Array ( [id] => 42 [destinationSetId] => 19 ) [3] => Array ( [id] => 43 [destinationSetId] => 11 ) ) ) ) ) 2015-05-27T10:43:00+01:00 DEBUG (7): form is not validArray ( [divertsFieldset] => Array ( [diverts] => Array ( [5] => Array ( [destinationSetId] => Array ( [isEmpty] => Value is required and can't be empty ) ) ) ) ) ------------------------------- Cheers, Greg. -- Greg Frith Sent with Sparrow (http://www.sparrowmailapp.com/?sig)