diff --git a/book/zend.form.advanced-use-of-forms.md b/book/zend.form.advanced-use-of-forms.md new file mode 100644 index 00000000..642b7a3c --- /dev/null +++ b/book/zend.form.advanced-use-of-forms.md @@ -0,0 +1,453 @@ +# Advanced use of forms + +Beginning with Zend Framework 2.1, forms elements can be registered using a designated plugin +manager of Zend\\\\ServiceManager <zend.service-manager.intro>. This is similar to how view +helpers, controller plugins, and filters are registered. This new feature has a number of benefits, +especially when you need to handle complex dependencies in forms/fieldsets. This section describes +all the benefits of this new architecture in ZF 2.1. + +## Short names + +The first advantage of pulling form elements from the service manager is that now you can use short +names to create new elements through the factory. Therefore, this code: + +```php +$form->add(array( + 'type' => 'Zend\Form\Element\Email', + 'name' => 'email' +)); +``` + +can now be replaced by: + +```php +$form->add(array( + 'type' => 'Email', + 'name' => 'email' +)); +``` + +Each element provided out-of-the-box by Zend Framework 2 support this natively, so you can now make +your initialization code more compact. + +## Creating custom elements + +`Zend\Form` also supports custom form elements. + +To create a custom form element, make it extend the `Zend\Form\Element` class, or if you need a more +specific one, extend one of the `Zend\Form\Element` classes. + +In the following we will show how to create a custom `Phone` element for entering phone numbers. It +will extend `Zend\Form\Element` class and provide some default input rules. + +Our custom phone element could look something like this: + +```php +namespace Application\Form\Element; + +use Zend\Form\Element; +use Zend\InputFilter\InputProviderInterface; +use Zend\Validator\Regex as RegexValidator; + +class Phone extends Element implements InputProviderInterface +{ + /** + * @var ValidatorInterface + */ + protected $validator; + + /** + * Get a validator if none has been set. + * + * @return ValidatorInterface + */ + public function getValidator() + { + if (null === $this->validator) { + $validator = new RegexValidator('/^\+?\d{11,12}$/'); + $validator->setMessage('Please enter 11 or 12 digits only!', + RegexValidator::NOT_MATCH); + + $this->validator = $validator; + } + + return $this->validator; + } + + /** + * Sets the validator to use for this element + * + * @param ValidatorInterface $validator + * @return Application\Form\Element\Phone + */ + public function setValidator(ValidatorInterface $validator) + { + $this->validator = $validator; + return $this; + } + + /** + * Provide default input rules for this element + * + * Attaches a phone number validator. + * + * @return array + */ + public function getInputSpecification() + { + return array( + 'name' => $this->getName(), + 'required' => true, + 'filters' => array( + array('name' => 'Zend\Filter\StringTrim'), + ), + 'validators' => array( + $this->getValidator(), + ), + ); + } +} +``` + +By implementing the `Zend\InputFilter\InputProviderInterface` interface, we are hinting to our form +object that this element provides some default input rules for filtering and/or validating values. +In this example the default input specification provides a `Zend\Filter\StringTrim` filter and a +`Zend\Validator\Regex` validator that validates that the value optionally has a + sign at the +beginning and is followed by 11 or 12 digits. + +The easiest way of start using your new custom element in your forms is to use the custom element's +FQCN: + +```php +$form = new Zend\Form\Form(); +$form->add(array( + 'name' => 'phone', + 'type' => 'Application\Form\Element\Phone', +)); +``` + +Or, if you are extending `Zend\Form\Form`: + +```php +namespace Application\Form; + +use Zend\Form\Form; + +class MyForm extends Form +{ + public function __construct($name = null) + { + parent::__construct($name); + + $this->add(array( + 'name' => 'phone', + 'type' => 'Application\Form\Element\Phone', + )) + } +} +``` + +If you don't want to use the custom element's FQCN, but rather a short name, as of Zend Framework +2.1 you can do so by adding them to the `Zend\Form\FormElementManager` plugin manager by utilising +the `getFormElementConfig` function. + +> ## Warning +To use custom elements with the FormElementManager needs a bit more work and most likely a change in +how you write and use your forms. + +First, add the custom element to the plugin manager, in your `Module.php` class: + +```php +namespace Application; + +use Zend\ModuleManager\Feature\FormElementProviderInterface; + +class Module implements FormElementProviderInterface +{ + public function getFormElementConfig() + { + return array( + 'invokables' => array( + 'phone' => 'Application\Form\Element\Phone' + ) + ); + } +} +``` + +Or, you can do the same in your `module.config.php` file: + +```php +return array( + 'form_elements' => array( + 'invokables' => array( + 'phone' => 'Application\Form\Element\Phone' + ) + ) +); +``` + +You can use a factory instead of an invokable in order to handle dependencies in your +elements/fieldsets/forms. + +**And now comes the first catch.** + +If you are creating your form class by extending `Zend\Form\Form`, you *must not* add the custom +element in the `__construct`-or (as we have done in the previous example where we used the custom +element's FQCN), but rather in the `init()` method: + +```php +namespace Application\Form; + +use Zend\Form\Form; + +class MyForm extends Form +{ + public function init() + { + $this->add(array( + 'name' => 'phone', + 'type' => 'Phone', + )); + } +} +``` + +**The second catch** is that you *must not* directly instantiate your form class, but rather get an +instance of it through the `Zend\Form\FormElementManager`: + +```php +namespace Application\Controller; + +use Zend\Mvc\Controller\AbstractActionController; + +class IndexController extends AbstractActionController +{ + public function indexAction() + { + $sl = $this->getServiceLocator(); + $form = $sl->get('FormElementManager')->get('\Application\Form\MyForm'); + return array('form' => $form); + } +} +``` + +The biggest gain of this is that you can easily override any built-in Zend Framework form elements +if they do not fit your needs. For instance, if you want to create your own Email element instead of +the standard one, you can simply create your element and add it to the form element config with the +same key as the element you want to replace: + +```php +namespace Application; + +use Zend\ModuleManager\Feature\FormElementProviderInterface; + +class Module implements FormElementProviderInterface +{ + public function getFormElementConfig() + { + return array( + 'invokables' => array( + 'Email' => 'Application\Form\Element\MyEmail' + ) + ); + } +} +``` + +Now, whenever you'll create an element whose `type` is 'Email', it will create the custom Email +element instead of the built-in one. + +> ## Note +if you want to be able to use both the built-in one and your own one, you can still provide the FQCN +of the element, i.e. `Zend\Form\Element\Email`. + +As you can see here, we first get the form manager (that we modified in our Module.php class), and +create the form by specifying the fully qualified class name of the form. Please note that you don't +need to add `Application\Form\MyForm` to the invokables array. If it is not specified, the form +manager will just instantiate it directly. + +In short, to create your own form elements (or even reusable fieldsets !) and be able to use them in +your form using the short-name notation, you need to: + +1. Create your element (like you did before). +2. Add it to the form element manager by defining the `getFormElementConfig`, exactly like using +`getServiceConfig()` and `getControllerConfig`. +3. Make sure the custom form element is not added in the form's `__construct`-or, but rather in +it's `init()` method, or after getting an instance of the form. +4. Create your form through the form element manager instead of directly instantiating it. + +## Handling dependencies + +One of the most complex issues in `Zend\Form 2.0` was dependency management. For instance, a very +frequent use case is a form that creates a fieldset, that itself need access to the database to +populate a `Select` element. Previously in such a situation, you would either rely on the Registry +using the Singleton pattern, or either you would "transfer" the dependency from controller to form, +and from form to fieldset (and even from fieldset to another fieldset if you have a complex form). +This was ugly and not easy to use. Hopefully, `Zend\ServiceManager` solves this use case in an +elegant manner. + +For instance, let's say that a form create a fieldset called `AlbumFieldset`: + +```php +namespace Application\Form; + +use Zend\Form\Form; + +class CreateAlbum extends Form +{ + public function init() + { + $this->add(array( + 'name' => 'album', + 'type' => 'AlbumFieldset' + )); + } +} +``` + +Let's now create the `AlbumFieldset` that depends on an `AlbumTable` object that allows you to fetch +albums from the database. + +```php +namespace Application\Form; + +use Album\Model\AlbumTable; +use Zend\Form\Fieldset; + +class AlbumFieldset extends Fieldset +{ + public function __construct(AlbumTable $albumTable) + { + // Add any elements that need to fetch data from database + // using the album table ! + } +} +``` + +For this to work, you need to add a line to the form element manager by adding an element to your +Module.php class: + +```php +namespace Application; + +use Application\Form\AlbumFieldset; +use Zend\ModuleManager\Feature\FormElementProviderInterface; + +class Module implements FormElementProviderInterface +{ + public function getFormElementConfig() + { + return array( + 'factories' => array( + 'AlbumFieldset' => function($sm) { + // I assume here that the Album\Model\AlbumTable + // dependency have been defined too + + $serviceLocator = $sm->getServiceLocator(); + $albumTable = $serviceLocator->get('Album\Model\AlbumTable'); + $fieldset = new AlbumFieldset($albumTable); + return $fieldset; + } + ) + ); + } +} +``` + +Create your form using the form element manager instead of directly instantiating it: + +```php +public function testAction() +{ + $formManager = $this->serviceLocator->get('FormElementManager'); + $form = $formManager->get('Application\Form\CreateAlbum'); +} +``` + +Finally, to use your fieldset in a view you need to use the formCollection function. + +```php +echo $this->form()->openTag($form); +echo $this->formCollection($form->get('album')); +echo $this->form()->closeTag(); +``` + +Et voilĂ ! The dependency will be automatically handled by the form element manager, and you don't +need to create the `AlbumTable` in your controller, transfer it to the form, which itself passes it +over to the fieldset. + +## The specific case of initializers + +In the previous example, we explicitly defined the dependency in the constructor of the +`AlbumFieldset` class. However, in some cases, you may want to use an initializer (like +`Zend\ServiceManager\ServiceLocatorAwareInterface`) to inject a specific object to all your +forms/fieldsets/elements. + +The problem with initializers is that they are injected AFTER the construction of the object, which +means that if you need this dependency when you create elements, it won't be available yet. For +instance, this example **won't work**: + +```php +namespace Application\Form; + +use Album\Model; +use Zend\Form\Fieldset; +use Zend\ServiceManager\ServiceLocatorAwareInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class AlbumFieldset extends Fieldset implements ServiceLocatorAwareInterface +{ + protected $serviceLocator; + + public function __construct() + { + // Here, $this->serviceLocator is null because it has not been + // injected yet, as initializers are run after __construct + } + + public function setServiceLocator(ServiceLocatorInterface $sl) + { + $this->serviceLocator = $sl; + } + + public function getServiceLocator() + { + return $this->serviceLocator; + } +} +``` + +Thankfully, there is an easy workaround: every form element now implements the new interface +`Zend\Stdlib\InitializableInterface`, that defines a single `init()` function. In the context of +form elements, this `init()` function is automatically called once all the dependencies (including +all initializers) are resolved. Therefore, the previous example can be rewritten as such: + +```php +namespace Application\Form; + +use Album\Model; +use Zend\Form\Fieldset; +use Zend\ServiceManager\ServiceLocatorAwareInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class AlbumFieldset extends Fieldset implements ServiceLocatorAwareInterface +{ + protected $serviceLocator; + + public function init() + { + // Here, we have $this->serviceLocator !! + } + + public function setServiceLocator(ServiceLocatorInterface $sl) + { + $this->serviceLocator = $sl; + } + + public function getServiceLocator() + { + return $this->serviceLocator; + } +} +``` diff --git a/book/zend.form.collections.md b/book/zend.form.collections.md new file mode 100644 index 00000000..e4bc006b --- /dev/null +++ b/book/zend.form.collections.md @@ -0,0 +1,804 @@ +# Form Collections + +Often, fieldsets or elements in your forms will correspond to other domain objects. In some cases, +they may correspond to collections of domain objects. In this latter case, in terms of user +interfaces, you may want to add items dynamically in the user interface -- a great example is adding +tasks to a task list. + +This document is intended to demonstrate these features. To do so, we first need to define some +domain objects that we'll be using. + +```php +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; + } +} + +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; + } +} + +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; + } +} +``` + +As you can see, this is really simple code. A Product has two scalar properties (name and price), a +OneToOne relationship (one product has one brand), and a OneToMany relationship (one product has +many categories). + +## Creating Fieldsets + +The first step is to create three fieldsets. Each fieldset will contain all the fields and +relationships for a specific entity. + +Here is the `Brand` fieldset: + +```php +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, + ), + ); + } +} +``` + +We can discover some new things here. As you can see, the fieldset calls the method `setHydrator()`, +giving it a `ClassMethods` hydrator, and the `setObject()` method, giving it an empty instance of a +concrete `Brand` object. + +When the data will be validated, the `Form` will automatically iterate through all the field sets it +contains, and automatically populate the sub-objects, in order to return a complete entity. + +Also notice that the `Url` element has a type of `Zend\Form\Element\Url`. This information will be +used to validate the input field. You don't need to manually add filters or validators for this +input as that element provides a reasonable input specification. + +Finally, `getInputFilterSpecification()` gives the specification for the remaining input ("name"), +indicating that this input is required. Note that *required* in the array "attributes" (when +elements are added) is only meant to add the "required" attribute to the form markup (and therefore +has semantic meaning only). + +Here is the `Category` fieldset: + +```php +namespace Application\Form; + +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, + ), + ); + } +} +``` + +Nothing new here. + +And finally the `Product` fieldset: + +```php +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 Zend\InputFilter\Factory::createInputFilter()}. + * + * @return array + */ + public function getInputFilterSpecification() + { + return array( + 'name' => array( + 'required' => true, + ), + 'price' => array( + 'required' => true, + 'validators' => array( + array( + 'name' => 'Float', + ), + ), + ), + ); + } +} +``` + +We have a lot of new things here! + +First, notice how the brand element is added: we specify it to be of type +`Application\Form\BrandFieldset`. This is how you handle a OneToOne relationship. When the form is +validated, the `BrandFieldset` will first be populated, and will return a `Brand` entity (as we have +specified a `ClassMethods` hydrator, and bound the fieldset to a `Brand` entity using the +`setObject()` method). This `Brand` entity will then be used to populate the `Product` entity by +calling the `setBrand()` method. + +The next element shows you how to handle OneToMany relationship. The type is +`Zend\Form\Element\Collection`, which is a specialized element to handle such cases. As you can see, +the name of the element ("categories") perfectly matches the name of the property in the `Product` +entity. + +This element has a few interesting options: + +- `count`: this is how many times the element (in this case a category) has to be rendered. We've +set it to two in this examples. +- `should_create_template`: if set to `true`, it will generate a template markup in a `` +element, in order to simplify adding new element on the fly (we will speak about this one later). +- `allow_add`: if set to `true` (which is the default), dynamically added elements will be retrieved +and validated; otherwise, they will be completely ignored. This, of course, depends on what you want +to do. +- `target_element`: this is either an element or, as this is the case in this example, an array that +describes the element or fieldset that will be used in the collection. In this case, the +`target_element` is a `Category` fieldset. + +## The Form Element + +So far, so good. We now have our field sets in place. But those are field sets, not forms. And only +`Form` instances can be validated. So here is the form : + +```php +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(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', + ), + )); + } +} +``` + +`CreateProduct` is quite simple, as it only defines a `Product` fieldset, as well as some other +useful fields (`CSRF` for security, and a `Submit` button). + +Notice the `use_as_base_fieldset` option. This option is here to say to the form: "hey, the object I +bind to you is, in fact, bound to the fieldset that is the base fieldset." This will be to true most +of the times. + +What's cool with this approach is that each entity can have its own `Fieldset` and can be reused. +You describe the elements, the filters, and validators for each entity only once, and the concrete +`Form` instance will only compose those fieldsets. You no longer have to add the "username" input to +every form that deals with users! + +## The Controller + +Now, let's create the action in the controller: + +```php +/** + * @return array + */ + public function indexAction() + { + $form = new CreateProduct(); + $product = new Product(); + $form->bind($product); + + $request = $this->getRequest(); + if ($request->isPost()) { + $form->setData($request->getPost()); + + if ($form->isValid()) { + var_dump($product); + } + } + + return array( + 'form' => $form, + ); + } +``` + +This is super easy. Nothing to do in the controllers. All the magic is done behind the scene. + +## The View + +And finally, the view: + +```php +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(); +``` + +A few new things here : + +- the `prepare()` method. You *must* call it prior to rendering anything in the view (this function +is only meant to be called in views, not in controllers). +- the `FormRow` helper renders a label (if present), the input itself, and errors. +- the `FormCollection` helper will iterate through every element in the collection, and render every +element with the FormRow helper (you may specify an alternate helper if desired, using the +`setElementHelper()` method on that `FormCollection` helper instance). If you need more control +about the way you render your forms, you can iterate through the elements in the collection, and +render them manually one by one. + +Here is the result: + +![image](../images/zend.form.collections.view.png) + +As you can see, collections are wrapped inside a fieldset, and every item in the collection is +itself wrapped in the fieldset. In fact, the `Collection` element uses label for each item in the +collection, while the label of the `Collection` element itself is used as the legend of the +fieldset. You must have a label on every element in order to use this feature. If you don't want the +fieldset created, but just the elements within it, simply add a boolean `false` as the second +parameter of the `FormCollection` view helper. + +If you validate, all elements will show errors (this is normal, as we've marked them as required). +As soon as the form is valid, this is what we get : + +![image](../images/zend.form.collections.view.result.png) + +As you can see, the bound object is completely filled, not with arrays, but with objects! + +But that's not all. + +## Adding New Elements Dynamically + +Remember the `should_create_template`? We are going to use it now. + +Often, forms are not completely static. In our case, let's say that we don't want only two +categories, but we want the user to be able to add other ones at runtime. `Zend\Form` has this +capability. First, let's see what it generates when we ask it to create a template: + +![image](../images/zend.form.collections.dynamic-elements.template.png) + +As you can see, the collection generates two fieldsets (the two categories) *plus* a span with a +`data-template` attribute that contains the full HTML code to copy to create a new element in the +collection. Of course `__index__` (this is the placeholder generated) has to be changed to a valid +value. Currently, we have 2 elements (`categories[0]` and `categories[1]`, so `__index__` has to be +changed to 2. + +If you want, this placeholder (`__index__` is the default) can be changed using the +`template_placeholder` option key: + +```php +$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\CategoryFieldset', + ), + ), +)); +``` + +First, let's add a small button "Add new category" anywhere in the form: + +```php + +``` + +The `add_category` function is fairly simple: + +1. First, count the number of elements we already have. +2. Get the template from the `span`'s `data-template` attribute. +3. Change the placeholder to a valid index. +4. Add the element to the DOM. + +Here is the code: + +```php + +``` + +(Note: the above example assumes `$()` is defined, and equivalent to jQuery's `$()` function, Dojo's +`dojo.query`, etc.) + +One small remark about the `template.replace`: the example uses `currentCount` and not `currentCount ++ 1`, as the indices are zero-based (so, if we have two elements in the collection, the third one +will have the index `2`). + +Now, if we validate the form, it will automatically take into account this new element by validating +it, filtering it and retrieving it: + +![image](../images/zend.form.collections.dynamic-elements.result.png) + +Of course, if you don't want to allow adding elements in a collection, you must set the option +`allow_add` to `false`. This way, even if new elements are added, they won't be validated and hence, +not added to the entity. Also, if we don't want elements to be added, we don't need the data +template, either. Here's how you do it: + +```php +$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', + ), + ), +)); +``` + +There are some limitations to this capability: + +- Although you can add new elements and remove them, you *CANNOT* remove more elements in a +collection than the initial count (for instance, if your code specifies `count == 2`, you will be +able to add a third one and remove it, but you won't be able to remove any others. If the initial +count is 2, you *must* have at least two elements. +- Dynamically added elements have to be added at the end of the collection. They can be added +anywhere (these elements will still be validated and inserted into the entity), but if the +validation fails, this newly added element will be automatically be replaced at the end of the +collection. + +## Validation groups for fieldsets and collection + +Validation groups allow you to validate a subset of fields. + +As an example, although the `Brand` entity has a `URL` property, we don't want the user to specify +it in the creation form (but may wish to later in the "Edit Product" form, for instance). Let's +update the view to remove the `URL` input: + +```php +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(); +``` + +This is what we get: + +![image](../images/zend.form.collections.validation-groups.png) + +The `URL` input has disappeared, but even if we fill every input, the form won't validate. In fact, +this is normal. We specified in the input filter that the `URL` is a *required* field, so if the +form does not have it, it won't validate, even though we didn't add it to the view! + +Of course, you could create a `BrandFieldsetWithoutURL` fieldset, but of course this is not +recommended, as a lot of code will be duplicated. + +The solution: validation groups. A validation group is specified in a `Form` object (hence, in our +case, in the `CreateProduct` form) by giving an array of all the elements we want to validate. Our +`CreateProduct` now looks like this: + +```php +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\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', + ), + )); + + $this->setValidationGroup(array( + 'csrf', + 'product' => array( + 'name', + 'price', + 'brand' => array( + 'name', + ), + 'categories' => array( + 'name', + ), + ), + )); + } +} +``` + +Of course, don't forget to add the `CSRF` element, as we want it to be validated too (but notice +that I didn't write the submit element, as we don't care about it). You can recursively select the +elements you want. + +There is one simple limitation currently: validation groups for collections are set on a +per-collection basis, not per-element in a collection basis. This means you cannot say, "validate +the name input for the first element of the categories collection, but don't validate it for the +second one." But, honestly, this is really an edge-case. + +Now, the form validates (and the `URL` is set to null as we didn't specify it). diff --git a/book/zend.form.elements.md b/book/zend.form.elements.md new file mode 100644 index 00000000..c5d57f04 --- /dev/null +++ b/book/zend.form.elements.md @@ -0,0 +1,1488 @@ +# Form Elements + +## Introduction + +A set of specialized elements are provided for accomplishing application-centric tasks. These +include several HTML5 input elements with matching server-side validators, the `Csrf` element (to +prevent Cross Site Request Forgery attacks), and the `Captcha` element (to display and validate +\[CAPTCHAs\](zend.captcha)). + +A `Factory` is provided to facilitate creation of elements, fieldsets, forms, and the related input +filter. See the \[Zend\\Form Quick Start\](zend.form.quick-start.factory) for more information. + +orphan + +## Element Base Class + +`Zend\Form\Element` is a base class for all specialized elements and `Zend\Form\Fieldset`. + +**Basic Usage** + +At the bare minimum, each element or fieldset requires a name. You will also typically provide some +attributes to hint to the view layer how it might render the item. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$username = new Element\Text('username'); +$username + ->setLabel('Username') + ->setAttributes(array( + 'class' => 'username', + 'size' => '30', + )); + +$password = new Element\Password('password'); +$password + ->setLabel('Password') + ->setAttributes(array( + 'size' => '30', + )); + +$form = new Form('my-form'); +$form + ->add($username) + ->add($password); +``` + +**Public Methods** + +## Standard Elements + +orphan + +### Button + +`Zend\Form\Element\Button` represents a button form input. It can be used with the +`Zend\Form\View\Helper\FormButton` view helper. + +`Zend\Form\Element\Button` extends from \[ZendFormElement\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `type` attribute of value `button`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$button = new Element\Button('my-button'); +$button->setLabel('My Button') + ->setValue('foo'); + +$form = new Form('my-form'); +$form->add($button); +``` + +orphan + +### Captcha + +`Zend\Form\Element\Captcha` can be used with forms where authenticated users are not necessary, but +you want to prevent spam submissions. It is paired with one of the `Zend\Form\View\Helper\Captcha\*` +view helpers that matches the type of *CAPTCHA* adapter in use. + +**Basic Usage** + +A *CAPTCHA* adapter must be attached in order for validation to be included in the element's input +filter specification. See the section on \[Zend CAPTCHA Adapters\](zend.captcha.adapters) for more +information on what adapters are available. + +```php +use Zend\Captcha; +use Zend\Form\Element; +use Zend\Form\Form; + +$captcha = new Element\Captcha('captcha'); +$captcha + ->setCaptcha(new Captcha\Dumb()) + ->setLabel('Please verify you are human'); + +$form = new Form('my-form'); +$form->add($captcha); +``` + +Here is with the array notation: + +```php +use Zend\Captcha; +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Captcha', + 'name' => 'captcha', + 'options' => array( + 'label' => 'Please verify you are human', + 'captcha' => new Captcha\Dumb(), + ), +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### Checkbox + +`Zend\Form\Element\Checkbox` is meant to be paired with the `Zend\Form\View\Helper\FormCheckbox` for +HTML inputs with type checkbox. This element adds an `InArray` validator to its input filter +specification in order to validate on the server if the checkbox contains either the checked value +or the unchecked value. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"checkbox"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$checkbox = new Element\Checkbox('checkbox'); +$checkbox->setLabel('A checkbox'); +$checkbox->setUseHiddenElement(true); +$checkbox->setCheckedValue("good"); +$checkbox->setUncheckedValue("bad"); + +$form = new Form('my-form'); +$form->add($checkbox); +``` + +Using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Checkbox', + 'name' => 'checkbox', + 'options' => array( + 'label' => 'A checkbox', + 'use_hidden_element' => true, + 'checked_value' => 'good', + 'unchecked_value' => 'bad' + ) +)); +``` + +When creating a checkbox element, setting an attribute of checked will result in the checkbox always +being checked regardless of any data object which might subsequently be bound to the form. The +correct way to set the default value of a checkbox is to set the value attribute as for any other +element. To have a checkbox checked by default make the value equal to the checked\_value eg: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Checkbox', + 'name' => 'checkbox', + 'options' => array( + 'label' => 'A checkbox', + 'use_hidden_element' => true, + 'checked_value' => 'yes', + 'unchecked_value' => 'no' + ), + 'attributes' => array( + 'value' => 'yes' + ) +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited \[methods of +Zend\\Form\\Element\](zend.form.element.methods) . + +orphan + +### Collection + +Sometimes, you may want to add input (or a set of inputs) multiple times, either because you don't +want to duplicate code, or because you do not know in advance how many elements you will need (in +the case of elements dynamically added to a form using JavaScript, for instance). For more +information about Collection, please refer to the \[Form Collections +tutorial\](zend.form.collections). + +`Zend\Form\Element\Collection` is meant to be paired with the +`Zend\Form\View\Helper\FormCollection`. + +**Basic Usage** + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$colors = new Element\Collection('collection'); +$colors->setLabel('Colors'); +$colors->setCount(2); +$colors->setTargetElement(new Element\Color()); +$colors->setShouldCreateTemplate(true); + +$form = new Form('my-form'); +$form->add($colors); +``` + +Using the array notation: + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Collection', + 'options' => array( + 'label' => 'Colors', + 'count' => 2, + 'should_create_template' => true, + 'target_element' => new Element\Color() + ) +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited \[methods of +Zend\\Form\\Element\](zend.form.element.methods) . + +orphan + +### Csrf + +`Zend\Form\Element\Csrf` pairs with the `Zend\Form\View\Helper\FormHidden` to provide protection +from *CSRF* attacks on forms, ensuring the data is submitted by the user session that generated the +form and not by a rogue script. Protection is achieved by adding a hash element to a form and +verifying it when the form is submitted. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"hidden"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$csrf = new Element\Csrf('csrf'); + +$form = new Form('my-form'); +$form->add($csrf); +``` + +You can change the options of the CSRF validator using the `setCsrfValidatorOptions` function, or by +using the `"csrf_options"` key. Here is an example using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Csrf', + 'name' => 'csrf', + 'options' => array( + 'csrf_options' => array( + 'timeout' => 600 + ) + ) +)); +``` + +> ## Note +If you are using more than one form on a page, and each contains its own CSRF element, you will need +to make sure that each form uniquely names its element; if you do not, it's possible for the value +of one to override the other within the server-side session storage, leading to the inability to +validate one or more of the forms on your page. We suggest prefixing the element name with the +form's name or function: "login\_csrf", "registration\_csrf", etc. + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### File + +`Zend\Form\Element\File` represents a form file input and provides a default input specification +with a type of \[FileInput\](zend.input-filter.file-input) (important for handling validators and +filters correctly). It can be used with the `Zend\Form\View\Helper\FormFile` view helper. + +`Zend\Form\Element\File` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"file"`. It will also set the form's +enctype to `multipart/form-data` during `$form->prepare()`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +// Single file upload +$file = new Element\File('file'); +$file->setLabel('Single file input'); + +// HTML5 multiple file upload +$multiFile = new Element\File('multi-file'); +$multiFile->setLabel('Multi file input') + ->setAttribute('multiple', true); + +$form = new Form('my-file'); +$form->add($file) + ->add($multiFile); +``` + +orphan + +### Hidden + +`Zend\Form\Element\Hidden` represents a hidden form input. It can be used with the +`Zend\Form\View\Helper\FormHidden` view helper. + +`Zend\Form\Element\Hidden` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"hidden"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$hidden = new Element\Hidden('my-hidden'); +$hidden->setValue('foo'); + +$form = new Form('my-form'); +$form->add($hidden); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Hidden', + 'name' => 'my-hidden', + 'attributes' => array( + 'value' => 'foo' + ) +)); +``` + +orphan + +### Image + +`Zend\Form\Element\Image` represents a image button form input. It can be used with the +`Zend\Form\View\Helper\FormImage` view helper. + +`Zend\Form\Element\Image` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"image"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$image = new Element\Image('my-image'); +$image->setAttribute('src', 'http://my.image.url'); // Src attribute is required + +$form = new Form('my-form'); +$form->add($image); +``` + +orphan + +### Month Select + +`Zend\Form\Element\MonthSelect` is meant to be paired with the +`Zend\Form\View\Helper\FormMonthSelect`. This element creates two select elements, where the first +one is populated with months and the second is populated with years. By default, it sets 100 years +in the past for the year element, starting with the current year. + +**Basic Usage** + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$monthYear = new Element\MonthSelect('monthyear'); +$monthYear->setLabel('Select a month and a year'); +$monthYear->setMinYear(1986); + +$form = new Form('dateselect'); +$form->add($monthYear); +``` + +Using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('dateselect'); +$form->add(array( + 'type' => 'Zend\Form\Element\MonthSelect', + 'name' => 'monthyear', + 'options' => array( + 'label' => 'Select a month and a year', + 'min_year' => 1986, + ) +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited \[methods of +Zend\\Form\\Element\](zend.form.element.methods). + +orphan + +### MultiCheckbox + +`Zend\Form\Element\MultiCheckbox` is meant to be paired with the +`Zend\Form\View\Helper\FormMultiCheckbox` for HTML inputs with type checkbox. This element adds an +`InArray` validator to its input filter specification in order to validate on the server if the +checkbox contains values from the multiple checkboxes. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"checkbox"` for every checkboxes. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$multiCheckbox = new Element\MultiCheckbox('multi-checkbox'); +$multiCheckbox->setLabel('What do you like ?'); +$multiCheckbox->setValueOptions(array( + '0' => 'Apple', + '1' => 'Orange', + '2' => 'Lemon' +)); + +$form = new Form('my-form'); +$form->add($multiCheckbox); +``` + +Using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\MultiCheckbox', + 'name' => 'multi-checkbox', + 'options' => array( + 'label' => 'What do you like ?', + 'value_options' => array( + '0' => 'Apple', + '1' => 'Orange', + '2' => 'Lemon', + ), + ) +)); +``` + +**Advanced Usage** + +In order to set attributes or customize the option elements, an array can be used instead of a +string. The following keys are supported: + +- `"label"` - The string displayed for the option. +- `"value"` - The form value associated with the option. +- `"selected"` - Boolean that sets whether the option is marked as selected. +- `"disabled"` - Boolean that sets whether the option will be disabled +- `"attributes"` - Array of html attributes that will be set on this option. Merged with the +attributes set on the element. +- `"label_attributes"` - Array of html attributes that will be set on the label. Merged with the +attributes set on the element's label. + +```php +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\MultiCheckbox', + 'name' => 'multi-checkbox', + 'options' => array( + 'label' => 'What do you like ?', + 'value_options' => array( + array( + 'value' => '0', + 'label' => 'Apple', + 'selected' => false, + 'disabled' => false, + 'attributes' => array( + 'id' => 'apple_option', + 'data-fruit' => 'apple', + ), + 'label_attributes' => array( + 'id' => 'apple_label', + ), + ), + array( + 'value' => '1', + 'label' => 'Orange', + 'selected' => true, + ), + array( + 'value' => '2', + 'label' => 'Lemon', + ), + ), + ), +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited \[methods of +Zend\\Form\\Element\\Checkbox\](zend.form.element.checkbox.methods) . + +orphan + +### Password + +`Zend\Form\Element\Password` represents a password form input. It can be used with the +`Zend\Form\View\Helper\FormPassword` view helper. + +`Zend\Form\Element\Password` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"password"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$password = new Element\Password('my-password'); +$password->setLabel('Enter your password'); + +$form = new Form('my-form'); +$form->add($password); +``` + +orphan + +### Radio + +`Zend\Form\Element\Radio` is meant to be paired with the `Zend\Form\View\Helper\FormRadio` for HTML +inputs with type radio. This element adds an `InArray` validator to its input filter specification +in order to validate on the server if the value is contains within the radio value elements. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"radio"` for every radio. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$radio = new Element\Radio('gender'); +$radio->setLabel('What is your gender ?'); +$radio->setValueOptions(array( + '0' => 'Female', + '1' => 'Male', +)); + +$form = new Form('my-form'); +$form->add($radio); +``` + +Using the array notation: + +```php +use Zend\Form\Form; + + $form = new Form('my-form'); + $form->add(array( + 'type' => 'Zend\Form\Element\Radio', + 'name' => 'gender', + 'options' => array( + 'label' => 'What is your gender ?', + 'value_options' => array( + '0' => 'Female', + '1' => 'Male', + ), + ), + )); +``` + +**Advanced Usage** + +See MultiCheckbox for examples<zend.form.element.multicheckbox.advanced> of how to apply +attributes and options to each radio button. + +**Public Methods** + +All the methods from the inherited \[methods of +Zend\\Form\\Element\\MultiCheckbox\](zend.form.element.multicheckbox.methods) are also available for +this element. + +orphan + +### Select + +`Zend\Form\Element\Select` is meant to be paired with the `Zend\Form\View\Helper\FormSelect` for +HTML inputs with type select. This element adds an `InArray` validator to its input filter +specification in order to validate on the server if the selected value belongs to the values. This +element can be used as a multi-select element by adding the "multiple" HTML attribute to the +element. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"select"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$select = new Element\Select('language'); +$select->setLabel('Which is your mother tongue?'); +$select->setValueOptions(array( + '0' => 'French', + '1' => 'English', + '2' => 'Japanese', + '3' => 'Chinese', +)); + +$form = new Form('language'); +$form->add($select); +``` + +Using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Select', + 'name' => 'language', + 'options' => array( + 'label' => 'Which is your mother tongue?', + 'value_options' => array( + '0' => 'French', + '1' => 'English', + '2' => 'Japanese', + '3' => 'Chinese', + ), + ) +)); +``` + +You can add an empty option (option with no value) using the `"empty_option"` option: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Select', + 'name' => 'language', + 'options' => array( + 'label' => 'Which is your mother tongue?', + 'empty_option' => 'Please choose your language', + 'value_options' => array( + '0' => 'French', + '1' => 'English', + '2' => 'Japanese', + '3' => 'Chinese', + ), + ) +)); +``` + +Option groups are also supported. You just need to add an 'options' key to the value options. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$select = new Element\Select('language'); +$select->setLabel('Which is your mother tongue?'); +$select->setValueOptions(array( + 'european' => array( + 'label' => 'European languages', + 'options' => array( + '0' => 'French', + '1' => 'Italian', + ), + ), + 'asian' => array( + 'label' => 'Asian languages', + 'options' => array( + '2' => 'Japanese', + '3' => 'Chinese', + ), + ), +)); + +$form = new Form('language'); +$form->add($select); +``` + +**Public Methods** + +The following methods are in addition to the inherited \[methods of +Zend\\Form\\Element\](zend.form.element.methods) . + +orphan + +### Submit + +`Zend\Form\Element\Submit` represents a submit button form input. It can be used with the +`Zend\Form\View\Helper\FormSubmit` view helper. + +`Zend\Form\Element\Submit` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"submit"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$submit = new Element\Submit('my-submit'); +$submit->setValue('Submit Form'); + +$form = new Form('my-form'); +$form->add($submit); +``` + +orphan + +### Text + +`Zend\Form\Element\Text` represents a text form input. It can be used with the +`Zend\Form\View\Helper\FormText` view helper. + +`Zend\Form\Element\Text` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"text"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$text = new Element\Text('my-text'); +$text->setLabel('Enter your name'); + +$form = new Form('my-form'); +$form->add($text); +``` + +orphan + +### Textarea + +`Zend\Form\Element\Textarea` represents a textarea form input. It can be used with the +`Zend\Form\View\Helper\FormTextarea` view helper. + +`Zend\Form\Element\Textarea` extends from \[Zend\\Form\\Element\](zend.form.element). + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"textarea"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$textarea = new Element\Textarea('my-textarea'); +$textarea->setLabel('Enter a description'); + +$form = new Form('my-form'); +$form->add($textarea); +``` + +## HTML5 Elements + +orphan + +### Color + +`Zend\Form\Element\Color` is meant to be paired with the `Zend\Form\View\Helper\FormColor` for +[HTML5 inputs with type +color](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#color-state-(type=color)). +This element adds filters and a `Regex` validator to it's input filter specification in order to +validate a [HTML5 valid simple +color](http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-simple-color) +value on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"color"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$color = new Element\Color('color'); +$color->setLabel('Background color'); + +$form = new Form('my-form'); +$form->add($color); +``` + +Here is the same example using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Color', + 'name' => 'color', + 'options' => array( + 'label' => 'Background color' + ) +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### Date + +`Zend\Form\Element\Date` is meant to be paired with the `Zend\Form\View\Helper\FormDate` for [HTML5 +inputs with type +date](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#date-state-(type=date)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 date input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"date"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$date = new Element\Date('appointment-date'); +$date + ->setLabel('Appointment Date') + ->setAttributes(array( + 'min' => '2012-01-01', + 'max' => '2020-01-01', + 'step' => '1', // days; default step interval is 1 day + )) + ->setOptions(array( + 'format' => 'Y-m-d' + )); + +$form = new Form('my-form'); +$form->add($date); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Date', + 'name' => 'appointment-date', + 'options' => array( + 'label' => 'Appointment Date', + 'format' => 'Y-m-d' + ), + 'attributes' => array( + 'min' => '2012-01-01', + 'max' => '2020-01-01', + 'step' => '1', // days; default step interval is 1 day + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of +Zend\\\\Form\\\\Element\\\\DateTime +<zend.form.element.date-time.methods>. + +orphan + +### DateTime + +`Zend\Form\Element\DateTime` is meant to be paired with the `Zend\Form\View\Helper\FormDateTime` for +[HTML5 inputs with type +datetime](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#date-and-time-state-(type=datetime)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 datetime input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"datetime"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$dateTime = new Element\DateTime('appointment-date-time'); +$dateTime + ->setLabel('Appointment Date/Time') + ->setAttributes(array( + 'min' => '2010-01-01T00:00:00Z', + 'max' => '2020-01-01T00:00:00Z', + 'step' => '1', // minutes; default step interval is 1 min + )) + ->setOptions(array( + 'format' => 'Y-m-d\TH:iP' + )); + +$form = new Form('my-form'); +$form->add($dateTime); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\DateTime', + 'name' => 'appointment-date-time', + 'options' => array( + 'label' => 'Appointment Date/Time', + 'format' => 'Y-m-d\TH:iP' + ), + 'attributes' => array( + 'min' => '2010-01-01T00:00:00Z', + 'max' => '2020-01-01T00:00:00Z', + 'step' => '1', // minutes; default step interval is 1 min + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### DateTimeLocal + +`Zend\Form\Element\DateTimeLocal` is meant to be paired with the +`Zend\Form\View\Helper\FormDateTimeLocal` for [HTML5 inputs with type +datetime-local](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#local-date-and-time-state-(type=datetime-local)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 a local datetime input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"datetime-local"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$dateTimeLocal = new Element\DateTimeLocal('appointment-date-time'); +$dateTimeLocal + ->setLabel('Appointment Date') + ->setAttributes(array( + 'min' => '2010-01-01T00:00:00', + 'max' => '2020-01-01T00:00:00', + 'step' => '1', // minutes; default step interval is 1 min + )) + ->setOptions(array( + 'format' => 'Y-m-d\TH:i' + )); + +$form = new Form('my-form'); +$form->add($dateTimeLocal); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\DateTimeLocal', + 'name' => 'appointment-date-time', + 'options' => array( + 'label' => 'Appointment Date', + 'format' => 'Y-m-d\TH:i' + ), + 'attributes' => array( + 'min' => '2010-01-01T00:00:00', + 'max' => '2020-01-01T00:00:00', + 'step' => '1', // minutes; default step interval is 1 min + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of +Zend\\\\Form\\\\Element\\\\DateTime +<zend.form.element.date-time.methods>. + +orphan + +### Email + +`Zend\Form\Element\Email` is meant to be paired with the `Zend\Form\View\Helper\FormEmail` for +[HTML5 inputs with type +email](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#e-mail-state-(type=email)). +This element adds filters and validators to it's input filter specification in order to validate +[HTML5 valid email +address](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address) +on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"email"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$form = new Form('my-form'); + +// Single email address +$email = new Element\Email('email'); +$email->setLabel('Email Address') +$form->add($email); + +// Comma separated list of emails +$emails = new Element\Email('emails'); +$emails + ->setLabel('Email Addresses') + ->setAttribute('multiple', true); +$form->add($emails); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Email', + 'name' => 'email', + 'options' => array( + 'label' => 'Email Address' + ), +)); + +$form->add(array( + 'type' => 'Zend\Form\Element\Email', + 'name' => 'emails', + 'options' => array( + 'label' => 'Email Addresses' + ), + 'attributes' => array( + 'multiple' => true + ) +)); +``` + +> ## Note +Note: the `multiple` attribute should be set prior to calling Zend\\Form::prepare(). Otherwise, the +default input specification for the element may not contain the correct validation rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### Month + +`Zend\Form\Element\Month` is meant to be paired with the `Zend\Form\View\Helper\FormMonth` for +[HTML5 inputs with type +month](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#month-state-(type=month)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 month input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"month"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$month = new Element\Month('month'); +$month + ->setLabel('Month') + ->setAttributes(array( + 'min' => '2012-01', + 'max' => '2020-01', + 'step' => '1', // months; default step interval is 1 month + )); + +$form = new Form('my-form'); +$form->add($month); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Month', + 'name' => 'month', + 'options' => array( + 'label' => 'Month' + ), + 'attributes' => array( + 'min' => '2012-12', + 'max' => '2020-01', + 'step' => '1', // months; default step interval is 1 month + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of +Zend\\\\Form\\\\Element\\\\DateTime +<zend.form.element.date-time.methods>. + +orphan + +### Number + +`Zend\Form\Element\Number` is meant to be paired with the `Zend\Form\View\Helper\FormNumber` for +[HTML5 inputs with type +number](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#number-state-(type=number)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 number input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"number"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$number = new Element\Number('quantity'); +$number + ->setLabel('Quantity') + ->setAttributes(array( + 'min' => '0', + 'max' => '10', + 'step' => '1', // default step interval is 1 + )); + +$form = new Form('my-form'); +$form->add($number); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Number', + 'name' => 'quantity', + 'options' => array( + 'label' => 'Quantity' + ), + 'attributes' => array( + 'min' => '0', + 'max' => '10', + 'step' => '1', // default step interval is 1 + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### Range + +`Zend\Form\Element\Range` is meant to be paired with the `Zend\Form\View\Helper\FormRange` for +[HTML5 inputs with type +range](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#range-state-(type=range)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 range values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"range"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$range = new Element\Range('range'); +$range + ->setLabel('Minimum and Maximum Amount') + ->setAttributes(array( + 'min' => '0', // default minimum is 0 + 'max' => '100', // default maximum is 100 + 'step' => '1', // default interval is 1 + )); + +$form = new Form('my-form'); +$form->add($range); +``` + +Here is with the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Range', + 'name' => 'range', + 'options' => array( + 'label' => 'Minimum and Maximum Amount' + ), + 'attributes' => array( + 'min' => 0, // default minimum is 0 + 'max' => 100, // default maximum is 100 + 'step' => 1 // default interval is 1 + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element\\\\Number +<zend.form.element.number.methods>. + +orphan + +### Time + +`Zend\Form\Element\Time` is meant to be paired with the `Zend\Form\View\Helper\FormTime` for [HTML5 +inputs with type +time](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#time-state-(type=time)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 time input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"time"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$time = new Element\Time('time'); +$time + ->setLabel('Time') + ->setAttributes(array( + 'min' => '00:00:00', + 'max' => '23:59:59', + 'step' => '60', // seconds; default step interval is 60 seconds + )) + ->setOptions(array( + 'format' => 'H:i:s' + )); + +$form = new Form('my-form'); +$form->add($time); +``` + +Here is the same example using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Time', + 'name' => 'time', + 'options'=> array( + 'label' => 'Time', + 'format' => 'H:i:s' + ), + 'attributes' => array( + 'min' => '00:00:00', + 'max' => '23:59:59', + 'step' => '60', // seconds; default step interval is 60 seconds + ) +)); +``` + +> ## Note +The `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +> ## Note +The default date format for the validator is `H:i:s`. A valid time string is however not required to +have a seconds part. In fact some user agent UIs such as Google Chrome and Opera submits a value on +the `H:i` format (i.e. without a second part). You might therefore want to set the date format +accordingly. + +**Public Methods** + +The following methods are in addition to the inherited methods of +Zend\\\\Form\\\\Element\\\\DateTime +<zend.form.element.date-time.methods>. + +orphan + +### Url + +`Zend\Form\Element\Url` is meant to be paired with the `Zend\Form\View\Helper\FormUrl` for [HTML5 +inputs with type +url](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#url-state-(type=url)). +This element adds filters and a `Zend\Validator\Uri` validator to it's input filter specification +for validating HTML5 URL input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"url"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$url = new Element\Url('webpage-url'); +$url->setLabel('Webpage URL'); + +$form = new Form('my-form'); +$form->add($url); +``` + +Here is the same example using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Url', + 'name' => 'webpage-url', + 'options' => array( + 'label' => 'Webpage URL' + ) +)); +``` + +**Public Methods** + +The following methods are in addition to the inherited methods of Zend\\\\Form\\\\Element +<zend.form.element.methods>. + +orphan + +### Week + +`Zend\Form\Element\Week` is meant to be paired with the `Zend\Form\View\Helper\FormWeek` for [HTML5 +inputs with type +week](http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#week-state-(type=week)). +This element adds filters and validators to it's input filter specification in order to validate +HTML5 week input values on the server. + +**Basic Usage** + +This element automatically adds a `"type"` attribute of value `"week"`. + +```php +use Zend\Form\Element; +use Zend\Form\Form; + +$week = new Element\Week('week'); +$week + ->setLabel('Week') + ->setAttributes(array( + 'min' => '2012-W01', + 'max' => '2020-W01', + 'step' => '1', // weeks; default step interval is 1 week + )); + +$form = new Form('my-form'); +$form->add($week); +``` + +Here is the same example using the array notation: + +```php +use Zend\Form\Form; + +$form = new Form('my-form'); +$form->add(array( + 'type' => 'Zend\Form\Element\Week', + 'name' => 'week', + 'options' => array( + 'label' => 'Week' + ), + 'attributes' => array( + 'min' => '2012-W01', + 'max' => '2020-W01', + 'step' => '1', // weeks; default step interval is 1 week + ) +)); +``` + +> ## Note +Note: the `min`, `max`, and `step` attributes should be set prior to calling Zend\\Form::prepare(). +Otherwise, the default input specification for the element may not contain the correct validation +rules. + +**Public Methods** + +The following methods are in addition to the inherited methods of +Zend\\\\Form\\\\Element\\\\DateTime +<zend.form.element.date-time.methods>. diff --git a/book/zend.form.file-upload.md b/book/zend.form.file-upload.md new file mode 100644 index 00000000..10035181 --- /dev/null +++ b/book/zend.form.file-upload.md @@ -0,0 +1,657 @@ +# File Uploading + +Zend Framework provides support for file uploading by using features in `Zend\Form`, +`Zend\InputFilter`, `Zend\Validator`, `Zend\Filter`, and `Zend\ProgressBar`. These reusable +framework components provide a convenient and secure way for handling file uploads in your projects. + +> ## Note +If the reader has experience with file uploading in Zend Framework v1.x, he/she will notice some +major differences. `Zend_File\Transfer` has been deprecated in favor of using the standard ZF2 +`Zend\Form` and `Zend\InputFilter` features. + +> ## Note +The file upload features described here are specifically for forms using the `POST` method. Zend +Framework itself does not currently provide specific support for handling uploads via the `PUT` +method, but it is possible with PHP. See the [PUT Method +Support](http://php.net/manual/en/features.file-upload.put-method.php) in the PHP documentation for +more information. + +## Standard Example + +Handling file uploads is *essentially* the same as how you would use `Zend\Form` for form +processing, but with some slight caveats that will be described below. + +In this example we will: + +- Define a **Form** for backend validation and filtering. +- Create a **view template** with a `
` containing a file input. +- Process the form within a **Controller action**. + +### The Form and InputFilter + +Here we define a `Zend\Form\Element\File` input in a Form class named `UploadForm`. + +```php +// File: UploadForm.php + +use Zend\Form\Element; +use Zend\Form\Form; + +class UploadForm extends Form +{ + public function __construct($name = null, $options = array()) + { + parent::__construct($name, $options); + $this->addElements(); + } + + public function addElements() + { + // File Input + $file = new Element\File('image-file'); + $file->setLabel('Avatar Image Upload') + ->setAttribute('id', 'image-file'); + $this->add($file); + } +} +``` + +The `File` element provides some automatic features that happen behind the scenes: + +- The form's `enctype` will automatically be set to `multipart/form-data` when the form `prepare()` +method is called. +- The file element's default input specification will create the correct `Input` type: +\[Zend\\InputFilter\\FileInput\](zend.input-filter.file-input). +- The `FileInput` will automatically prepend an \[UploadFile +Validator\](zend.validator.file.upload-file), to securely validate that the file is actually an +uploaded file, and to report other types of upload errors to the user. + +### The View Template + +In the view template we render the ``, a file input (with label and errors), and a submit +button. + +```php +// File: upload-form.phtml +prepare(); // The correct enctype is set here ?> +form()->openTag($form); ?> + +
+ get('image-file'); ?> + formLabel($fileElement); ?> + formFile($fileElement); ?> + formElementErrors($fileElement); ?> +
+ + + +form()->closeTag(); ?> +``` + +When rendered, the HTML should look similar to: + +```php + +
+ + +
+ + +
+``` + +### The Controller Action + +For the final step, we will instantiate the `UploadForm` and process any postbacks in a Controller +action. + +The form processing in the controller action will be similar to normal forms, *except* that you +**must** merge the `$_FILES` information in the request with the other post data. + +```php +// File: MyController.php + +public function uploadFormAction() +{ + $form = new UploadForm('upload-form'); + + $request = $this->getRequest(); + if ($request->isPost()) { + // Make certain to merge the files info! + $post = array_merge_recursive( + $request->getPost()->toArray(), + $request->getFiles()->toArray() + ); + + $form->setData($post); + if ($form->isValid()) { + $data = $form->getData(); + // Form is valid, save the form! + return $this->redirect()->toRoute('upload-form/success'); + } + } + + return array('form' => $form); +} +``` + +Upon a successful file upload, `$form->getData()` would return: + +```php +array(1) { + ["image-file"] => array(5) { + ["name"] => string(11) "myimage.png" + ["type"] => string(9) "image/png" + ["tmp_name"] => string(22) "/private/tmp/phpgRXd58" + ["error"] => int(0) + ["size"] => int(14908679) + } +} +``` + +> ## Note +It is suggested that you always use the `Zend\Http\PhpEnvironment\Request` object to retrieve and +merge the `$_FILES` information with the form, instead of using `$_FILES` directly. +This is due to how the file information is mapped in the `$_FILES` array: +```php +// A $_FILES array with single input and multiple files: +array(1) { +["image-file"]=array(2) { +["name"]=array(2) { +[0]=string(9)"file0.txt" +[1]=string(9)"file1.txt" +} +["type"]=array(2) { +[0]=string(10)"text/plain" +[1]=string(10)"text/html" +} +} +} +// How Zend\Http\PhpEnvironment\Request remaps the $_FILES array: +array(1) { +["image-file"]=array(2) { +[0]=array(2) { +["name"]=string(9)"file0.txt" +["type"]=string(10)"text/plain" +}, +[1]=array(2) { +["name"]=string(9)"file1.txt" +["type"]=string(10)"text/html" +} +} +} +``` +\[Zend\\InputFilter\\FileInput\](zend.input-filter.file-input) expects the file data be in this +re-mapped array format. + +## File Post-Redirect-Get Plugin + +When using other standard form inputs (i.e. `text`, `checkbox`, `select`, etc.) along with file +inputs in a Form, you can encounter a situation where some inputs may become invalid and the user +must re-select the file and re-upload. PHP will delete uploaded files from the temporary directory +at the end of the request if it has not been moved away or renamed. Re-uploading a valid file each +time another form input is invalid is inefficient and annoying to users. + +One strategy to get around this is to split the form into multiple forms. One form for the file +upload inputs and another for the other standard inputs. + +When you cannot separate the forms, the \[File Post-Redirect-Get Controller +Plugin\](zend.mvc.controller-plugins.file-postredirectget) can be used to manage the file inputs and +save off valid uploads until the entire form is valid. + +Changing our earlier example to use the `fileprg` plugin will require two changes. + +1. Adding a `RenameUpload` filter to our form's file input, with details on where the valid files +should be stored: + + ```php + // File: UploadForm.php + + use Zend\InputFilter; + use Zend\Form\Element; + use Zend\Form\Form; + + class UploadForm extends Form + { + public function __construct($name = null, $options = array()) + { + parent::__construct($name, $options); + $this->addElements(); + $this->addInputFilter(); + } + + public function addElements() + { + // File Input + $file = new Element\File('image-file'); + $file->setLabel('Avatar Image Upload') + ->setAttribute('id', 'image-file'); + $this->add($file); + } + + public function addInputFilter() + { + $inputFilter = new InputFilter\InputFilter(); + + // File Input + $fileInput = new InputFilter\FileInput('image-file'); + $fileInput->setRequired(true); + $fileInput->getFilterChain()->attachByName( + 'filerenameupload', + array( + 'target' => './data/tmpuploads/avatar.png', + 'randomize' => true, + ) + ); + $inputFilter->add($fileInput); + + $this->setInputFilter($inputFilter); + } + } + ``` + + The `filerenameupload` options above would cause an uploaded file to be renamed and moved to: +`./data/tmpuploads/avatar_4b3403665fea6.png`. + + See the \[RenameUpload filter\](zend.filter.file.rename-upload) documentation for more +information on its supported options. + +2. And, changing the Controller action to use the `fileprg` plugin: + + ```php + // File: MyController.php + + public function uploadFormAction() + { + $form = new UploadForm('upload-form'); + $tempFile = null; + + $prg = $this->fileprg($form); + if ($prg instanceof \Zend\Http\PhpEnvironment\Response) { + return $prg; // Return PRG redirect response + } elseif (is_array($prg)) { + if ($form->isValid()) { + $data = $form->getData(); + // Form is valid, save the form! + return $this->redirect()->toRoute('upload-form/success'); + } else { + // Form not valid, but file uploads might be valid... + // Get the temporary file information to show the user in the view + $fileErrors = $form->get('image-file')->getMessages(); + if (empty($fileErrors)) { + $tempFile = $form->get('image-file')->getValue(); + } + } + } + + return array( + 'form' => $form, + 'tempFile' => $tempFile, + ); + } + ``` + +Behind the scenes, the `FilePRG` plugin will: + +- Run the Form's filters, namely the `RenameUpload` filter, to move the files out of temporary +storage. +- Store the valid POST data in the session across requests. +- Change the `required` flag of any file inputs that had valid uploads to `false`. This is so that +form re-submissions without uploads will not cause validation errors. + +> ## Note +In the case of a partially valid form, it is up to the developer whether to notify the user that +files have been uploaded or not. For example, you may wish to hide the form input and/or display the +file information. These things would be implementation details in the view or in a custom view +helper. Just note that neither the `FilePRG` plugin nor the `formFile` view helper will do any +automatic notifications or view changes when files have been successfully uploaded. + +## HTML5 Multi-File Uploads + +With HTML5 we are able to select multiple files from a single file input using the `multiple` +attribute. Not all [browsers support multiple file uploads](http://caniuse.com/#feat=forms), but the +file input will safely remain a single file upload for those browsers that do not support the +feature. + +To enable multiple file uploads in Zend Framework, just set the file element's `multiple` attribute +to true: + +```php +// File: UploadForm.php + +use Zend\InputFilter; +use Zend\Form\Element; +use Zend\Form\Form; + +class UploadForm extends Form +{ + public function __construct($name = null, $options = array()) + { + parent::__construct($name, $options); + $this->addElements(); + $this->addInputFilter(); + } + + public function addElements() + { + // File Input + $file = new Element\File('image-file'); + $file->setLabel('Avatar Image Upload') + ->setAttribute('id', 'image-file') + ->setAttribute('multiple', true); // That's it + $this->add($file); + } + + public function addInputFilter() + { + $inputFilter = new InputFilter\InputFilter(); + + // File Input + $fileInput = new InputFilter\FileInput('image-file'); + $fileInput->setRequired(true); + + // You only need to define validators and filters + // as if only one file was being uploaded. All files + // will be run through the same validators and filters + // automatically. + $fileInput->getValidatorChain() + ->attachByName('filesize', array('max' => 204800)) + ->attachByName('filemimetype', array('mimeType' => 'image/png,image/x-png')) + ->attachByName('fileimagesize', array('maxWidth' => 100, 'maxHeight' => 100)); + + // All files will be renamed, i.e.: + // ./data/tmpuploads/avatar_4b3403665fea6.png, + // ./data/tmpuploads/avatar_5c45147660fb7.png + $fileInput->getFilterChain()->attachByName( + 'filerenameupload', + array( + 'target' => './data/tmpuploads/avatar.png', + 'randomize' => true, + ) + ); + $inputFilter->add($fileInput); + + $this->setInputFilter($inputFilter); + } +} +``` + +You do not need to do anything special with the validators and filters to support multiple file +uploads. All of the files that are uploaded will have the same validators and filters run against +them automatically (from logic within `FileInput`). You only need to define them as if one file was +being uploaded. + +## Upload Progress + +While pure client-based upload progress meters are starting to become available with [HTML5's +Progress Events](http://www.w3.org/TR/progress-events/), not all browsers have [XMLHttpRequest level +2 support](http://caniuse.com/#feat=xhr2). For upload progress to work in a greater number of +browsers (IE9 and below), you must use a server-side progress solution. + +`Zend\ProgressBar\Upload` provides handlers that can give you the actual state of a file upload in +progress. To use this feature you need to choose one of the \[Upload Progress +Handlers\](zend.progress-bar.upload) (APC, uploadprogress, or Session) and ensure that your server +setup has the appropriate extension or feature enabled. + +> ## Note +For this example we will use PHP **5.4**'s [Session progress +handler](http://php.net/manual/en/session.upload-progress.php) +**PHP 5.4 is required** and you may need to verify these php.ini settings for it to work: +```php +file_uploads = On +post_max_size = 50M +upload_max_filesize = 50M +session.upload_progress.enabled = On +session.upload_progress.freq = "1%" +session.upload_progress.min_freq = "1" +; Also make certain 'upload_tmp_dir' is writable +``` + +When uploading a file with a form POST, you must also include the progress identifier in a hidden +input. The \[File Upload Progress View Helpers\](zend.form.view.helper.file) provide a convenient +way to add the hidden input based on your handler type. + +```php +// File: upload-form.phtml +prepare(); ?> +form()->openTag($form); ?> + formFileSessionProgress(); // Must come before the file input! ?> + +
+ get('image-file'); ?> + formLabel($fileElement); ?> + formFile($fileElement); ?> + formElementErrors($fileElement); ?> +
+ + + +form()->closeTag(); ?> +``` + +When rendered, the HTML should look similar to: + +```php +
+ + +
+ + +
+ + +
+``` + +There are a few different methods for getting progress information to the browser (long vs. short +polling). Here we will use short polling since it is simpler and less taxing on server resources, +though keep in mind it is not as responsive as long polling. + +When our form is submitted via AJAX, the browser will continuously poll the server for upload +progress. + +The following is an example Controller action which provides the progress information: + +```php +// File: MyController.php + +public function uploadProgressAction() +{ + $id = $this->params()->fromQuery('id', null); + $progress = new \Zend\ProgressBar\Upload\SessionProgress(); + return new \Zend\View\Model\JsonModel($progress->getProgress($id)); +} + +// Returns JSON +//{ +// "total" : 204800, +// "current" : 10240, +// "rate" : 1024, +// "message" : "10kB / 200kB", +// "done" : false +//} +``` + +> ## Warning +This is *not* the most efficient way of providing upload progress, since each polling request must +go through the Zend Framework bootstrap process. A better example would be to use a standalone php +file in the public folder that bypasses the MVC bootstrapping and only uses the essential +`Zend\ProgressBar` adapters. + +Back in our view template, we will add the JavaScript to perform the AJAX POST of the form data, and +to start a timeout interval for the progress polling. To keep the example code relatively short, we +are using the [jQuery Form plugin](https://github.com/malsup/form) to do the AJAX form POST. If your +project uses a different JavaScript framework (or none at all), this will hopefully at least +illustrate the necessary high-level logic that would need to be performed. + +```php +// File: upload-form.phtml +// ...after the form... + + +
+
+
+
+

