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)

Reply via email to