Содержание


Collections


Collections, колекции, form, ЗФ2, Zend Framework 2, ZF2, ру, ru




Часто группы и элементы формы соответствуют другим объектам. Иногда они могут соответствовать коллекциям объектов. В этом случае, с точки зрения пользовательского интерфейса, Вы возможно захотите добавлять элементы динамически в интерфейсе пользователя -  хороший пример такой реализации добавление полей в списке задач. 

 

Рассмотрим пример, иллюстрирующий сказанное. Сначала создадим область объектов, которую будем использовать:

namespace Application\Entity;
 
class Product
{
    /**
     * @var string
     */
    protected $name;
 
    /**
     * @var int
     */
    protected $price;
 
    /**
     * @var Brand
     */
    protected $brand;
 
    /**
     * @var array
     */
    protected $categories;
 
    /**
     * @param string $name
     * @return Product
     */
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }
 
    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
 
    /**
     * @param int $price
     * @return Product
     */
    public function setPrice($price)
    {
        $this->price = $price; 
        return $this;
    }
 
    /**
     * @return int
     */
    public function getPrice()
    {
        return $this->price;
    }
 
    /**
     * @param Brand $brand
     * @return Product
     */
    public function setBrand(Brand $brand)
    {
        $this->brand = $brand; 
        return $this;
    }
 
    /**
     * @return Brand
     */
    public function getBrand()
    {
        return $this->brand;
    }
 
    /**
     * @param array $categories
     * @return Product
     */
    public function setCategories(array $categories)
    {
        $this->categories = $categories;
        return $this;
    }
 
    /**
     * @return array
     */
    public function getCategories()
    {
        return $this->categories;
    }
}
namespace ApplicationEntity

class Brand { /** * @var string */ protected $name;   /** * @var string */ protected $url;   /** * @param string $name * @return Brand */ public function setName($name) { $this->name = $name; return $this; }   /** * @return string */ public function getName() { return $this->name; }   /** * @param string $url * @return Brand */ public function setUrl($url) { $this->url = $url; return $this; }   /** * @return string */ public function getUrl() { return $this->url; } }  
namespace ApplicationEntity;
 
class Category
{
    /**
     * @var string
     */
    protected $name;
 
    /**
     * @param string $name
     * @return Category
     */
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }
 
    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
}

Довольно легкий для понимания код. В классе «Product» имеется две скалярные переменные «name» и «price». Связь один-к-одному: у одного продукта один бренд. Связь один-ко-многим: у одного продукта может быть много категорий.

 

Создание Группы (Fieldsets)



 

Создадим три группы. Каждая группа будет содержать все необходимые поля и связи для заданной сущности.

 

Группа «Brand»:

namespace Application\Form;
 
use Application\Entity\Brand;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
 
class BrandFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('brand');
        $this->setHydrator(new ClassMethodsHydrator(false))
            ->setObject(new Brand());
 
        $this->add(array(
            'name' => 'name',
            'options' => array(
                'label' => 'Name of the brand'
            ),
            'attributes' => array(
                'required' => 'required'
            )
        ));
 
        $this->add(array(
            'name' => 'url',
            'type' => 'Zend\Form\Element\Url',
            'options' => array(
                'label' => 'Website of the brand'
            ),
            'attributes' => array(
                'required' => 'required'
            )
        ));
    }
 
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return array(
            'name' => array(
                'required' => true,
            )
        );
    }
}

Тут видим пару новых вещей. Группа (fieldset) вызывает метод «setHydrator()», передавая в него гидратор «ClassMethods», и метод «setObject()» передавая в него пустой экземпляр объекта «Brand».

 

Когда данные будут проходить валидацию, Форма (Form) автоматически проитерирует все содержимое заданных полей и автоматически заполнит вспомогательные объекты, и вернет полную сущность (entity).

 

Элемент «Url» имееттип «Zend\Form\Element\Url». Эта информация будет использоваться при валидации «input field». Вам нет необходимости добавлять вручную фильтры и валидаторы к элементу, так как  он предоставляет достаточно спецификаций.

 

И наконец, метод «getInputFilterSpecification()» передает спецификации оставшемуся «input» («name»), сообщая, что этот input -  это name. Обратите внимание, что «required» в массиве «attributes» (при добавлении элементов) используется только для обязательных атрибутов.

 

Группа «Category»:

namespace ApplicationForm;
 
use Application\Entity\Category;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
 
class CategoryFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('category');
        $this->setHydrator(new ClassMethodsHydrator(false))
             ->setObject(new Category());
 
        $this->setLabel('Category');
 
        $this->add(array(
            'name' => 'name',
            'options' => array(
                'label' => 'Name of the category'
            ),
            'attributes' => array(
                'required' => 'required'
            )
        ));
    }
 
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return array(
            'name' => array(
                'required' => true,
            )
        );
    }
}

Группа «Product»:

namespace Application\Form;
 
use Application\Entity\Product;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
 
class ProductFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('product');
        $this->setHydrator(new ClassMethodsHydrator(false))
             ->setObject(new Product());
 
        $this->add(array(
            'name' => 'name',
            'options' => array(
                'label' => 'Name of the product'
            ),
            'attributes' => array(
                'required' => 'required'
            )
        ));
 
        $this->add(array(
            'name' => 'price',
            'options' => array(
                'label' => 'Price of the product'
            ),
            'attributes' => array(
                'required' => 'required'
            )
        ));
 
        $this->add(array(
            'type' => 'Application\Form\BrandFieldset',
            'name' => 'brand',
            'options' => array(
                'label' => 'Brand of the product'
            )
        ));
 
        $this->add(array(
            'type' => 'Zend\Form\Element\Collection',
            'name' => 'categories',
            'options' => array(
                'label' => 'Please choose categories for this product',
                'count' => 2,
                'should_create_template' => true,
                'allow_add' => true,
                'target_element' => array(
                    'type' => 'Application\Form\CategoryFieldset'
                )
            )
        ));
    }
 
    /**
     * Should return an array specification compatible with
     * {@link ZendInputFilterFactory::createInputFilter()}.
     *
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return array(
            'name' => array(
                'required' => true,
            ),
            'price' => array(
                'required' => true,
                'validators' => array(
                    array(
                        'name' => 'Float'
                    )
                )
            )
        );
    }
}

И снова много новенького!

 

Рассмотрим как добавляются элементы «brand»:сначала указываем тип «Application\Form\BrandFieldset». Так мы создаем связь один-к-одному. Когда форму проходит валидацию, «BrandFieldset» будет заполнен, а потом вернет сущность (entity) – благодаря гидратору и связи группы с сущностью «Brand», используя метод «setObject()».  Затем эта сущность «Brand» будет использоваться для заполнения сущности «Product» через метод «setBrand()».

 

Далее создается связь один-ко-многим. Тип «Zend\Form\Element\Collection», который является специальным элементом, использующимся в подобных случаях. Как Вы заметили, имя элемента «categories» совпадает с именем свойства в сущности «Product».

 

У этого элемента есть несколько интересных опций:

 

- count

Сколько раз этот элемент должен рендериться. В этом примере установлено в «2».

 

- should_create_template

Если установлено в «TRUE», то будет генерировать шаблон разметки в элементе «<span> » , для того, чтобы упростить процесс добавления нового элемента на лету (мы будем говорить об этом позже).

 

- allow_add

Если установлено в «TRUE» (значение по умолчанию), то динамическое добавление элементов будет подключено и проверено. Иначе будет игнорироваться. Все зависит от потребностей разработчика.

 

- target_element

Это или элемент, или как в примере – массив, описывающий элемент или группу, которая будет использоваться в коллекции. В этом примере «target_element» это группа «Category».

 

 


Элемент формы (Form Element)

 

Теперь в нашем распоряжении имеется группа. Но это пока что только набор полей, а не форма. А валидацию может пройти только экземпляр формы.

 

Построим форму:

namespace Application\Form;
 
use Zend\FormForm;
use Zend\InputFilter\InputFilter;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
 
class CreateProduct extends Form
{
    public function __construct()
    {
        parent::__construct('create_product');
 
        $this->setAttribute('method', 'post')
             ->setHydrator(new ClassMethodsHydrator(false))
             ->setInputFilter(new InputFilter());
 
        $this->add(array(
            'type' => 'Application\Form\ProductFieldset',
            'options' => array(
                'use_as_base_fieldset' => true
            )
        ));
 
        $this->add(array(
            'type' => 'Zend\Form\Element\Csrf',
            'name' => 'csrf'
        ));
 
        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'type' => 'submit',
                'value' => 'Send'
            )
        ));
    }
}

Создание формы достаточно простое занятие, так как она определяет только группу «Product», а так же несколько других нужных полей (CSRF, Submit).

 

Использование use_base_fieldset сообщает форме приблизительно следующую информацию: привет, объект с которым ты связана существует, и связан с группой, которая вляется основной группой. Эту опцию в большинстве случаев устанавливают в «TRUE».

 

Самое замечательное в таком подходе, это то, что каждая сущность может иметь свою собственную группу, которую можно использовать повторно. Вы описываете элементы, фильтры, валидаторы для каждой сущности только один раз., а для каждого экземпляра формы просто собираете группы. Теперь не нужно будет добавлять поле «username» в каждую форму, в которой оно необходимо!

 


Контроллер

 

Создадим действие в контроллере

/**
  * @return array
  */
 public function indexAction()
 {
     $form = new CreateProduct();
     $product = new Product();
     $form->bind($product);
 
     if ($this->request->isPost()) {
         $form->setData($this->request->getPost());
 
         if ($form->isValid()) {
             var_dump($product);
         }
     }
 
     return array(
         'form' => $form
     );
 }