+
+ + + + +``` + +And finally, our Controller action can be modified to return form status and validation messages in +JSON format if we see the 'isAjax' post parameter (which was set in the JavaScript just before +submit): + +```php +// File: MyController.php + +public function uploadFormAction() +{ + $form = new UploadForm('upload-form'); + + $request = $this->getRequest(); + if ($request->isPost()) { + // Make certain to merge the files info! + $post = array_merge_recursive( + $request->getPost()->toArray(), + $request->getFiles()->toArray() + ); + + $form->setData($post); + if ($form->isValid()) { + $data = $form->getData(); + // Form is valid, save the form! + if (!empty($post['isAjax'])) { + return new JsonModel(array( + 'status' => true, + 'redirect' => $this->url()->fromRoute('upload-form/success'), + 'formData' => $data, + )); + } else { + // Fallback for non-JS clients + return $this->redirect()->toRoute('upload-form/success'); + } + } else { + if (!empty($post['isAjax'])) { + // Send back failure information via JSON + return new JsonModel(array( + 'status' => false, + 'formErrors' => $form->getMessages(), + 'formData' => $form->getData(), + )); + } + } + } + + return array('form' => $form); +} +``` + +## Additional Info + +Related documentation: + +- \[Form File Element\](zend.form.element.file) +- \[Form File View Helper\](zend.form.view.helper.form-file) +- \[List of File Validators\](zend.validator.file) +- \[List of File Filters\](zend.filter.file) +- \[File Post-Redirect-Get Controller Plugin\](zend.mvc.controller-plugins.file-postredirectget) +- \[Zend\\InputFilter\\FileInput\](zend.input-filter.file-input) +- \[Upload Progress Handlers\](zend.progress-bar.upload) +- \[Upload Progress View Helpers\](zend.form.view.helper.file) + +External resources and blog posts from the community: + +- [ZF2FileUploadExamples](https://github.com/cgmartin/ZF2FileUploadExamples) : A ZF2 module with +several file upload examples. + diff --git a/book/zend.form.intro.md b/book/zend.form.intro.md new file mode 100644 index 00000000..624d8a1f --- /dev/null +++ b/book/zend.form.intro.md @@ -0,0 +1,32 @@ +# Introduction + +`Zend\Form` is intended primarily as a bridge between your domain models and the View Layer. It +composes a thin layer of objects representing form elements, an \[InputFilter\](zend.input-filter), +and a small number of methods for binding data to and from the form and attached objects. + +The `Zend\Form` component consists of the following objects: + +- `Elements`, which simply consist of a name and attributes. +- `Fieldsets`, which extend from `Elements`, but allow composing other fieldsets and elements. +- `Forms`, which extend from `Fieldsets` (and thus `Elements`). They provide data and object +binding, and compose \[InputFilters\](zend.input-filter.intro). Data binding is done via +\[Zend\\Stdlib\\Hydrator\](zend.stdlib.hydrator). + +To facilitate usage with the view layer, the `Zend\Form` component also aggregates a number of +form-specific view helpers. These accept elements, fieldsets, and/or forms, and use the attributes +they compose to render markup. + +A small number of specialized elements are provided for accomplishing application-centric tasks. +These include the `Csrf` element, used to prevent Cross Site Request Forgery attacks, and the +`Captcha` element, used to display and validate \[CAPTCHAs\](zend.captcha). + +A `Factory` is provided to facilitate creation of elements, fieldsets, forms, and the related input +filter. The default `Form` implementation is backed by a factory to facilitate extension and ease +the process of form creation. + +The code related to forms can often spread between a variety of concerns: a form definition, an +input filter definition, a domain model class, and one or more hydrator implementations. As such, +finding the various bits of code and how they relate can become tedious. To simplify the situation, +you can also annotate your domain model class, detailing the various input filter definitions, +attributes, and hydrators that should all be used together. `Zend\Form\Annotation\AnnotationBuilder` +can then be used to build the various objects you need. diff --git a/book/zend.form.quick-start.md b/book/zend.form.quick-start.md new file mode 100644 index 00000000..506ec1c1 --- /dev/null +++ b/book/zend.form.quick-start.md @@ -0,0 +1,975 @@ +# Quick Start + +Forms are relatively easy to create. At the bare minimum, each element or fieldset requires a name; +typically, you'll also provide some attributes to hint to the view layer how it might render the +item. The form itself will also typically compose an `InputFilter`-- which you can also conveniently +create directly in the form via a factory. Individual elements can hint as to what defaults to use +when generating a related input for the input filter. + +Form validation is as easy as providing an array of data to the `setData()` method. If you want to +simplify your work even more, you can bind an object to the form; on successful validation, it will +be populated from the validated values. + +## Programmatic Form Creation + +If nothing else, you can simply start creating elements, fieldsets, and forms and wiring them +together. + +```php +use Zend\Captcha; +use Zend\Form\Element; +use Zend\Form\Fieldset; +use Zend\Form\Form; +use Zend\InputFilter\Input; +use Zend\InputFilter\InputFilter; + +$name = new Element('name'); +$name->setLabel('Your name'); +$name->setAttributes(array( + 'type' => 'text' +)); + +$email = new Element\Email('email'); +$email->setLabel('Your email address'); + +$subject = new Element('subject'); +$subject->setLabel('Subject'); +$subject->setAttributes(array( + 'type' => 'text' +)); + +$message = new Element\Textarea('message'); +$message->setLabel('Message'); + +$captcha = new Element\Captcha('captcha'); +$captcha->setCaptcha(new Captcha\Dumb()); +$captcha->setLabel('Please verify you are human'); + +$csrf = new Element\Csrf('security'); + +$send = new Element('send'); +$send->setValue('Submit'); +$send->setAttributes(array( + 'type' => 'submit' +)); + + +$form = new Form('contact'); +$form->add($name); +$form->add($email); +$form->add($subject); +$form->add($message); +$form->add($captcha); +$form->add($csrf); +$form->add($send); + +$nameInput = new Input('name'); +// configure input... and all others +$inputFilter = new InputFilter(); +// attach all inputs + +$form->setInputFilter($inputFilter); +``` + +As a demonstration of fieldsets, let's alter the above slightly. We'll create two fieldsets, one for +the sender information, and another for the message details. + +```php +$sender = new Fieldset('sender'); +$sender->add($name); +$sender->add($email); + +$details = new Fieldset('details'); +$details->add($subject); +$details->add($message); + +$form = new Form('contact'); +$form->add($sender); +$form->add($details); +$form->add($captcha); +$form->add($csrf); +$form->add($send); +``` + +Regardless of approach, as you can see, this can be tedious. + +## Creation via Factory + +You can create the entire form, and input filter, using the `Factory`. This is particularly nice if +you want to store your forms as pure configuration; you can simply pass the configuration to the +factory and be done. + +```php +use Zend\Form\Factory; + +$factory = new Factory(); +$form = $factory->createForm(array( + 'hydrator' => 'Zend\Stdlib\Hydrator\ArraySerializable', + 'elements' => array( + array( + 'spec' => array( + 'name' => 'name', + 'options' => array( + 'label' => 'Your name', + ), + 'type' => 'Text', + ) + ), + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Email', + 'name' => 'email', + 'options' => array( + 'label' => 'Your email address', + ) + ), + ), + array( + 'spec' => array( + 'name' => 'subject', + 'options' => array( + 'label' => 'Subject', + ), + 'type' => 'Text', + ), + ), + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Textarea', + 'name' => 'message', + 'options' => array( + 'label' => 'Message', + ) + ), + ), + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Captcha', + 'name' => 'captcha', + 'options' => array( + 'label' => 'Please verify you are human.', + 'captcha' => array( + 'class' => 'Dumb', + ), + ), + ), + ), + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Csrf', + 'name' => 'security', + ), + ), + array( + 'spec' => array( + 'name' => 'send', + 'type' => 'Submit', + 'attributes' => array( + 'value' => 'Submit', + ), + ), + ), + ), + /* If we had fieldsets, they'd go here; fieldsets contain + * "elements" and "fieldsets" keys, and potentially a "type" + * key indicating the specific FieldsetInterface + * implementation to use. + 'fieldsets' => array( + ), + */ + + // Configuration to pass on to + // Zend\InputFilter\Factory::createInputFilter() + 'input_filter' => array( + /* ... */ + ), +)); +``` + +If we wanted to use fieldsets, as we demonstrated in the previous example, we could do the +following: + +```php +use Zend\Form\Factory; + +$factory = new Factory(); +$form = $factory->createForm(array( + 'hydrator' => 'Zend\Stdlib\Hydrator\ArraySerializable', + 'fieldsets' => array( + array( + 'spec' => array( + 'name' => 'sender', + 'elements' => array( + array( + 'spec' => array( + 'name' => 'name', + 'options' => array( + 'label' => 'Your name', + ), + 'type' => 'Text' + ), + ), + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Email', + 'name' => 'email', + 'options' => array( + 'label' => 'Your email address', + ), + ), + ), + ), + ), + ), + array( + 'spec' => array( + 'name' => 'details', + 'elements' => array( + array( + 'spec' => array( + 'name' => 'subject', + 'options' => array( + 'label' => 'Subject', + ), + 'type' => 'Text', + ), + ), + array( + 'spec' => array( + 'name' => 'message', + 'type' => 'Zend\Form\Element\Textarea', + 'options' => array( + 'label' => 'Message', + ), + ), + ), + ), + ), + ), + ), + 'elements' => array( + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Captcha', + 'name' => 'captcha', + 'options' => array( + 'label' => 'Please verify you are human. ', + 'captcha' => array( + 'class' => 'Dumb', + ), + ), + ), + ), + array( + 'spec' => array( + 'type' => 'Zend\Form\Element\Csrf', + 'name' => 'security', + ), + ), + array( + 'spec' => array( + 'name' => 'send', + 'type' => 'Submit', + 'attributes' => array( + 'value' => 'Submit', + ), + ), + ), + ), + // Configuration to pass on to + // Zend\InputFilter\Factory::createInputFilter() + 'input_filter' => array( + /* ... */ + ), +)); +``` + +Note that the chief difference is nesting; otherwise, the information is basically the same. + +The chief benefits to using the `Factory` are allowing you to store definitions in configuration, +and usage of significant whitespace. + +## Factory-backed Form Extension + +The default `Form` implementation is backed by the `Factory`. This allows you to extend it, and +define your form internally. This has the benefit of allowing a mixture of programmatic and +factory-backed creation, as well as defining a form for re-use in your application. + +```php +namespace Contact; + +use Zend\Captcha\AdapterInterface as CaptchaAdapter; +use Zend\Form\Element; +use Zend\Form\Form; + +class ContactForm extends Form +{ + protected $captcha; + + public function __construct(CaptchaAdapter $captcha) + { + + parent::__construct(); + + $this->captcha = $captcha; + + // add() can take either an Element/Fieldset instance, + // or a specification, from which the appropriate object + // will be built. + + $this->add(array( + 'name' => 'name', + 'options' => array( + 'label' => 'Your name', + ), + 'type' => 'Text', + )); + $this->add(array( + 'type' => 'Zend\Form\Element\Email', + 'name' => 'email', + 'options' => array( + 'label' => 'Your email address', + ), + )); + $this->add(array( + 'name' => 'subject', + 'options' => array( + 'label' => 'Subject', + ), + 'type' => 'Text', + )); + $this->add(array( + 'type' => 'Zend\Form\Element\Textarea', + 'name' => 'message', + 'options' => array( + 'label' => 'Message', + ), + )); + $this->add(array( + 'type' => 'Zend\Form\Element\Captcha', + 'name' => 'captcha', + 'options' => array( + 'label' => 'Please verify you are human.', + 'captcha' => $this->captcha, + ), + )); + $this->add(new Element\Csrf('security')); + $this->add(array( + 'name' => 'send', + 'type' => 'Submit', + 'attributes' => array( + 'value' => 'Submit', + ), + )); + + // We could also define the input filter here, or + // lazy-create it in the getInputFilter() method. + } +} +``` + +You'll note that this example, the elements are added in the constructor. This is done to allow +altering and/or configuring either the form or input filter factory instances, which could then have +bearing on how elements, inputs, etc. are created. In this case, it also allows injection of the +CAPTCHA adapter, allowing us to configure it elsewhere in our application and inject it into the +form. + +## Validating Forms + +Validating forms requires three steps. First, the form must have an input filter attached. Second, +you must inject the data to validate into the form. Third, you validate the form. If invalid, you +can retrieve the error messages, if any. + +```php +// assuming $captcha is an instance of some Zend\Captcha\AdapterInterface +$form = new Contact\ContactForm($captcha); + +// If the form doesn't define an input filter by default, inject one. +$form->setInputFilter(new Contact\ContactFilter()); + +// Get the data. In an MVC application, you might try: +$data = $request->getPost(); // for POST data +$data = $request->getQuery(); // for GET (or query string) data + +$form->setData($data); + +// Validate the form +if ($form->isValid()) { + $validatedData = $form->getData(); +} else { + $messages = $form->getMessages(); +} +``` + +You can get the raw data if you want, by accessing the composed input filter. + +```php +$filter = $form->getInputFilter(); + +$rawValues = $filter->getRawValues(); +$nameRawValue = $filter->getRawValue('name'); +``` + +## Hinting to the Input Filter + +Often, you'll create elements that you expect to behave in the same way on each usage, and for which +you'll want specific filters or validation as well. Since the input filter is a separate object, how +can you achieve these latter points? + +Because the default form implementation composes a factory, and the default factory composes an +input filter factory, you can have your elements and/or fieldsets hint to the input filter. If no +input or input filter is provided in the input filter for that element, these hints will be +retrieved and used to create them. + +To do so, one of the following must occur. For elements, they must implement +`Zend\InputFilter\InputProviderInterface`, which defines a `getInputSpecification()` method; for +fieldsets, they must implement `Zend\InputFilter\InputFilterProviderInterface`, which defines a +`getInputFilterSpecification()` method. + +In the case of an element, the `getInputSpecification()` method should return data to be used by the +input filter factory to create an input. Every HTML5 (email, url, color…) elements have a built-in +element that use this logic. For instance, here is how the `Zend\Form\Element\Color` element is +defined: + +```php +namespace Zend\Form\Element; + +use Zend\Form\Element; +use Zend\InputFilter\InputProviderInterface; +use Zend\Validator\Regex as RegexValidator; +use Zend\Validator\ValidatorInterface; + +class Color extends Element implements InputProviderInterface +{ + /** + * Seed attributes + * + * @var array + */ + protected $attributes = array( + 'type' => 'color', + ); + + /** + * @var ValidatorInterface + */ + protected $validator; + + /** + * Get validator + * + * @return ValidatorInterface + */ + protected function getValidator() + { + if (null === $this->validator) { + $this->validator = new RegexValidator('/^#[0-9a-fA-F]{6}$/'); + } + return $this->validator; + } + + /** + * Provide default input rules for this element + * + * Attaches an email validator. + * + * @return array + */ + public function getInputSpecification() + { + return array( + 'name' => $this->getName(), + 'required' => true, + 'filters' => array( + array('name' => 'Zend\Filter\StringTrim'), + array('name' => 'Zend\Filter\StringToLower'), + ), + 'validators' => array( + $this->getValidator(), + ), + ); + } +} +``` + +The above would hint to the input filter to create and attach an input named after the element, +marking it as required, and giving it a `StringTrim` and `StringToLower` filters and a `Regex` +validator. Note that you can either rely on the input filter to create filters and validators, or +directly instantiate them. + +For fieldsets, you do very similarly; the difference is that `getInputFilterSpecification()` must +return configuration for an input filter. + +```php +namespace Contact\Form; + +use Zend\Form\Fieldset; +use Zend\InputFilter\InputFilterProviderInterface; +use Zend\Validator; + +class SenderFieldset extends Fieldset implements InputFilterProviderInterface +{ + public function getInputFilterSpecification() + { + return array( + 'name' => array( + 'required' => true, + 'filters' => array( + array('name' => 'Zend\Filter\StringTrim'), + ), + 'validators' => array( + array( + 'name' => 'Zend\Validator\StringLength', + 'options' => array( + 'min' => 3, + 'max' => 256 + ), + ), + ), + ), + 'email' => array( + 'required' => true, + 'filters' => array( + array('name' => 'Zend\Filter\StringTrim'), + ), + 'validators' => array( + new Validator\EmailAddress(), + ), + ), + ); + } +} +``` + +Specifications are a great way to make forms, fieldsets, and elements re-usable trivially in your +applications. In fact, the `Captcha` and `Csrf` elements define specifications in order to ensure +they can work without additional user configuration! + +> ## Note +If you set custom input filter specification either in `getInputSpecification()` or in +`getInputFilterSpecification()`, the `Zend\InputFilter\InputInterface` set for that specific field +is reset to the default `Zend\InputFilter\Input`. +Some form elements may need a particular input filter, like `Zend\Form\Element\File`: in this case +it's mandatory to specify the `type` key in your custom specification to match the original one (in +ex. for the file element it's `Zend\InputFilter\FileInput`). + +## Binding an object + +As noted in the intro, forms in Zend Framework bridge the domain model and the view layer. Let's see +that in action. + +When you `bind()` an object to the form, the following happens: + +- The composed `Hydrator` calls `extract()` on the object, and uses the values returned, if any, to +populate the `value` attributes of all elements. If a form contains a fieldset that itself contains +another fieldset, the form will recursively extract the values. +- When `isValid()` is called, if `setData()` has not been previously set, the form uses the composed +`Hydrator` to extract values from the object, and uses those during validation. +- If `isValid()` is successful (and the `bindOnValidate` flag is enabled, which is true by default), +then the `Hydrator` will be passed the validated values to use to hydrate the bound object. (If you +do not want this behavior, call `setBindOnValidate(FormInterface::BIND_MANUAL)`). +- If the object implements `Zend\InputFilter\InputFilterAwareInterface`, the input filter it +composes will be used instead of the one composed on the form. + +This is easier to understand in practice. + +```php +$contact = new ArrayObject; +$contact['subject'] = '[Contact Form] '; +$contact['message'] = 'Type your message here'; + +$form = new Contact\ContactForm; + +$form->bind($contact); // form now has default values for + // 'subject' and 'message' + +$data = array( + 'name' => 'John Doe', + 'email' => 'j.doe@example.tld', + 'subject' => '[Contact Form] \'sup?', +); +$form->setData($data); + +if ($form->isValid()) { + // $contact now looks like: + // array( + // 'name' => 'John Doe', + // 'email' => 'j.doe@example.tld', + // 'subject' => '[Contact Form] \'sup?', + // 'message' => 'Type your message here', + // ) + // only as an ArrayObject +} +``` + +When an object is bound to the form, calling `getData()` will return that object by default. If you +want to return an associative array instead, you can pass the `FormInterface::VALUES_AS_ARRAY` flag +to the method. + +```php +use Zend\Form\FormInterface; +$data = $form->getData(FormInterface::VALUES_AS_ARRAY); +``` + +Zend Framework ships several standard \[hydrators\](zend.stdlib.hydrator), and implementation is as +simple as implementing `Zend\Stdlib\Hydrator\HydratorInterface`, which looks like this: + +```php +namespace Zend\Stdlib\Hydrator; + +interface HydratorInterface +{ + /** @return array */ + public function extract($object); + public function hydrate(array $data, $object); +} +``` + +## Rendering + +As noted previously, forms are meant to bridge the domain model and view layer. We've discussed the +domain model binding, but what about the view? + +The form component ships a set of form-specific view helpers. These accept the various form objects, +and introspect them in order to generate markup. Typically, they will inspect the attributes, but in +special cases, they may look at other properties and composed objects. + +When preparing to render, you will likely want to call `prepare()`. This method ensures that certain +injections are done, and will likely in the future munge names to allow for +`scoped[array][notation]`. + +The simplest view helpers available are `Form`, `FormElement`, `FormLabel`, and `FormElementErrors`. +Let's use them to display the contact form. + +```php +form; +$form->prepare(); + +// Assuming the "contact/process" route exists... +$form->setAttribute('action', $this->url('contact/process')); + +// Set the method attribute for the form +$form->setAttribute('method', 'post'); + +// Get the form label plugin +$formLabel = $this->plugin('formLabel'); + +// Render the opening tag +echo $this->form()->openTag($form); +?> +
+get('name'); + echo $formLabel->openTag() . $name->getOption('label'); + echo $this->formInput($name); + echo $this->formElementErrors($name); + echo $formLabel->closeTag(); +?>
+ +
+get('subject'); + echo $formLabel->openTag() . $subject->getOption('label'); + echo $this->formInput($subject); + echo $this->formElementErrors($subject); + echo $formLabel->closeTag(); +?>
+ +
+get('message'); + echo $formLabel->openTag() . $message->getOption('label'); + echo $this->formTextarea($message); + echo $this->formElementErrors($message); + echo $formLabel->closeTag(); +?>
+ +
+get('captcha'); + echo $formLabel->openTag() . $captcha->getOption('label'); + echo $this->formCaptcha($captcha); + echo $this->formElementErrors($captcha); + echo $formLabel->closeTag(); +?>
+ +formElement($form->get('security')) ?> +formElement($form->get('send')) ?> + +form()->closeTag() ?> +``` + +There are a few things to note about this. First, to prevent confusion in IDEs and editors when +syntax highlighting, we use helpers to both open and close the form and label tags. Second, there's +a lot of repetition happening here; we could easily create a partial view script or a composite +helper to reduce boilerplate. Third, note that not all elements are created equal -- the CSRF and +submit elements don't need labels or error messages necessarily. Finally, note that the +`FormElement` helper tries to do the right thing -- it delegates actual markup generation to other +view helpers; however, it can only guess what specific form helper to delegate to based on the list +it has. If you introduce new form view helpers, you'll need to extend the `FormElement` helper, or +create your own. + +However, your view files can quickly become long and repetitive to write. While we do not currently +provide a single-line form view helper (as this reduces the form customization), the simplest and +most recommended way to render your form is by using the `FormRow` view helper. This view helper +automatically renders a label (if present), the element itself using the `FormElement` helper, as +well as any errors that could arise. Here is the previous form, rewritten to take advantage of this +helper : + +```php +form; +$form->prepare(); + +// Assuming the "contact/process" route exists... +$form->setAttribute('action', $this->url('contact/process')); + +// Set the method attribute for the form +$form->setAttribute('method', 'post'); + +// Render the opening tag +echo $this->form()->openTag($form); +?> +
+get('name'); + echo $this->formRow($name); +?>
+ +
+get('subject'); + echo $this->formRow($subject); +?>
+ +
+get('message'); + echo $this->formRow($message); +?>
+ +
+get('captcha'); + echo $this->formRow($captcha); +?>
+ +formElement($form->get('security')) ?> +formElement($form->get('send')) ?> + +form()->closeTag() ?> +``` + +Note that `FormRow` helper automatically prepends the label. If you want it to be rendered after the +element itself, you can pass an optional parameter to the `FormRow` view helper : + +```php +
+get('name'); + echo $this->formRow($name, 'append'); +?>
+``` + +### Taking advantage of HTML5 input attributes + +HTML5 brings a lot of exciting features, one of them being a simplified client form validations. +Adding HTML5 attributes is simple as you just need to add specify the attributes. However, please +note that adding those attributes does not automatically add Zend validators to the form's input +filter. You still need to manually add them. + +```php +$form->add(array( + 'name' => 'phoneNumber', + 'options' => array( + 'label' => 'Your phone number' + ), + 'attributes' => array( + 'type' => 'tel' + 'required' => 'required', + 'pattern' => '^0[1-68]([-. ]?[0-9]{2}){4}$' + ) +)); +``` + +View helpers will automatically render those attributes, and hence allowing modern browsers to +perform automatic validation. + +> ## Note +Although client validation is nice from a user experience point of view, it has to be used in +addition with server validation, as client validation can be easily fooled. + +## Validation Groups + +Sometimes you want to validate only a subset of form elements. As an example, let's say we're +re-using our contact form over a web service; in this case, the `Csrf`, `Captcha`, and submit button +elements are not of interest, and shouldn't be validated. + +`Zend\Form` provides a proxy method to the underlying `InputFilter`'s `setValidationGroup()` method, +allowing us to perform this operation. + +```php +$form->setValidationGroup('name', 'email', 'subject', 'message'); +$form->setData($data); +if ($form->isValid()) { + // Contains only the "name", "email", "subject", and "message" values + $data = $form->getData(); +} +``` + +If you later want to reset the form to validate all, simply pass the `FormInterface::VALIDATE_ALL` +flag to the `setValidationGroup()` method. + +```php +use Zend\Form\FormInterface; +$form->setValidationGroup(FormInterface::VALIDATE_ALL); +``` + +When your form contains nested fieldsets, you can use an array notation to validate only a subset of +the fieldsets : + +```php +$form->setValidationGroup(array( + 'profile' => array( + 'firstname', + 'lastname' + ) +)); +$form->setData($data); +if ($form->isValid()) { + // Contains only the "firstname" and "lastname" values from the + // "profile" fieldset + $data = $form->getData(); +} +``` + +## Using Annotations + +Creating a complete forms solution can often be tedious: you'll create some domain model object, an +input filter for validating it, a form object for providing a representation for it, and potentially +a hydrator for mapping the form elements and fieldsets to the domain model. Wouldn't it be nice to +have a central place to define all of these? + +Annotations allow us to solve this problem. You can define the following behaviors with the shipped +annotations in `Zend\Form`: + +- *AllowEmpty*: mark an input as allowing an empty value. This annotation does not require a value. +- *Attributes*: specify the form, fieldset, or element attributes. This annotation requires an +associative array of values, in a JSON object format: +`@Attributes({"class":"zend_form","type":"text"})`. +- *ComposedObject*: specify another object with annotations to parse. Typically, this is used if a +property references another object, which will then be added to your form as an additional fieldset. +Expects a string value indicating the class for the object being composed +`@ComposedObject("Namespace\Model\ComposedObject")` or an array to compose a collection: +`@ComposedObject({ "target_object":"Namespace\Model\ComposedCollection", "is_collection":"true", +"options":{"count":2}})` `target_object` is the element to compose, `is_collection` flags this as a +collection and `options` can take an array of options to pass into the collection. +- *ErrorMessage*: specify the error message to return for an element in the case of a failed +validation. Expects a string value. +- *Exclude*: mark a property to exclude from the form or fieldset. This annotation does not require +a value. +- *Filter*: provide a specification for a filter to use on a given element. Expects an associative +array of values, with a "name" key pointing to a string filter name, and an "options" key pointing +to an associative array of filter options for the constructor: `@Filter({"name": "Boolean", +"options": {"casting":true}})`. This annotation may be specified multiple times. +- *Flags*: flags to pass to the fieldset or form composing an element or fieldset; these are usually +used to specify the name or priority. The annotation expects an associative array: +`@Flags({"priority": 100})`. +- *Hydrator*: specify the hydrator class to use for this given form or fieldset. A string value is +expected. +- *InputFilter*: specify the input filter class to use for this given form or fieldset. A string +value is expected. +- *Input*: specify the input class to use for this given element. A string value is expected. +- *Instance*: specify an object class instance to bind to the form or fieldset. +- *Name*: specify the name of the current element, fieldset, or form. A string value is expected. +- *Object*: specify an object class instance to bind to the form or fieldset. (Note: this is +deprecated in 2.4.0; use *Instance* instead.) +- *Options*: options to pass to the fieldset or form that are used to inform behavior -- things that +are not attributes; e.g. labels, CAPTCHA adapters, etc. The annotation expects an associative array: +`@Options({"label": "Username:"})`. +- *Required*: indicate whether an element is required. A boolean value is expected. By default, all +elements are required, so this annotation is mainly present to allow disabling a requirement. +- *Type*: indicate the class to use for the current element, fieldset, or form. A string value is +expected. +- *Validator*: provide a specification for a validator to use on a given element. Expects an +associative array of values, with a "name" key pointing to a string validator name, and an "options" +key pointing to an associative array of validator options for the constructor: `@Validator({"name": +"StringLength", "options": {"min":3, "max": 25}})`. This annotation may be specified multiple times. + +To use annotations, you simply include them in your class and/or property docblocks. Annotation +names will be resolved according to the import statements in your class; as such, you can make them +as long or as short as you want depending on what you import. + +> ## Note +Form annotations require `Doctrine\Common`, which contains an annotation parsering engine. The +simplest way to install `Doctrine\Common` is if you are using `Composer`; simply update your +`composer.json` and add the following line to the `require` section: +```php +"doctrine/common": "=2.1", +``` +Then run `php composer.phar update` to install the dependency. +If you're not using `Composer`, visit [the Doctrine project +website](http://www.doctrine-project.org/projects/common.html) for more details on installation. + +Here's a simple example. + +```php +use Zend\Form\Annotation; + +/** + * @Annotation\Name("user") + * @Annotation\Hydrator("Zend\Stdlib\Hydrator\ObjectProperty") + */ +class User +{ + /** + * @Annotation\Exclude() + */ + public $id; + + /** + * @Annotation\Filter({"name":"StringTrim"}) + * @Annotation\Validator({"name":"StringLength", "options":{"min":1, "max":25}}) + * @Annotation\Validator({"name":"Regex", +"options":{"pattern":"/^[a-zA-Z][a-zA-Z0-9_-]{0,24}$/"}}) + * @Annotation\Attributes({"type":"text"}) + * @Annotation\Options({"label":"Username:"}) + */ + public $username; + + /** + * @Annotation\Type("Zend\Form\Element\Email") + * @Annotation\Options({"label":"Your email address:"}) + */ + public $email; +} +``` + +The above will hint to the annotation build to create a form with name "user", which uses the +hydrator `Zend\Stdlib\Hydrator\ObjectProperty`. That form will have two elements, "username" and +"email". The "username" element will have an associated input that has a `StringTrim` filter, and +two validators: a `StringLength` validator indicating the username is between 1 and 25 characters, +and a `Regex` validator asserting it follows a specific accepted pattern. The form element itself +will have an attribute "type" with value "text" (a text element), and a label "Username:". The +"email" element will be of type `Zend\Form\Element\Email`, and have the label "Your email address:". + +To use the above, we need `Zend\Form\Annotation\AnnotationBuilder`: + +```php +use Zend\Form\Annotation\AnnotationBuilder; + +$builder = new AnnotationBuilder(); +$form = $builder->createForm('User'); +``` + +At this point, you have a form with the appropriate hydrator attached, an input filter with the +appropriate inputs, and all elements. + +> ## Note +#### You're not done +In all likelihood, you'll need to add some more elements to the form you construct. For example, +you'll want a submit button, and likely a CSRF-protection element. We recommend creating a fieldset +with common elements such as these that you can then attach to the form you build via annotations. diff --git a/book/zend.form.view.helpers.md b/book/zend.form.view.helpers.md new file mode 100644 index 00000000..0b5caca9 --- /dev/null +++ b/book/zend.form.view.helpers.md @@ -0,0 +1,1311 @@ +# Form View Helpers + +## Introduction + +Zend Framework comes with an initial set of helper classes related to Forms: e.g., rendering a text +input, selection box, or form labels. You can use helper, or plugin, classes to perform these +behaviors for you. + +See the section on \[view helpers\](zend.view.helpers) for more information. + +## Standard Helpers + +orphan + +### Form + +The `Form` view helper is used to render a `
` HTML element and its attributes. + +It iterates through all its elements and relies on the `FormCollection` and `FormRow` view helpers +to render them appropriately. + +You can also use \[Zend\\Form\\View\\Helper\\FormRow\](zend.form.view.helper.form-row) in +conjunction with `Form::openTag()` and `Form::closeTag()` to have a more fine grained control over +the output. + +Basic usage: + +```php +/** + * inside view template + * + * @var \Zend\View\Renderer\PhpRenderer $this + * @var \Zend\Form\Form $form + */ + +echo $this->form($form); +// i.e. +// +// +//
+``` + +The following public methods are in addition to those inherited from +\[Zend\\Form\\View\\Helper\\AbstractHelper\](zend.form.view.helper.abstract-helper.methods). + +orphan + +### FormButton + +The `FormButton` view helper is used to render a ` + +/** + * Example #2: Render button in 3 steps + */ +// Render the opening tag +echo $this->formButton()->openTag($element); +// + +/** + * Example #3: Override the element label + */ +echo $this->formButton()->render($element, 'My Content'); +// +``` + +The following public methods are in addition to those inherited from +\[Zend\\Form\\View\\Helper\\FormInput\](zend.form.view.helper.form-input.methods). + +orphan + +### FormCaptcha + +TODO + +Basic usage: + +```php +use Zend\Captcha; +use Zend\Form\Element; + +$captcha = new Element\Captcha('captcha'); +$captcha + ->setCaptcha(new Captcha\Dumb()) + ->setLabel('Please verify you are human'); + +// Within your view... + +echo $this->formCaptcha($captcha); + +// TODO +``` + +orphan + +### FormCheckbox + +The `FormCheckbox` view helper can be used to render a `` HTML form input. It +is meant to work with the \[Zend\\Form\\Element\\Checkbox\](zend.form.element.checkbox) element, +which provides a default input specification for validating the checkbox values. + +`FormCheckbox` extends from +\[Zend\\Form\\View\\Helper\\FormInput\](zend.form.view.helper.form-input.methods). + +Basic usage: + +```php +use Zend\Form\Element; + +$element = new Element\Checkbox('my-checkbox'); + +// Within your view... + +/** + * Example #1: Default options + */ +echo $this->formCheckbox($element); +// +// + +/** + * Example #2: Disable hidden element + */ +$element->setUseHiddenElement(false); +echo $this->formCheckbox($element); +// + +/** + * Example #3: Change checked/unchecked values + */ +$element->setUseHiddenElement(true) + ->setUncheckedValue('no') + ->setCheckedValue('yes'); +echo $this->formCheckbox($element); +// +// +``` + +orphan + +### FormCollection + +TODO + +Basic usage: + +TODO + +orphan + +### FormElement + +The `FormElement` view helper proxies the rendering to specific form view helpers depending on the +type of the `Zend\Form\Element` that is passed in. For instance, if the passed in element had a type +of "text", the `FormElement` helper will retrieve and use the `FormText` helper to render the +element. + +Basic usage: + +```php +use Zend\Form\Form; +use Zend\Form\Element; + +// Within your view... + +/** + * Example #1: Render different types of form elements + */ +$textElement = new Element\Text('my-text'); +$checkboxElement = new Element\Checkbox('my-checkbox'); + +echo $this->formElement($textElement); +// + +echo $this->formElement($checkboxElement); +// +// + +/** + * Example #2: Loop through form elements and render them + */ +$form = new Form(); +// ...add elements and input filter to form... +$form->prepare(); + +// Render the opening tag +echo $this->form()->openTag($form); + +// ...loop through and render the form elements... +foreach ($form as $element) { + echo $this->formElement($element); // <-- Magic! + echo $this->formElementErrors($element); +} + +// Render the closing tag +echo $this->form()->closeTag(); +``` + +orphan + +### FormElementErrors + +The `FormElementErrors` view helper is used to render the validation error messages of an element. + +Basic usage: + +```php +use Zend\Form\Form; +use Zend\Form\Element; +use Zend\InputFilter\InputFilter; +use Zend\InputFilter\Input; + +// Create a form +$form = new Form(); +$element = new Element\Text('my-text'); +$form->add($element); + +// Create a input +$input = new Input('my-text'); +$input->setRequired(true); + +$inputFilter = new InputFilter(); +$inputFilter->add($input); +$form->setInputFilter($inputFilter); + +// Force a failure +$form->setData(array()); // Empty data +$form->isValid(); // Not valid + +// Within your view... + +/** + * Example #1: Default options + */ +echo $this->formElementErrors($element); +// + +/** + * Example #2: Add attributes to open format + */ +echo $this->formElementErrors($element, array('class' => 'help-inline')); +// + +/** + * Example #3: Custom format + */ +echo $this->formElementErrors() + ->setMessageOpenFormat('
') + ->setMessageSeparatorString('
') + ->setMessageCloseString('
') + ->render($element); +//
Value is required and can't be empty
+``` + +The following public methods are in addition to those inherited from +\[Zend\\Form\\View\\Helper\\AbstractHelper\](zend.form.view.helper.abstract-helper.methods). + +orphan + +### FormFile + +The `FormFile` view helper can be used to render a `` form input. It is meant to +work with the \[Zend\\Form\\Element\\File\](zend.form.element.file) element. + +`FormFile` extends from \[Zend\\Form\\View\\Helper\\FormInput\](zend.form.view.helper.form-input). + +Basic usage: + +```php +use Zend\Form\Element; + +$element = new Element\File('my-file'); + +// Within your view... + +echo $this->formFile($element); +// +``` + +For HTML5 multiple file uploads, the `multiple` attribute can be used. Browsers that do not support +HTML5 will default to a single upload input. + +```php +use Zend\Form\Element; + +$element = new Element\File('my-file'); +$element->setAttribute('multiple', true); + +// Within your view... + +echo $this->formFile($element); +// +``` + +orphan + +### FormHidden + +The `FormHidden` view helper can be used to render a `` HTML form input. It is +meant to work with the \[Zend\\Form\\Element\\Hidden\](zend.form.element.hidden) element. + +`FormHidden` extends from \[Zend\\Form\\View\\Helper\\FormInput\](zend.form.view.helper.form-input). + +Basic usage: + +```php +use Zend\Form\Element; + +$element = new Element\Hidden('my-hidden'); +$element->setValue('foo'); + +// Within your view... + +echo $this->formHidden($element); +// +``` + +orphan + +### FormImage + +The `FormImage` view helper can be used to render a `` HTML form input. It is +meant to work with the \[Zend\\Form\\Element\\Image\](zend.form.element.image) element. + +`FormImage` extends from \[Zend\\Form\\View\\Helper\\FormInput\](zend.form.view.helper.form-input). + +Basic usage: + +```php +use Zend\Form\Element; + +$element = new Element\Image('my-image'); +$element->setAttribute('src', '/img/my-pic.png'); + +// Within your view... + +echo $this->formImage($element); +// +``` + +orphan + +### FormInput + +The `FormInput` view helper is used to render a `` HTML form input tag. It acts as a base +class for all of the specifically typed form input helpers (FormText, FormCheckbox, FormSubmit, +etc.), and is not suggested for direct use. + +It contains a general map of valid tag attributes and types for attribute filtering. Each subclass +of `FormInput` implements it's own specific map of valid tag attributes. + +The following public methods are in addition to those inherited from +\[Zend\\Form\\View\\Helper\\AbstractHelper\](zend.form.view.helper.abstract-helper.methods). + +orphan + +### FormLabel + +The `FormLabel` view helper is used to render a `