View Source

<ac:macro ac:name="toc" />

<h2>Overview</h2>

<p><code>Zend_Form</code> was a big step forward with the 1.5 release; it provided a number of things that simply worked out of the box:</p>

<ul>
<li>Validation</li>
<li>Filtering against XSS attacks</li>
<li>Form output</li>
</ul>


<p>That said, it came with several problems:</p>

<ul>
<li>Since validation and filtering were integrated into the form, there was no way to push that to the domain model easily; many developers found they were needing to duplicate validation and filtering efforts.
<ul>
<li>This is largely due to the fact that validation/filter chains are attached to individual elements, and not the form itself, making aggregation of all rules difficult.</li>
</ul>
</li>
<li>The decorators, while powerful and flexible, (a) are also difficult to explain to new users, and (b) can pose new difficulties to the end-user (what precedence is taken? how do I alter output of this decorator? why does this one allow wrapping in HTML tags, and this one does not? etc.)</li>
</ul>


<h2>Proposal</h2>

<p>The chief goals of this RFC for refactoring Zend\Form are:</p>

<ul>
<li>Separate validation/normalization from the form object hierarchy; forms should consume validation/normalization chains only, and only for purposes of error reporting.</li>
<li>Move view-related functionality &ndash; aka, the decorators &ndash; to the view layer, and make the functionality more declarative/programmatic.</li>
<li>Forms, fieldsets, and elements would primarily be value objects. Forms would contain fieldsets and elements, and metadata describing the form. Elements would contain their value, and then metadata describing the element.</li>
</ul>


<h3>General Architecture</h3>

<p>Forms will aggregate:</p>

<ul>
<li>Form metadata (action, method, id, etc.)</li>
<li>Elements and fieldsets (i.e., groups of elements)</li>
<li>Validation/Normalization chains (input filters)</li>
</ul>


<p>Additionally, a Factory subcomponent would be provided. The factory will create Forms (and all the objects forms composes). Optionally, it can use a Builder, which will try to automate creation of forms based on annotated models and a provided Form Definition object.</p>

<h3>Validation/Normalization</h3>

<p>It's unusual to validate an element in isolation. As such, validation chains should be attached only to the form.</p>

<ul>
<li><strong>MUST</strong> allow detaching/attaching validation/normalization chains, hereafter titled <strong>&quot;input filters&quot;</strong></li>
<li><strong>MUST</strong> allow defining a tree of elements (i.e., (nested) fieldsets of elements)</li>
<li><strong>MUST</strong> allow retrieving a tree of error messages
<ul>
<li><strong>MUST</strong> allow passing element-specific error messages to that element</li>
</ul>
</li>
<li><strong>MUST</strong> allow partial validations
<ul>
<li><strong>SHOULD</strong> allow indicating which specific elements must be valid</li>
</ul>
</li>
<li><strong>COULD</strong> allow form elements to hint which validators/filters are desired, e.g., via annotations; a directive would then tell the form to use this data when retrieving the input filter.</li>
</ul>


<p>In practice, the input filters would operate similarly to <code>Zend\Filter\Input</code>, though the goal is to provide a more programmatic API as well as a factory for generating the chains. Additionally, the chains for individual elements should be detachable.</p>

<p>The desired outcome is something like this:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$entity = new SomeEntity($data);
if (!$entity->isValid()) {
$form = new SomeForm();
$form->setInputFilter($entity->getInputFilter());
// pass form to view model, etc.
}
]]></ac:plain-text-body></ac:macro>

<p>Alternately, you could bind a model (an object). The form would validate data via its composed input filter, but, on validation, pass the normalized values into the model.</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$entity = new SomeEntity();
$form->bindInput($data);
$form->bindModel($entity);
if ($form->isValid()) {
// do something with the entity
$repository->save($entity);
} else {
// redisplay form
}
]]></ac:plain-text-body></ac:macro>

<p>If you're not binding validated values to an object, you can simply retrieve them as an array.</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$entity = new SomeEntity();
$form->bindInput($data);
if ($form->isValid()) {
// Grab the values. If not bound to a model, returns an array; otherwise,
// this returns the model object
$values = $form->getData();
} else {
// redisplay form
}
]]></ac:plain-text-body></ac:macro>


