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)