phpzend-framework2zend-formzend-inputfilter

zf2 Form Collection Validation - Unique Elements in Fieldsets


I want to add unique Elements in a Zend Form Collection. I found this awesome work from Aron Kerr

I do the forms and fieldsets like in Aron Kerr´s Example and it works fine.

In my case i create a Form to insert a collection of stores from a company.

My Form

First of all i have a Application\Form\CompanyStoreForm with a StoreFieldset like this:

$this->add(array(
                'name' => 'company',
                'type' => 'Application\Form\Stores\CompanyStoresFieldset',
            ));

The Fieldsets

Application\Form\Stores\CompanyStoresFieldset has a Collection of Store Entities like this:

$this->add(array(
            'type' => 'Zend\Form\Element\Collection',
            'name' => 'stores',
            'options' => array(
                'target_element' => array(
                    'type' => 'Application\Form\Fieldset\StoreEntityFieldset',
                ),
            ),
        ));

Application\Form\Fieldset\StoreEntityFieldset

$this->add(array(
            'name' => 'storeName',
            'attributes' => ...,
            'options' => ...,
        ));

        //AddressFieldset
        $this->add(array(
            'name' => 'address',
            'type' => 'Application\Form\Fieldset\AddressFieldset',
        ));

The difference to Arron Kerrs CategoryFieldset is I adding one more fieldset: Application\Form\Fieldset\AddressFieldset

Application\Form\Fieldset\AddressFieldset has a text-element streetName.

The Inputfilters

The CompanyStoresFieldsetInputFilter has no elements to validate.

The StoreEntityFieldsetInputFilter has validators for storeName and the Application\Form\Fieldset\AddressFieldset like this

public function __construct() {
        $factory = new InputFactory(); 

        $this->add($factory->createInput([ 
            'name' => 'storeName', 
            'required' => true, 
            'filters' => array( ....
            ),
            'validators' => array(...
            ),
        ]));

        $this->add(new AddressFieldsetInputFilter(), 'address');

    }

The AddressFieldset has another Inputfilter AddressFieldsetInputFilter. In AddressFieldsetInputFilter I adding a InputFilter for streetName.

FormFactory

Adding all Inputfilters to the Form like this

    public function createService(ServiceLocatorInterface $serviceLocator) {
            $form = $serviceLocator->get('FormElementManager')->get('Application\Form\CompanyStoreForm');

            //Create a Form Inputfilter
            $formFilter = new InputFilter();

            //Create Inputfilter for CompanyStoresFieldsetInputFilter()
            $formFilter->add(new CompanyStoresFieldsetInputFilter(), 'company');

            //Create Inputfilter for StoreEntityFieldsetInputFilter()
            $storeInputFilter = new CollectionInputFilter();
            $storeInputFilter->setInputFilter(new StoreEntityFieldsetInputFilter());
            $storeInputFilter->setUniqueFields(array('storeName'));
            $storeInputFilter->setMessage('Just insert one entry with this store name.');
            $formFilter->get('company')->add($storeInputFilter, 'stores');


            $form->setInputFilter($formFilter);


            return $form;
        }

I use Aron Kerrs CollectionInputFilter.

The storeName should be unique in the whole collection. All works fine, so far!

But now my problem!

The streetName should be unique in the whole collection. But the Element is in the AddressFieldset. I can´t do something like this:

$storeInputFilter->setUniqueFields(array('storeName', 'address' => array('streetName')));

I thought I should extend Aron Kerrs isValid() from CollectionInputFilter

Aron Kerrs Original isValid()