<p>To accomplish partial validation, you hint to the form which elements you're interested in prior to validation.</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$form->setValidationGroup('username', 'password', 'password_confirmation');
if ($form->isValid()) {
$values = $form->getData(); // contains only 'username', 'password' and 'password_confirmation'
}
// setValidationGroup(Form::VALIDATE_ALL) resets to default behavior
]]></ac:plain-text-body></ac:macro>

<h3>Rendering</h3>

<p>Decorators are an elegant way to render elements. However, they are also very difficult to teach, particularly to developers not familiar with design patterns. (It also doesn't help that the implementation is mis-named; they more accurately follow the <em>Visitor</em> pattern.)</p>

<p>An additional problem in ZF1 is that developers and/or designers often want very fine-grained control of the output, making the decorators a poor fit.</p>

<p>My proposal is to instead create new and/or modify existing form view helpers. These would consume the form and/or element metadata in order to create output; a given object could be passed to several helpers to create compound output when desired. As a simple example:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
<?php echo $this->form($form)->emitStartTag(); // form opening tag ?>

<?php $email = $form->getElement('email'); ?>
<div class="element">
<?php echo $this->formLabel($email) ?>
<?php echo $this->formEmail($email) ?>
<?php echo $this->formErrors($email) ?>
</div>

<input type="submit" name="submit-email" value="Send" />
<?php echo $this->form()->emitEndTag(); // form closing tag ?>
]]></ac:plain-text-body></ac:macro>

<p>A view helper per general form input type will be provided, along with &quot;generic&quot; view helpers that will introspect the type and proxy to the appropriate specific view helper.</p>

<p>&quot;Wait, this looks like a lot of work!&quot; I hear some saying. However, using the lesson from decorators, repitition can be easily addressed: create helpers for common combinations:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
namespace My\Helper;

use Zend\Form\ElementInterface,
Zend\View\Helper\AbstractHelper;

class WrappedElement extends AbstractHelper
{
public function __invoke(ElementInterface $element, $type = 'text', $class = 'element')
{
$view = $this->getView();
$broker = $view->broker();
$input = $broker->load('form_' . $type);
$label = $broker->load('form_label');
$errors = $broker->load('form_errors');
return sprintf(
"<div class=\"%s\">\n%s\n%s\n%s\n</div>",
$view->escape($class),
$label($element),
$input($element),
$errors($element)
);
}
}
]]></ac:plain-text-body></ac:macro>

<p>and the above would be used like this within a view script:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$email = $form->getElement('email');
echo $this->wrappedElement($email, 'email');
]]></ac:plain-text-body></ac:macro>

<p>The goal for ZF2 is to provide a basic set of these &quot;combination&quot; helpers that can be used to accomplish basic form markup with minimal effort by developers. For anything more complex or specific, developers will need to write their own helpers and/or turn to third party modules.</p>

<h3>Factory, Builder, and model binding</h3>

<p>The Factory will be used to create form objects, filtersets, elements, and the input filter used by the form based on the configuration provided.</p>

<p>The Builder component would provide a way to generate forms from models automatically. Additionally, Form would provide methods for mapping values to models automatically. These features will benefit rapid application development.</p>

<ul>
<li><strong>MUST</strong> provide a factory for form elements</li>
<li><strong>MUST</strong> provide a factory for filtersets</li>
<li><strong>MUST</strong> provide a factory for forms</li>
<li><strong>MUST</strong> provide a factory for the input filter</li>
<li><strong>MUST</strong> allow binding form data directly to objects (models).</li>
<li><strong>SHOULD</strong> include a form builder service to build forms from annotated objects automatically (type guessing, filters, validators, etc)
<ul>
<li><strong>SHOULD</strong> be extensible to allow module developers to developer additional annotations, e.g., Doctrine ORM\ODM.</li>
<li><strong>SHOULD</strong> provide caching to reduce the performance hit of building forms from annotations/reflection.</li>
</ul>
</li>
</ul>