Шаблон

 

Создадим шаблон

<?php
$form->setAttribute('action', $this->url('home'))
     ->prepare();
 
echo $this->form()->openTag($form);
 
$product = $form->get('product');
 
echo $this->formRow($product->get('name'));
echo $this->formRow($product->get('price'));
echo $this->formCollection($product->get('categories'));
 
$brand = $product->get('brand');
 
echo $this->formRow($brand->get('name'));
echo $this->formRow($brand->get('url'));
 
echo $this->formHidden($form->get('csrf'));
echo $this->formElement($form->get('submit'));
 
echo $this->form()->closeTag();

И снова немного о новом:

 

- Метод «prepare()». Вызывается в шаблоне вида до рендеринга чего угодно.

- Помощник вида «FormRow». Рендерит (отображает) лейбл (если есть), непосредственно само поле и ошибки.

-  Помощник вида «FormCollection» проходит через каждый элемент коллекции и рендерит каждый элемент через помощник «FormRow» (если нужно, можете задать другой помощник, используя  метод «setElementHelper()» в экземпляре помощника «FormCollection»).

Если же Вам необходим больший контроль при рендеринге, можете итерировать и рендерить вручную.

 

В итоге получим результат:





Как можно наблюдать, коллекции находятся внутри групп,  и каждый элемент коллекции так же находится внутри группы. Каждый элемент коллекции использует лейбл для каждого элемента, а лейбл самой коллекции используется как имя группы. Что б использовать этот функционал -  у вас должны лейблы быть заданы на все элементы. Если же Вам не нужно создавать группы, а только создать элементы внутри групп, то передайте «FALSE» как второй параметр в помощник вида «FormCollection».

 

Если вы отправите форму, то все элементы не пройдут валидацию и высветиться ошибка. Это нормально. Так как все элементы помечены как «required» -  обязательны к заполнению. После заполнения и отправки формы, прохождения валидации мы увидим:





Прикрепленный объект заполнен, и не массивом, а объектами!

 


Динамическое добавление нового элемента в форму

 

Помните «should_create_template»? Теперь используем его. Часть формы далеко не статичны. Например в нашем случае мы не хоти только две категории,  а ч то б пользователь мог добавлять сколько нужно ему. Zend\Form предоставляет и такие возможности.  Сначала посмотрим, что создается, когда мы создаем шаблон:




Коллекция создает две группы (две категории) и «span» с шаблоном атрибутов, содержащий  полный HTML код, который нужно просто скопировать, что б создать элемент. Конечно, «__index__» (это заполнитель) будет заменен на нужное валидное значение.  Сейчас имеется два элемента (categories[0] и categories[1]), поэтому «__index__» будет заменен на «2».

 

Если необходимо, то заполнитель («__index__» по умолчанию) может быть изменен, используя ключ опций «template_placeholder»:

$this->add(array(
    'type' => 'Zend\Form\Element\Collection',
    'name' => 'categories',
    'options' => array(
        'label' => 'Please choose categories for this product',
        'count' => 2,
        'should_create_template' => true,
        'template_placeholder' => '__placeholder__',
        'target_element' => array(
            'type' => 'Application\Form\Category\Fieldset'
        )
    )
));

Сначала добавим кнопочку «Add new category» где нибудь в форме:

<button onclick="return add_category()">Add a new category</button>

Функция «add_category»:

 

1) Первое, подсчитываем количество элементов, которое уже имеется.

2) Получаем шаблон  с атрибута шаблона «span»

