9 – Create abstract form

When creating forms we’re going to be following the same principles we’ve followed earlier, mainly DRY and separation of concerns.

When you paste the following code into module/Blog/src/Blog/Form/PostForm.php you’ll see quickly enough what I mean.

namespace Blog\Form;

use Application\Form\AbstractForm;
use Zend\Form\Element;

class PostForm extends AbstractForm
{
    public function __construct($name = 'post-form', $options = [])
    {
        parent::__construct($name, $options);

        $this->addElements();
    }

    public function addElements()
    {
        $this->add([
            'name'     => 'id',
            'type'     => 'hidden',
            'required' => false,
        ]);

        $this->add([
            'name'     => 'title',
            'type'     => 'text',
            'required' => true,
            'options'  => [
                'label' => 'Title',
            ],
        ]);

        $this->add([
            'name'     => 'body',
            'type'     => 'textarea',
            'required' => true,
            'options'  => [
                'label' => 'Body',
            ],
        ]);

        $this->add([
            'name'       => 'submit',
            'type'       => 'submit',
            'attributes' => [
                'value' => 'Create post',
            ],
        ]);
    }
}

If you look at the code from the add.phml view, you’ll notice that the element ‘csrf’ is not present. Also, your editor should be giving you a warning about the usage and extending of AbstractForm; it doesn’t exist. As mentioned above the code, we’re being DRY.

We’re going to create the AbstractForm next. It contains stuff that you’ll wish to use in every form you create in the future. However, it is custom to my personal needs for it, so when you’ve read it and understand it, feel free to modify it to your own needs!

(As a side note, not part of the tutorial, this code contains the generation of CSRF hashes to prevent XSRF forgery. It’s not perfect and it’s not session/user bound. If you decide that you wish to continue using this, make it more secure by binding a CSRF token to a user so you can validate explicitly.)

To create the AbstractForm copy the following code into module/Application/src/Application/Form/AbstractForm.php:

namespace Application\Form;

use Zend\Di\ServiceLocator;
use Zend\Form\Element\Csrf;
use Zend\Form\ElementInterface;
use Zend\Form\Exception;
use Zend\Form\Exception\InvalidElementException;

class AbstractForm extends \Zend\Form\Form
{
    /**
    * CSRF timeout in seconds
    */
    protected $csrfTimeout = 7200; // 2 hours

    /**
    * @var ServiceLocator
    */
    protected $serviceLocator;

    /**
    * {@inheritDoc}
    */
    public function __construct($name = null, $options = [])
    {
        if (isset($options['serviceLocator'])) {
            $this->serviceLocator = $options['serviceLocator'];
            unset($options['serviceLocator']);
        }

        $csrfName = null;
        if (isset($options['csrfCorrector'])) {
            $csrfName = $options['csrfCorrector'];
            unset($options['csrfCorrector']);
        }

        parent::__construct($name, $options);

        if (null === $csrfName) {
            $csrfName = 'csrf';
        }

        $this->addElementCsrf($csrfName);
    }

    /**
    * Retrieve a named element or fieldset
    *
    * Extends Zend\Form with CSRF fields that can be retrieved by the name "CSRF"
    * but are resolved to their unique name
    *
    * @param  string                  $elementOrFieldset
    * @throws InvalidElementException
    * @return ElementInterface
    */
    public function get($elementOrFieldset)
    {
        if ($elementOrFieldset === 'csrf') {
            // Find CSRF element
            foreach ($this->elements as $formElement) {
                if ($formElement instanceof Csrf) {
                    return $formElement;
                }
            }
        } else {
            return parent::get($elementOrFieldset);
        }
    }

    /**
    * Adds CSRF protection
    */
    protected function addElementCsrf($csrfName = 'csrf')
    {
        // Set unique CSRF name based on module name, form name and csrfName variable to avoid CSRF collisions
        // on multiple forms in different browser tabs
        $className = get_class($this);
        $parts = explode('\\', $className);
        $uniqueCsrfName = lcfirst($parts[0]);
        if (count($parts) > 1) {
            $lastPart = lcfirst($parts[count($parts) - 1]);
            $lastPart = substr($lastPart, 0, strripos($lastPart, 'Form'));
            $uniqueCsrfName .= '_' . $lastPart;
        }
        $uniqueCsrfName .= '_' . $csrfName;

        $this->add([
            'type'    => 'Zend\Form\Element\Csrf',
            'name'    => $uniqueCsrfName,
            'options' => [
                'csrf_options' => [
                    'timeout' => $this->csrfTimeout,
                ],
            ],
        ]);
    }
}

Something the code above also takes care of is that we also have the Doctrine EntityManager available in Forms. Have a look at Zend Collections as to why this is useful.

In the AbstractForm we also create a custom implementation of the get() function for usage with CSRF Form Elements. If the field we’re trying to get is not the ‘csrf’ element, we’re executing the default get() function as defined in the Zend\Form\Form class.

If you now go back to PostForm.php you’ll notice any errors are gone.

What a Form needs, though it is not required, is validation rules. Zend Framework magically already defines what the InputFilter and Validators should be based on how the form is setup. However, we need to use a custom InputFilter so that we can match the input of the user to the limits as they are defined in our PostForm.php.