How can you use ZF3 Forms outside of Zend Framework? It's pretty involved, but not that complicated. How about mustache templates, can you mix ZF3 Forms with a project that's already using Mustache templates?

Using ZF3 Forms outside of a complete ZF3 project is somethign you might want to do if you are trying to standardize a legacy project, or you're using a framework like F3, Pixie, or Code Igniter (CI) and you don't like the forms component available. Whatever the reason, here's how you can jam the zf3 forms component into an existing project.

Dependencies

The Documentation site leads you to believe that you can simply use composer to require the forms component and off you go. Oh, if only that were true.

You need about 12 ZF components in total to make the forms work.

$ composer require zendframework/zend-form
$ composer require zendframework/zend-servicemanager
$ composer require zendframework/zend-view
$ composer require zendframework/zend-i18n
$ composer require zendframework/zend-escaper

Why aren't these components dependencies of the form library if you they are required? Who knows! In fact, there's already a bug about it that I blogged about previously.

Trial and Error

After you install the required dependencies (by hand) the next thing you should do is to just copy and paste some of the quick start code from the ZF3 doc site into your project and see what happens.

<?php
use Zend\Captcha;
use Zend\Form\Element;
use Zend\Form\Fieldset;
use Zend\Form\Form;
use Zend\InputFilter\Input;
use Zend\InputFilter\InputFilter;

// Create a text element to capture the user name:
$name = new Element('name');
$name->setLabel('Your name');
$name->setAttributes([
    'type' => 'text',
]);

// Create a text element to capture the user email address:
$email = new Element\Email('email');
$email->setLabel('Your email address');

// Create a text element to capture the message subject:
$subject = new Element('subject');
$subject->setLabel('Subject');
$subject->setAttributes([
    'type' => 'text',
]);

// Create a textarea element to capture a message:
$email = new Element\Email('email');
$message = new Element\Textarea('message');
$message->setLabel('Message');

// Create a CAPTCHA:
// let's skip the captcha for now

// Create a CSRF token:
$captcha = new Element\Captcha('captcha');
$csrf = new Element\Csrf('security');

// Create a submit button:
$send = new Element('send');
$send->setValue('Submit');
$send->setAttributes([
    'type' => 'submit',
]);

// Create the form and add all elements:
$form = new Form('contact');
$form->add($name);
$form->add($email);
$form->add($subject);
$form->add($message);
$form->add($csrf);
$form->add($send);

At this point you should have no errors. Now let's try to render the form

Rendering via PHP

Right in your controller, let's just try to render the form with PHP. Eventually we're going to replace this with Mustache templates, so it doesn't matter where this code sits right now.

$form->prepare();

// Assuming change the / to whatever framework you're using does for routes or app urls
$form->setAttribute('action', '/');

// Set the method attribute for the form
$form->setAttribute('method', 'post');

$formHelper     = new Zend\Form\View\Helper\Form();
$labelHelper    = new Zend\Form\View\Helper\FormLabel();
$elementHelper  = new Zend\Form\View\Helper\FormElement();
$inputHelper    = new Zend\Form\View\Helper\FormInput();
$textareaHelper = new Zend\Form\View\Helper\FormTextarea();
$errorHelper    = new Zend\Form\View\Helper\FormElementErrors();

// Render the opening tag
echo $formHelper->openTag($form);
?>
<div class="form_element">
<?php
    $name = $form->get('name');
    echo $labelHelper->openTag($name) . $name->getLabel();
    echo $inputHelper($name);
    echo $errorHelper($name);
    echo $labelHelper->closeTag($name);
?></div>

<div class="form_element">
<?php
    $subject = $form->get('subject');
    echo $labelHelper->openTag($subject) . $subject->getLabel();
    echo $inputHelper($subject);
    echo $errorHelper($subject);
    echo $labelHelper->closeTag($subject);
?></div>

<div class="form_element">
<?php
    $message = $form->get('message');
    echo $labelHelper->openTag($message) . $message->getLabel();
    echo $textareaHelper($message);
    echo $errorHelper($message);
    echo $labelHelper->closeTag($message);
?></div>

<?= $elementHelper($form->get('send')); ?>

<?= $formHelper->closeTag(); ?>

This code differs from the ZF3 documentation because ZF3 has a unique way to automatically load helpers from the controller using $this->helper(). For our purposes, we need to create these View Helpers by hand.

If you can see a form without the submit button you're doing it right.

We can't see the submit button yet because the FormElement view helper requires zend view to dynamically load plugins.

If we change the definition of $elementHelper to an instance of \Zend\Form\View\Helper\FormButton and pass 'submit' as the second argument when invoking it, you should see a submit button. But, we need to make all the dynamic Zend View plugin loading work so that our mustache templates are a lot simpler.

