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)
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 ..'))