Часто группы и элементы формы соответствуют другим объектам. Иногда они могут соответствовать коллекциям объектов. В этом случае, с точки зрения пользовательского интерфейса, Вы возможно захотите добавлять элементы динамически в интерфейсе пользователя - хороший пример такой реализации добавление полей в списке задач.
Рассмотрим пример, иллюстрирующий сказанное. Сначала создадим область объектов, которую будем использовать:
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».
Теперь в нашем распоряжении имеется группа. Но это пока что только набор полей, а не форма. А валидацию может пройти только экземпляр формы.
Построим форму:
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» для первого элемента категории коллекции, и не проверять для второго. Но обычно, такая ситуция – это действительно крайний случай.
Все, теперь форма проходит валидацию.