form rendered with submit button, but poor layout

Turning on Zend View

Now let's enable the Zend View and Zend Service Manager so we can trim down this code by using the dynamic FormRow helper. The FormRow helper should print out the label, the input, and any errors with one function call.


//start ZF3 Form deps
$rowHelper    = new Zend\Form\View\Helper\FormRow();
$elementHelper  = new Zend\Form\View\Helper\FormElement();
$renderer = new Zend\View\Renderer\PhpRenderer();
$configProvider = new \Zend\Form\ConfigProvider();
$renderer->setHelperPluginManager(new \Zend\View\HelperPluginManager(new \Zend\ServiceManager\ServiceManager(), $configProvider()['view_helpers']));
$rowHelper->setView($renderer);
$elementHelper->setView($renderer);
//end ZF3 Form deps

Paste the above code after you create the form, but before you render it. Remove the call to create the other $elementHelper

Now the Submit button should show up!

Refactor into Mustache templates

Now we can take that code marked with "ZF3 Form deps" and make it avaiable to mustache templates. Paste that code above wherever you enable the Mustache template engine and add 2 helpers to mustache.

include_once 'local/mustache/mustache/src/Mustache/Autoloader.php';
Mustache_Autoloader::register();
$options = [];

//start ZF3 Form deps
$rowHelper    = new Zend\Form\View\Helper\FormRow();
$renderer = new Zend\View\Renderer\PhpRenderer();
$configProvider = new \Zend\Form\ConfigProvider();
$renderer->setHelperPluginManager(new \Zend\View\HelperPluginManager(new \Zend\ServiceManager\ServiceManager(), $configProvider()['view_helpers']));
$rowHelper->setView($renderer);
//end ZF3 Form deps
//notice we don't need $elementHelper at all anymore,
//rowHelper does it all


//replace this with your own way to locate the template folder for Mustache
$templatePath = _get('template_basedir')._get('template_name').'/';

$this->m = new Mustache_Engine([
	'cache' => 'var/cache/templates/',
	'loader' => new Mustache_Loader_FilesystemLoader( $templatePath, $options),
	'partials_loader' => new Mustache_Loader_FilesystemLoader( $templatePath, $options),
	'helpers' => array(
	'inputHelper' => function($element) use ($rowHelper){
		return $rowHelper($element);
	},
	'var_dump' => function($element) { 
		return var_dump($element);
	}
]);

Now Mustache is configured with a helper called inputHelper. We must use this as a filter and not a lambda. Filters can get access to data as objects, lambdas only get access to the text of the template.

{{! this will not work, this calls as a lambda and we only get the string '{{.}}', not any FormElement object }}
{% raw %}
{{#myForm}}
    <div>
        {{#inputHelper}}{{.}}{{/inputHelper}}
    </div>
{{/myForm}}

{{! this will call inputHelper helper as a filter and pass the current elememnt as an object }}
{{%FILTERS}}
{{#myForm}}
    <div>
        {{{. | inputHelper}}}
    </div>
{{/myForm}}

Notice that you can iterate over all the elements of a form by simply calling the form as a section.

Notice the 3 {{{ and 3 }}} meaning we want to unescape the output that comes from inputHelper.

Making the template more generic

Suppose you want all your forms rendered the same way, you can make a partial called renderFormBootstrap and call it from another template.

{{#myFormWrapped}}
{{> form/renderFormBootstrap }}
{{/myFormWrapped}}}
{{%FILTERS}}

<form action="{{#getAttributes}}{{action}}{{/getAttributes}}">

{{#.}}
<div class="form-group">
    {{{. | inputHelper}}}
</div>
{{/.}}

</form>

Because the Zend Form component is Iterable and Countable, just using the key as a section {{#myForm}} will produce output as many times as there are form elements. You don't want that!

We have to satisfy mustache by wrapping our form in an empty array. This is why you see {{#myFormWrapped}}, this will output what is inside only once, AND it will pass the form itself as the item referenced by {{.}} to the partial "renderFormBootstrap".

Bootstrap classes

Now, if we just add some classes to the form elements in the original PHP code, we can get some bootstrap styling activated.

$name->setAttributes([
    'type' => 'text',
    'class' => 'form-control' //<-- just this line is important
]);

Add that to each form element, including the $message element. form rendered with decent, bootstrap-style layout

Taking it further

Now that you understand how to use ZF3 Form components in a framework agnostic way, there are a few things you can try to add on your own.

  • CSRF Tokens
  • CAPTCHA Inputs
  • Form Factories

Have fun with ZF3 Form components!