Forms aren't particularly hard at all. They're important, but we don't truly like making them (coder parents in the audience that pack lunches for their kids can empathize on two planes here). I actually set out today to check out the ZF2 Annotations (and Annotation Builder) and got deflated pretty quickly. Adding a DbRecordNoExists Validator to your email field with Annotations? I stumbled a few times, and hit the #zftalk chan on Freenode and asked:
Is there a clean way to specify the database to be used in a no exists validator using Annotations?
<The usual IRC strange-moment-of-silence passes>
"Using annotations? Are you crazy?"
Revisiting the Usual Approach
Alright, shit - I guess I should try to clean up "the usual". That thought was quickly put on hold as an IRC member (whose name eludes me, but whose blog I still have tabbed) said "Here I do forms like this! Rig this once, and the forms are easy thereafter".
If you check the source at that link, there's an interesting nugget in there, which is reusing a single config syntax to define both validator and element. Pretty low hanging fruit, but quite good!
Then, grizzm0 (a channel regular) posts his take on forms. What struck me is the cleanliness offered by the PHP 5.4+ square-bracket array instantiation. Every ZF2 resource you'll find probably uses array(), and where code is art, [] is a bit more of a masterpiece.
Form-Fu
Combining the blog and gist above, I quickly whittled it down to to a class, EasyForm, that your other forms can extend. You could probably shave out the two managers, but if you use htmlpurifier (you should!) you'll want them there.
namespace CirclicalUser\Form;
use Zend\Filter\FilterChain;
use Zend\Form\Form,
Zend\Form\Element,
Zend\Captcha,
Zend\InputFilter,
Zend\Validator\ValidatorChain;
class EasyForm extends Form implements InputFilter\InputFilterProviderInterface
{
private $raw_elements = [];
public function __construct( $name, $filter_manager, $validator_manager )
{
parent::__construct( $name );
$chain = new FilterChain();
$chain->setPluginManager($filter_manager);
$this->getFormFactory()
->getInputFilterFactory()
->setDefaultFilterChain( $chain );
$chain = new ValidatorChain();
$chain->setPluginManager( $validator_manager );
$this->getFormFactory()
->getInputFilterFactory()
->setDefaultValidatorChain( $chain );
$this->setAttribute('novalidate', '' );
}
public function add( $elementOrFieldset, array $flags = array() )
{
if( is_array( $elementOrFieldset ) )
$this->raw_elements[] = $elementOrFieldset;
return parent::add( $elementOrFieldset, $flags );
}
/**
* Should return an array specification compatible with
* {@link Zend\InputFilter\Factory::createInputFilter()}.
*
* @return array
*/
public function getInputFilterSpecification()
{
$filters = [];
foreach( $this->raw_elements as $e )
{
if( isset( $e['type'] ) )
unset( $e['type'] );
$filters[] = $e;
}
return $filters;
}
}
This form class overrides "add" to store a copy of your config on the way in (if it was an array, since you can also pass Elements to add when creating forms).
Usage is pretty simple, sample form getup:
namespace CirclicalUser\Form;
use Zend\Form\Element,
Zend\Captcha,
Zend\InputFilter,
Zend\Validator\Db\AbstractDb;
class User extends EasyForm implements InputFilter\InputFilterProviderInterface
{
private $slave_table;
/**
* Construct a registration form, with an AuthenticationFormInterface instance to establish minimum field count
*/
public function __construct( $filter_manager, $validator_manager, $slave_table, $config = array() )
{
parent::__construct('registration', $filter_manager, $validator_manager );
$this->slave_table = $slave_table;
$this->add([
'name' => 'email',
'type' => 'email',
'required' => true,
'options' => [
'label' => 'Email',
],
'attributes' => [
'class' => 'form-control',
],
'validators' => [
[
'name' => 'dbnorecordexists',
'options' => [
'table' => 'users',
'field' => 'email',
'adapter' => $this->slave_table,
'messages' => [ AbstractDb::ERROR_RECORD_FOUND => _( "That email is already taken, please log in instead" ), ],
],
],
[
'name' => 'emailaddress',
'options' => [
'useMxCheck' => true,
'useDeepMxCheck' => true,
'useDomainCheck' => true,
'message' => _( "That email address has a typo in it, or its domain can't be checked." ),
],
],
[
'name' => '\CirclicalUser\Form\Validator\SafeEmailAddress',
],
],
'filters' => [
['name' => 'arrayblock'],
['name' => 'stringtrim'],
['name' => 'htmlpurifier'],
['name' => 'stringtolower'],
]
]);
$honeypot = new Element\Hidden( 'axis' );
$this->add( $honeypot );
}
}
I build that form using a Factory, that's quite simply rigged like so:
//...
'user_form' => function( $sm ){
return new User(
$sm->get('FilterManager'),
$sm->get('ValidatorManager'),
$sm->get('slave_database')
);
},
//...
I like it!
If you however, have stronger Kung Fu -- please share (and thanks!).