public function isValid()
{
    $valid = parent::isValid();

// Check that any fields set to unique are unique
if($this->uniqueFields)
{
    // for each of the unique fields specified spin through the collection rows and grab the values of the elements specified as unique.
    foreach($this->uniqueFields as $k => $elementName)
    {
        $validationValues = array();
        foreach($this->collectionValues as $rowKey => $rowValue)
        {
            // Check if the row has a deleted element and if it is set to 1. If it is don't validate this row.
            if(array_key_exists('deleted', $rowValue) && $rowValue['deleted'] == 1) continue;

            $validationValues[] = $rowValue[$elementName];
        }


        // Get only the unique values and then check if the count of unique values differs from the total count
        $uniqueValues = array_unique($validationValues);
        if(count($uniqueValues) < count($validationValues))
        {            
            // The counts didn't match so now grab the row keys where the duplicate values were and set the element message to the element on that row
            $duplicates = array_keys(array_diff_key($validationValues, $uniqueValues));
            $valid = false;
            $message = ($this->getMessage()) ? $this->getMessage() : $this::UNIQUE_MESSAGE;
            foreach($duplicates as $duplicate)
            {
                $this->invalidInputs[$duplicate][$elementName] = array('unique' => $message);
            }
        }
    }

    return $valid;
}
}

First of all I try (just for testing) to add a error message to streetName in the first entry of the collection.

$this->invalidInputs[0]['address']['streetName'] = array('unique' => $message);

But it doens´t work.

Adding it to storeName it works

$this->invalidInputs[0]['storeName'] = array('unique' => $message);

I think the reason is the Fieldset has an own InputFilter()?

When i do a var_dump($this->collectionValues()) i received a multidimensional array of all values (also of the addressFieldset). That´s fine! But i can´t add error messages to the element in the fieldset.

How can i do this? I don´t want to insert all Elements of the AddressFieldset in the StoreEntityFieldset. (I use the AddressFieldset also in other Forms)


