zf2 表单集合验证 - 字段集中的唯一元素

zf2 Form Collection Validation - Unique Elements in Fieldsets

我想在 Zend 表单集合中添加独特的元素。 我从 Aron Kerr

找到了这个很棒的作品

我按照 Aron Kerr 的示例中的方式处理表单和字段集,并且效果很好。

在我的例子中,我创建了一个表单来插入公司的商店集合。

我的表格

首先我有一个 Application\Form\CompanyStoreForm 和这样的 StoreFieldset:

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

字段集

Application\Form\Stores\CompanyStoresFieldset 有一个这样的商店实体集合:

$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',
        ));

与 Arron Kerrs CategoryFieldset 的不同之处在于我添加了一个字段集:Application\Form\Fieldset\AddressFieldset

Application\Form\Fieldset\AddressFieldset 有一个文本元素 streetName.

输入过滤器

CompanyStoresFieldsetInputFilter 没有要验证的元素。

StoreEntityFieldsetInputFilterstoreNameApplication\Form\Fieldset\AddressFieldset 的验证器,像这样

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

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

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

    }

AddressFieldset 有另一个 Inputfilter AddressFieldsetInputFilter。 在 AddressFieldsetInputFilter 中,我为 streetName.

添加了一个 InputFilter

FormFactory

像这样将所有输入过滤器添加到表单

    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;
        }

我使用 Aron Kerrs CollectionInputFilter。

storeName 在整个集合中应该是唯一的。 到目前为止一切正常!

但现在我的问题来了!

streetName 在整个集合中应该是唯一的。 但是元素在 AddressFieldset 中。 我不能做这样的事情:

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

我想我应该从 CollectionInputFilter

扩展 Aron Kerrs isValid()

Aron Kerrs 原创 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;
}
}

首先,我尝试(仅用于测试)在集合的第一个条目中向 streetName 添加一条错误消息。

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

但它不起作用。

将其添加到 storeName 有效

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

我认为原因是 Fieldset 有自己的 InputFilter()?

当我执行 var_dump($this->collectionValues()) 时,我收到了一个包含所有值(也是地址字段集)的多维数组。 没关系!但是我无法向字段集中的元素添加错误消息。

我该怎么做? 我不想在 StoreEntityFieldset 中插入 AddressFieldset 的所有元素。 (我在其他表单中也使用了 AddressFieldset)

我明白了。您只需使用

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

我不知道它昨天为什么不起作用。这是另一个错误。

我为我的问题写了一个解决方案。也许它不是最好的,但它适合我。

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;
    }
}

定义一个 CollectionInputFilter(在工厂中)

$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');

所以让我解释一下:

现在,我们可以在集合的字段集中添加元素作为唯一元素。 我们不能在我们的集合中添加集合字段集,也不能在我们的字段集中添加另一个字段集。 在我看来,如果有人想做这种情况,他们最好重构表格 :-)

setUniqueFields 添加一个简单的元素作为 unique

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

如果要将元素添加为字段集中的唯一元素

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

我们可以使用 setMessage

为每个元素添加特殊消息

为集合中的元素添加消息

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

为字段集中的元素添加消息

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