<h4>Sample class and definition</h4>

<p>The following classes are used examples below:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
namespace My\Model;

use Zend\Filter,
Zend\Form\Element,
Zend\Validator as Assert;

class User
{
/**
* @Assert\Length({"min":3,"max":25})
* @Filter\StringTrim
*/
public $username;

/**
* @Assert\EmailAddress
* @Filter\StringTrim
* @Element\Password({label="My Super Cool Label"})
*/
public $password;
}

namespace My\Form\Definition;

use Zend\Form\Builder,
Zend\Form\Definition;

class User
{
public function build(Builder $builder)
{
// Builder::add($name, $type, $options);
// options would be merged (override) annotation supplied options; if no
// annotations are found, or no model_class specified, these would be
// used alone.

$builder->add('username') // An exception is thrown because no element annotation or $type was provided.
->add('password'); // Element is type "password" as specified by annotation.
}

// Used to uniquely identify the form.
// Typically, this is the id and/or name used when rendering the form.
public function getName()
{
return 'user';
}

// Sets default Zend\Form options (action, method, etc) as well as
// specifying the model class for the definition (or anything else
// required).
public function getOptions()
{
return array('model_class' => 'My\Model\User');
}
}
]]></ac:plain-text-body></ac:macro>

<h4>Sample usage (create)</h4>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
// Assume posted data contains:
// username: foo
// password: bar
//
// Assume a configured form factory with a configured form builder has been
// injected into the controller.
public function createAction()
{
$request = $this->getRequest();
$form = $this->formFactory->getForm('My\Form\Definition\User');

if ($request->isPost()) {
$userForm->bindInput($request->post());

$user = $userForm->getData(); // My\Model\User
echo $user->username; // foo
echo $user->password; // bar

if ($userForm->isValid()) {
// do stuff
}
}

return new ViewModel(array('form' => $userForm));
}
]]></ac:plain-text-body></ac:macro>

<h4>Sample usage (edit)</h4>
<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
public function editAction()
{
$user = new \My\Model\User;
$user->username = 'foo2';
$user->password = 'bar2';

$request = $this->getRequest();
$form = $this->formFactory->getForm('My\Form\Definition\User');
$form->bindModel($user);

return new ViewModel(array('form' => $userForm));
}
]]></ac:plain-text-body></ac:macro>

<h3>Interfaces</h3>

<p>These are some of the interfaces for the various objects discussed in the proposal. They should be considered incomplete.</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
interface FormInterface extends IteratorAggregate, FieldsetInterface
{
const VALIDATE_ALL = 1;
const VALUES_NORMALIZED = 2;
const VALUES_RAW = 3;
const VALUES_AS_ARRAY = 4;

public function bindInput($data);
public function bindModel(object $model);
public function isValid();
public function getData($flag = FormInterface::VALUES_NORMALIZED);
public function setInputFilter();
public function getInputFilter();
public function setValidationGroup(); // likely proxies to input filter
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
interface FieldsetInterface extends ElementInterface
{
public function add($elementOrFieldset, $order = null);
public function remove($elementOrFieldset);
public function getElements();
public function getFieldsets();
public function getIterator(); // iterate over elements and filtersets in order
public function setMessages($messages); // hash of element names => messages
public function getMessages($elementName);
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
interface ElementInterface
{
public function setAttribute($key, $value);
public function getAttribute($optionalKey);
public function setAttributes($arrayyOrTraversable);
public function getAttributes();
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
interface DefinitionInterface
{
public function getName();
public function getOptions();
public function build(Builder $builder);
}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
// allows listeners so that a listener can intercept types, inject options, etc
interface BuilderInterface extends \Zend\EventManager\EventManagerAware
{
// Map of element types => classes
public function setTypeMap($typeMap);

// Accept definition object or class name
public function getForm($definition);

// Annotations we know about
public function setAnnotations($annotations);

// Add an element to the form being built
public function add($name, $type = null, array $options = array());
}
]]></ac:plain-text-body></ac:macro>