Solution

  • I figured it out. You simply can add values with

    $this->invalidInputs[<entry-key>]['address']['streetName'] = array('unique' => $message);
    

    I don´t know how it does not work yesterday. It was another bug.

    I wrote a solution for my problem. Maybe it´s not the best, but it works for me.

    CollectionInputFilter

    class CollectionInputFilter extends ZendCollectionInputFilter
    {    
        protected $uniqueFields;
        protected $validationValues = array();
        protected $message = array();
    
        const UNIQUE_MESSAGE = 'Each item must be unique within the collection';
    
        /**
         * @return the $message
         */
        public function getMessageByElement($elementName, $fieldset = null)
        {
            if($fieldset != null){
                return $this->message[$fieldset][$elementName];
            }
            return $this->message[$elementName];
        }
    
        /**
         * @param field_type $message
         */
        public function setMessage($message)
        {
            $this->message = $message;
        }
    
        /**
         * @return the $uniqueFields
         */
        public function getUniqueFields()
        {
            return $this->uniqueFields;
        }
    
     /**
         * @param multitype:string  $uniqueFields
         */
        public function setUniqueFields($uniqueFields)
        {
            $this->uniqueFields = $uniqueFields;
        }
    
        public function isValid()
        {
            $valid = parent::isValid();
    
            // Check that any fields set to unique are unique
            if($this->uniqueFields)
            {
                foreach($this->uniqueFields as $key => $elementOrFieldset){
                    // if the $elementOrFieldset is a fieldset, $key is our fieldset name, $elementOrFieldset is our collection of elements we have to check
                    if(is_array($elementOrFieldset) && !is_numeric($key)){
                        // We need to validate every element in the fieldset that should be unique
                        foreach($elementOrFieldset as $elementKey => $elementName){
                            // $key is our fieldset key, $elementName is the name of our element that should be unique
                            $validationValues = $this->getValidCollectionValues($elementName, $key);
    
                            // get just unique values
                            $uniqueValues = array_unique($validationValues);
    
                            //If we have a difference, not all are unique
                            if(count($uniqueValues) < count($validationValues))
                            {
                                // The counts didn't match so now grab the row keys where the duplicate values were and set the element message to the element on that row
                                $duplicates = array_keys(array_diff_key($validationValues, $uniqueValues));
                                $valid = false;
                                $message = ($this->getMessageByElement($elementName, $key)) ? $this->getMessageByElement($elementName, $key) : $this::UNIQUE_MESSAGE;
                                // set error messages
                                foreach($duplicates as $duplicate)
                                {
                                    //$duplicate = our collection entry key, $key is our fieldsetname
                                    $this->invalidInputs[$duplicate][$key][$elementName] = array('unique' => $message);
                                }
                            }
                        }
                    }
                    //its just a element in our collection, $elementOrFieldset is a simple element
                    else {
                        // in this case $key is our element key , we don´t need the second param because we haven´t a fieldset
                        $validationValues = $this->getValidCollectionValues($elementOrFieldset);
    
                        $uniqueValues = array_unique($validationValues);
                        if(count($uniqueValues) < count($validationValues))
                        {            
                            // The counts didn't match so now grab the row keys where the duplicate values were and set the element message to the element on that row
                            $duplicates = array_keys(array_diff_key($validationValues, $uniqueValues));
                            $valid = false;
                            $message = ($this->getMessageByElement($elementOrFieldset)) ? $this->getMessageByElement($elementOrFieldset) : $this::UNIQUE_MESSAGE;
                            foreach($duplicates as $duplicate)
                            {
                                $this->invalidInputs[$duplicate][$elementOrFieldset] = array('unique' => $message);
                            }
                        }
                    }
                }
    
            }
            return $valid;
        }
    
        /**
         * 
         * @param type $elementName
         * @param type $fieldset
         * @return type
         */
        public function getValidCollectionValues($elementName, $fieldset = null){
            $validationValues = array();
            foreach($this->collectionValues as $rowKey => $collection){
                // If our values are in a fieldset
                if($fieldset != null && is_array($collection[$fieldset])){
                    $rowValue = $collection[$fieldset][$elementName];
                }
                else{
                    //collection is one element like $key => $value
                    $rowValue = $collection[$elementName];
                }
                // Check if the row has a deleted element and if it is set to 1. If it is don't validate this row.
                if($rowValue == 1 && $rowKey == 'deleted') continue;
                $validationValues[$rowKey] = $rowValue;
            }
            return $validationValues;
        }
    
    
        public function getMessages()
        {
            $messages = array();
            if (is_array($this->getInvalidInput()) || $this->getInvalidInput() instanceof Traversable) {
                foreach ($this->getInvalidInput() as $key => $inputs) {
                    foreach ($inputs as $name => $input) {
                        if(!is_string($input) && !is_array($input))
                        {
                            $messages[$key][$name] = $input->getMessages();                                                
                            continue;
                        }         
                        $messages[$key][$name] = $input;
                    }
                }
            }
            return $messages;
        }
    }
    

    Define a CollectionInputFilter (in a factory)

    $storeInputFilter = new CollectionInputFilter();
            $storeInputFilter->setInputFilter(new StoreEntityFieldsetInputFilter());
            $storeInputFilter->setUniqueFields(array('storeName', 'address' => array('streetName')));
            $storeInputFilter->setMessage(array('storeName' => 'Just insert one entry with this store name.', 'address' => array('streetName' => 'You already insert a store with this street name')));
            $formFilter->get('company')->add($storeInputFilter, 'stores');
    

    So let me explain:

    Now, we can add elements as unique in fieldsets in our collection. We can NOT add collection fieldsets in our collection and not another fieldsets in our fieldsets. In my opinion if anyone want to do this cases, they better should refactor the form :-)

    setUniqueFields Add a simple element as unique

    array('your-unique-element','another-element'); 
    

    If you want to add a element as unique in a fieldset

    array('your-unique-element', 'fieldsetname' => array('your-unique-element-in-fieldset'))
    

    We can add special messages for every element with setMessage

    Add Message for a Element in the collection

    array('storeName' => 'Just insert one entry...')
    

    Add message for a Element in a fieldset

    array('fieldset-name' => array('your-unique-element-in-fieldset' => 'You already insert ..'))