Integrating ZF2 forms into Slim

Let's say that you want to use Zend Framework 2's Form component outside of ZF2 itself. In this case, a Slim application.

It turns out that Composer makes this quite easy, though there's quite a lot of code involved, so this is a long article.

Start with a really simple Slim Application.

index.php:

require 'vendor/autoload.php';
$app = new \Slim\Slim();

$app->map('/', function () use ($app) {
    $app->render('home.php', array(
        'form' => $form
    ));
})->via('GET', 'POST');

$app->run();

templates/home.php:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
</head>

<body role="document">
  <div class="container">
    <h1 class="content-subhead">Using ZF2 Forms with Slim</h1>
  </div>
</body>
</html>

and of course, composer.json:

{
    "require": {
        "slim/slim": "2.*",
    }
}

Given this starting point, we can now add Zend\Form.

Firstly, we update composer.json:

{
    "require": {
        "slim/slim": "2.*",
        "zendframework/zend-form": "2.3.*",
        "zendframework/zend-servicemanager": "2.3.*",
        "zendframework/zend-i18n": "2.3.*",
        "zendframework/zend-view": "2.3.*",
        "zendframework/zend-escaper": "2.3.*"
    },
    "autoload": {
        "psr-4": {
            "RKA\\" : "RKA"
        }
    }
}

I've also set up the RKA namespace as we'll have to create some classes ourselves.

These are minimum set of components for it all to work. If you do your own rendering, then you don't need zend-view or zend-escaper. You'll then only need zend-i18n for some validators and filters.

When you run composer update, you'll see that the following ZF2 components are installed for you:

  • Zend\StdLib
  • Zend\Validator
  • Zend\Filter
  • Zend\InputFilter
  • Zend\Form
  • Zend\Loader
  • Zend\EventManager
  • Zend\View
  • Zend\I18n
  • Zend\ServiceManager
  • Zend\Escaper

Other than EventManger which is required by View, this doesn't seem an unreasonable list to me.

As, we'll probably want to override the default validation messages and add our own validators & filters, it's worth spending the time setting up our own instance of Zend\ServiceManager and using that to instantiate the form for us.

Configuring the service manager with the relevant plugin managers is usually done by Zend\Mvc & Zend\ModuleManager, so we'll simplify and create a single class called ServiceManagerConfigurator. This is the most complicated part of the entire thing and looks like this:

RKA\ServiceManagerConfigurator:

namespace RKA;

use Zend\ServiceManager\Config as ServiceConfig;
use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class ServiceManagerConfigurator
{
    // List of plugin managers to register (key name => class name)
    public $pluginManagers = [
        'FilterManager'      => 'Zend\Filter\FilterPluginManager',
        'FormElementManager' => 'Zend\Form\FormElementManager',
        'InputFilterManager' => 'Zend\InputFilter\InputFilterPluginManager',
        'ValidatorManager'   => 'Zend\Validator\ValidatorPluginManager',
        'ViewHelperManager'  => 'Zend\View\HelperPluginManager',
    ];
    
    // Keys to look for in the config array (config key name => sm key name)
    public $configKeys = [
        'validators'      => 'ValidatorManager',
        'filters'         => 'FilterManager',
        'form_elements'   => 'FormElementManager',
        'input_filters'   => 'InputFilterManager',
        'view_helpers'    => 'ViewHelperManager',
    ];

    // Main method that gives us back a configured ServiceManager
    public function createServiceManager(array $config)
    {
        // create ServiceManager
        $serviceManager = new ServiceManager();

        // set an initializer for ServiceLocatorAwareInterface
        $serviceManager->addInitializer(
            function ($instance, ServiceLocatorInterface $serviceLocator) {
                if ($instance instanceof ServiceLocatorAwareInterface) {
                    $instance->setServiceLocator($serviceLocator);
                }
            }
        );
        // add $serviceManger to the config keys so we can configure it
        $this->configKeys = ['service_manager' => $serviceManager] 
                            + $this->configKeys;

        // add the pluginManagers to the ServiceManager
        foreach ($this->pluginManagers as $key => $className) {
            $serviceManager->setInvokableClass($key, $className);
        }

        // add Zend\Form's view helpers to the ViewHelperManager
        $viewHelperManager = $serviceManager->get('ViewHelperManager');
        $vhConfig = new \Zend\Form\View\HelperConfig;
        $vhConfig->configureServiceManager($viewHelperManager);

        // apply configuration
        $this->applyConfig($serviceManager, $config);
        return $serviceManager;
    }

    // Helper method to apply config to the service manager and all plugins
    public function applyConfig($serviceManager, $config)
    {
        foreach ($this->configKeys as $keyName => $thisManager) {

            $smConfig = array();
            if (isset($config[$keyName]) && is_array($config[$keyName])) {
                $smConfig = $config[$keyName];
            }
            
            // Get this service manager from the main service manager if it
            // isn't an instance already
            if (!$thisManager instanceof ServiceManager) {
                $thisManager = $serviceManager->get($thisManager);
            }

            // Apply the config to this service manager
            $serviceConfig = new ServiceConfig($smConfig);
            $serviceConfig->configureServiceManager($thisManager);
        }
    }
}