3) Меняем заполнитель (placeholder) на нужное значение.

4) Добавляем элемент в DOM.

<script>
    function add_category() {
        var currentCount = $('form > fieldset > fieldset').length;
        var template = $('form > fieldset > span').data('template');
        template = template.replace(/__index__/g, currentCount);
 
        $('form > fieldset').append(template);
 
        return false;
    }
</script>

В предыдущем примере «$()» эквивалентно «$()» в jQuery или «dojo.query» в Dojo.

 

В примере используется «currentCount», а не «currentCount + 1», так как индексы начинаются с нуля (тоесть если будет три элемента, то его индекс будет два «2»).

 

Теперь новый элемент будет принят и сможет проходить фильтрацию, валидацию и т.д.





Если же Вы хотите запретить динамическое добавление новых элементов необходимо установить «FALSE»  в опции «allow_add». Тогда, даже если новые элементы будут добавлены, они не смогут пройти валидацию и не будут добавлены в сущность. А если не нужны элементы, то не нужны и шаблоны. Сделаем тогда  так:

$this->add(array(
    'type' => 'Zend\Form\Element\Collection',
    'name' => 'categories',
    'options' => array(
        'label' => 'Please choose categories for this product',
        'count' => 2,
        'should_create_template' => false,
        'allow_add' => false,
        'target_element' => array(
            'type' => 'Application\Form\CategoryFieldset'
        )
    )
));

Существуют некоторые ограничения этой возможности:

 

- Хотя Вы можете добавлять и удалять элементы, Вы НЕ можете удалить больше, чем было изначально.

- Динамически добавляемые элементы добавляются в конец коллекции.   Вы их можете добавить куда угодно, но если во время валидации возникнут ошибки, то они автоматически будут добавлены в конец коллекции.

 

Валидация групп для группировок и коллекций.



Валидация групп позволяет проверить подмножество полей.

Как пример, у сущности «Brand» имеется свойство URL, но мы не хотим указывать его в форме, что б он был доступен каждому. Изменим шаблон вида и уберем URL поле:

<?php
$form->setAttribute('action', $this->url('home'))
     ->prepare();
 
echo $this->form()->openTag($form);
 
$product = $form->get('product');
 
echo $this->formRow($product->get('name'));
echo $this->formRow($product->get('price'));
echo $this->formCollection($product->get('categories'));
 
$brand = $product->get('brand');
 
echo $this->formRow($brand->get('name'));
 
echo $this->formHidden($form->get('csrf'));
echo $this->formElement($form->get('submit'));
 
echo $this->form()->closeTag();

И получим:

 


Поле URL исчезло, но если мы заполним каждое поле, форма не пройдет валидации. Просто мы указали, что поле URL – обязательно. А так как в форме его нету -  оно не может пройти валидации, даже если мы не добавили его в шаблон вида.

 

Можно создать группу «BrandFieldsetWithoutURL», но так делать не рекомендуют, так как много кода будет повторяться.

 

Решение: валидация групп. Она определена в объекте Form, передавая массив элементов, который должны проходить валидацию:

namespace Application\Form;
 
use Zend\Form\Form;
use Zend\InputFilter\InputFilter;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
 
class CreateProduct extends Form
{
    public function __construct()
    {
        parent::__construct('create_product');
 
        $this->setAttribute('method', 'post')
             ->setHydrator(new ClassMethodsHydrator())
             ->setInputFilter(new InputFilter());
 
        $this->add(array(
            'type' => 'Application\Form\Product\Fieldset',
            'options' => array(
                'use_as_base_fieldset' => true
            )
        ));
 
        $this->add(array(
            'type' => 'Zend\Form\Element\Csrf',
            'name' => 'csrf'
        ));
 
        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'type' => 'submit',
                'value' => 'Send'
            )
        ));
 
        $this->setValidationGroup(array(
            'csrf',
            'product' => array(
                'name',
                'price',
                'brand' => array(
                    'name'
                ),
                'categories' => array(
                    'name'
                )
            )
        ));
    }
}

Так же не забудьте добавить элемент «CSRF».

Есть оно ограничение: группы валидации назначаются на основе коллекций, а не элементов. Тоесть, Вы НЕ можете сделать так: проверять поле «name» для первого элемента категории коллекции, и не проверять для второго. Но обычно, такая ситуция – это действительно крайний случай.

 

Все, теперь форма проходит валидацию.


Автор статьи: DuB