I've commented the code so that hopefully, it's fairly easy to follow.

The key method is createServiceManager which will instantiate a ServiceManager object, add the plugin managers and other bits and bobs before calling the helper method applyConfig which will do the custom configuration.

To use it, we update our index.php:

index.php:

$app = new \Slim\Slim();

$config = [
    'validators' => array(
        'invokables' => array(
            'email-address' => 'RKA\Validator\EmailAddress',
            'string-length' => 'RKA\Validator\StringLength',
            // etc.
        ),
    ),
    'form_elements' => array(
        'invokables' => array(
            'RKA\ExampleForm'  => 'RKA\ExampleForm',
        ),
    ),

];

$smConfigurator = new RKA\ServiceManagerConfigurator();
$app->serviceManager = $smConfigurator->createServiceManager($config);
$app->view(new RKA\View());

// continue with $app->map() calls

We firstly set up the config that we want. In this case, I want to add my custom validators and then I add the form we want to use. Obviously, if we had custom filters, form elements or view helpers we could also add those too. Also, although I'm using invokables, you could also use factories if any of the classes you were adding had dependencies that needed adding.

The form is defined as any other Zend\Form is. In this case, I created a separate class called RKA\ExampleForm, but we could equally have used the factory creation.

RKA\ExampleForm:

namespace RKA;

use Zend\Form\Form;
use Zend\InputFilter\InputFilterProviderInterface;

class ExampleForm extends Form implements InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'email',
            'options' => [
                'label' => 'Email address',
            ],
            'attributes' => [
                'id'       => 'email',
                'class'    => 'form-control',
                'required' => 'required',
            ],
        ]);

        $this->add([
            'name' => 'submit',
            'type' => 'button',
            'options' => [
                'label' => 'Go!',
            ],
            'attributes' => [
                'class' => 'btn btn-default',
            ],
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'email' => [
                'required' => true,
                'filters'  => [
                    ['name' => 'StringTrim'],
                    ['name' => 'StripTags'],
                ],
                'validators' => [
                    ['name' => 'EmailAddress'],
                ],
            ],
        ];
    }
}

Nothing complicated is going on here. We add two elements to the form and then define some validation for the email element.

To use the form, we update our handler for '/':

$app->map('/', function () use ($app) {
    $formElementManager = $app->serviceManager->get('FormElementManager');
    $form = $formElementManager->get("RKA\ExampleForm");

    if ($app->request->isPost()) {
        $data = $app->request->post();
        $form->setData($data);
        $isValid = $form->isValid();
        if ($form->isValid()) {
            echo "Success!";
            exit;
        }
    }

    $app->render('home.php', array(
        'form' => $form
    ));
})->via('GET', 'POST');

We grab the $formElementManager from the service manager and then grab our example form form there. If the request is a POST, then we set the posted data to the form and test for validity. On success, we would probably redirect. Finally we render our view template, passing the form to it.

We render the form within the view template using Zend\Form's formRow view helpers to render the label, element and errors and label in one go for us, though we could have used the formLabel, formElement and formElementErrors view helpers separately:

templates/home.php:

    <!-- ... -->
    <h1 class="content-subhead">Using ZF2 Forms with Slim</h1>
      
      <form method="POST" role="form">
          <div class="form-group">
              <?= $this->formRow($form->get('email')) ?>
          </div>
          <?= $this->formElement($form->get('submit')) ?>
      </form>

    <!-- ... -->        
 

Finally, we have to extends Slim\View to integrate ZF2's view helper system with it. This requires implementing the Zend\View\Renderer\RendererInterface and also writing a __call() method:

Rka\View:

namespace RKA;

use Slim\Slim;
use Zend\View\Renderer\RendererInterface;

class View extends \Slim\View implements RendererInterface
{
    protected $viewHelpers;
    protected $helperCache;

    public function __construct()
    {
        parent::__construct();

        $app = Slim::getInstance();
        $sm = $app->serviceManager;
        $this->viewHelpers = $sm->get('ViewHelperManager');
    }

    public function plugin($name, array $options = null)
    {
        $helper = $this->viewHelpers->get($name, $options);
        $helper->setView($this);
        return $helper;
    }

    public function __call($method, $argv)
    {
        if (!isset($this->helperCache[$method])) {
            $this->helperCache[$method] = $this->plugin($method);
        }
        if (is_callable($this->helperCache[$method])) {
            return call_user_func_array($this->helperCache[$method], $argv);
        }
        return $this->helperCache[$method];

    }

    // Required by RendererInterface
    public function render($template, $data = null)
    {
        return parent::render($template, $data);
    }

    public function getEngine()
    {
        return $this;
    }
    
    public function setResolver(\Zend\View\Resolver\ResolverInterface $resolver)
    {
        return $this;
    }
}

One undocumented feature of the formElement view helper is that it looks for a method called plugin in the View class, so I created one for it!

The final result looks like:

Slim zendform

That's it. The code that's shown here is on GitHub in the slim-zendform project, so you can download it and play around yourself.

One thought on “Integrating ZF2 forms into Slim

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>