Category Archives: Zend Framework 2

Zend\Input fallback value

Recently an issue was reported against Zend\InputFilter where the reporter has discovered a regression where the fallback value wasn't being populated correctly. Matthew investigated, fixed it and asked me to review it.

I was fascinated as I didn't realise (or had completely forgotten!) that Zend\Input and Zend\InputFilter supported fallback values so I looked into it and it turns out that it's simple and works exactly as its name implies.

For the basic case of using an Input directly, you use it like this:

<?php
$name = new \Zend\InputFilter\Input('name');
$validators = $name->getValidatorChain();
$validators->addValidator(new \Zend\Validator\StringLength(5), true);

$name->setFallbackValue('Rob Allen');
$name->setValue('');

$isValid = $name->isValid();
var_dump($name->getValue());

The output is "Rob Allen".

That is, when the value supplied is empty, then, the fallback value is set instead and used.

A wrinkle

There is, however, a wrinkle! Sometimes $name->getValue() returns an empty string and this occurs due to particular combinations of required, allow_empty & continue_if_empty as I've already discussed.

If continue_if_empty is false then the combination of required & allow_empty affects whether you get the fallback value or an empty string when calling getValue() after isValid():

required allow_empty What happens
true false With an empty value, the fallback value is returned in getValue().
true true The fallback value is ignored and an empty value is returned in getValue().
false false The fallback value is ignored and an empty value is returned in getValue().
false true The fallback value is ignored and an empty value is returned in getValue().

If you compare this table to the one in the last post, you'll notice that in the three cases where the validators are not run, the fallback value is not set. This is not surprising as those combinations result in a short-circuit of isValid().

Test app

Again, I used a test application to check this out

<php
require 'vendor/autoload.php';

$values = [
    // [contine_if_empty, required, allow_empty]
    [false, true, false],
    [false, true, true],
    [false, false, false],
    [false, false, true],
    [true, true, false],
    [true, true, true],
    [true, false, false],
    [true, false, true],
];

foreach ($values as $row) {
    test(...$row);
}

function test($continueIfEmpty, $required, $allowEmpty)
{
    // set up Input with a StringLength validator so we'll know if the
    // validators have run as they will always fail
    $name = new \Zend\InputFilter\Input('name');
    $validators = $name->getValidatorChain();
    $validators->addValidator(new \Zend\Validator\StringLength(5), true);

    $name->setFallbackValue('Rob Allen');
    $name->setValue('');

    $name->setRequired($required);
    $name->setAllowEmpty($allowEmpty);
    $name->setContinueIfEmpty($continueIfEmpty);

    // Test
    echo "continue_if_empty: " . (int)$continueIfEmpty;
    echo ", required: " . (int)$required;
    echo ", allow_empty: " . (int)$allowEmpty;
    $isValid = (int)$name->isValid();
    echo " - Result: isValid() = $isValid";
    if (!$isValid) {
        echo " " . current($name->getMessages());
    } else {
        echo ", value = " . $name->getValue();
    }
    echo "\n";
}

As before, this app simply runs through all combinations of required, allow_empty & continue_if_empty against a Zend\InputFilter\Input with a fallback value set and sees what happens.

This is the output:

$ php test.php 
continue_if_empty: 0, required: 1, allow_empty: 0 - Result: isValid() = 1, value = Rob Allen
continue_if_empty: 0, required: 1, allow_empty: 1 - Result: isValid() = 1, value = 
continue_if_empty: 0, required: 0, allow_empty: 0 - Result: isValid() = 1, value = 
continue_if_empty: 0, required: 0, allow_empty: 1 - Result: isValid() = 1, value = 
continue_if_empty: 1, required: 1, allow_empty: 0 - Result: isValid() = 1, value = Rob Allen
continue_if_empty: 1, required: 1, allow_empty: 1 - Result: isValid() = 1, value = Rob Allen
continue_if_empty: 1, required: 0, allow_empty: 0 - Result: isValid() = 1, value = Rob Allen
continue_if_empty: 1, required: 0, allow_empty: 1 - Result: isValid() = 1, value = Rob Allen\

Zend\InputFilter

Note that with the fix discussed at the start of this post, Zend\InputFilter works exactly the same as Zend\Input, as you'd expect. This fix was back ported to the 2.4 release too, so if you are using fallback values, ensure that you're using the latest 2.4 or 2.5 version.

Conclusion

If you want to use a fallback value with Zend\Input make sure that you set required to true and allow_empty to false. Fortunately this is the default, so that's probably what you're doing anyway!

Zend\Input and empty values

I'm forever getting confused about how the combination of Zend\Input's required, allow_empty & continue_if_empty interact with an empty value, so I've decided to write it down.

These settings define what happens when you try to validate an empty value for a given input. For Zend\Input, empty means exactly equal to null, an empty string or an empty array.

Firstly, let's start with the three settings:

Setting Default What it does
required true When true, the value must not be empty (unless allow_empty is true).
allow_empty false When true, the field may be empty if it's required.
continue_if_empty false When true, the validators are run even if the field is empty.

Firstly, contine_if_empty controls if the validators are run for an empty value. If contine_if_empty is true, then the validators are always executed.

If contine_if_empty is false, then the decision on whether to run the validators depends on the combination of required and allow_empty:

required allow_empty What happens
true false For an empty value, the validators are run. isValid() is determined by the validators and is false as the NotEmpty validator fails. (This is the default)
true true For an empty value, the validators are not run. isValid() = true.
false false For an empty value, the validators are not run. isValid() = true.
false true For an empty value, the validators are not run. isValid() = true.

Test app

To prove this, I wrote this little test app:

 ?php
require 'vendor/autoload.php';

$values = [
    // [contine_if_empty, required, allow_empty]
    [false, true, false],
    [false, true, true],
    [false, false, false],
    [false, false, true],
    [true, true, false],
    [true, true, true],
    [true, false, false],
    [true, false, true],
];

foreach ($values as $row) {
    test(...$row);
}

function test($continueIfEmpty, $required, $allowEmpty)
{
    // set up Input with a StringLength validator so we'll know if the
    // validators have run as they will always fail
    $name = new \Zend\InputFilter\Input('name');
    $validators = $name->getValidatorChain();
    $validators->addValidator(new \Zend\Validator\StringLength(5), true);
    $name->setValue('');

    $name->setRequired($required);
    $name->setAllowEmpty($allowEmpty);
    $name->setContinueIfEmpty($continueIfEmpty);

    // Test
    echo "continue_if_empty: " . (int)$continueIfEmpty;
    echo ", required: " . (int)$required;
    echo ", allow_empty: " . (int)$allowEmpty;
    $isValid = (int)$name->isValid();
    echo " - Result: isValid() = $isValid";
    if (!$isValid) {
        echo " " . current($name->getMessages());
    }
    echo "\n";
}

You'll need to run composer require zendframework/zend-inputfilter if you want to test it yourself.

Interesting side note: I got to use the splat operator, so this is PHP 5.6 only code :)

The test is quite simple. We add a StringLength validator to an input so that we know that the validators have run as it will always fail. We then iterate through all the the combinations of required, allow_empty & continue_if_empty and see what happens.

The output is:

$ php test.php 
continue_if_empty: 0, required: 1, allow_empty: 0 - Result: isValid() = 0 Value is required and can't be empty
continue_if_empty: 0, required: 1, allow_empty: 1 - Result: isValid() = 1
continue_if_empty: 0, required: 0, allow_empty: 0 - Result: isValid() = 1
continue_if_empty: 0, required: 0, allow_empty: 1 - Result: isValid() = 1
continue_if_empty: 1, required: 1, allow_empty: 0 - Result: isValid() = 0 The input is less than 5 characters long
continue_if_empty: 1, required: 1, allow_empty: 1 - Result: isValid() = 0 The input is less than 5 characters long
continue_if_empty: 1, required: 0, allow_empty: 0 - Result: isValid() = 0 The input is less than 5 characters long
continue_if_empty: 1, required: 0, allow_empty: 1 - Result: isValid() = 0 The input is less than 5 characters long

Conclusion

The interactions between required, allow_empty & continue_if_empty aren't that complicated, but there's enough complexity that it's worth having a reference handy!

Replacing Pimple in a Slim 3 application

One feature of Slim 3 is that the DI container is loosely coupled to the core framework. This is done in two ways:

  1. The App composes the container instance rather than extending from it.
  2. Internally, App depends on the container implementing the container-interop interface.

You can see this decoupling in the way that you instantiate a Slim 3 application:

$settings = [];
$container = new Slim\Container($settings);
$app = new Slim\App($container);

Slim 3 ships with Pimple by default, but my preference is for Zend\ServiceManager, so I decided to integrate ServiceManager into Slim 3.

RKA\ZsmSlimContainer is the result of this work.

Usage

To use RKA\ZsmSlimContainer, simply add it via composer:

composer require akrabat/rka-slim-zfsm-container

and then update your index.php:

$settings = [];
$container = new RKA\ZsmSlimContainer\Container($settings);
$app = new Slim\App($container);

As you can see, due to the decoupling, the change required is very small and everything still works! The only thing that doesn't work is Pimple\ServiceProviderInterface's register() method as that is tightly coupled to the Pimple container itself.

Implementation

To implement this, I extended Zend\ServiceManager and in the constructor registered Slim's default services. This is easy enough to do with ServiceManager's setFactory method. For example:

    $this->setFactory(
        'callableResolver',
        function ($c) { return new CallableResolver($c); },
        false
    );

In this example, the callableResolver service needs to return a new instance every time it is retrieved from the container, so I pass false in as the third parameter to setFactory as it will default to shared otherwise.

This was the minimum effort required, but I wanted to be able to use code that had been written for Pimple (as far as possible), such as Twig-View. To do this, I implemented ArrayAccess as that's how Pimple works. Implementing offsetGet, offsetExists and offsetUnset was easy as I simply had to call ServiceManager's get, has and unregisterService methods respectively, but offsetSet required a bit more work.

When setting a service via array access, you can assign settings, closures or instances:

$container['foo'] = 'a setting';
$container['bar'] = function($c) { return new Bar($c->get('foo)); }
$container['baz'] = new Bar('another setting');

In ServiceManager, you would use different methods, such as setService or setFactory.

I needed to detect what was intended and call the correct internal method, so I came up with this:

    public function offsetSet($id, $value)
    {
        if (is_object($value)) {
            if ($value instanceof \Closure) {
                return $this->setFactory($id, $value);
            }
            return $this->setService($id, $value);
        }

        if (is_string($value) && class_exists($value)) {
            return $this->setInvokableClass($id, $value);
        }

        return $this->setService($id, $value);
    }

I also support the invokable feature of ServiceManager, where if the value is a class name string, then ServiceManager can instantiate when required. This would be used like this:

$container['qux'] = \My\Fun\Thing::class;

 

All in all, I was pleased at how the decoupled nature of Slim 3 made it easy to replace Pimple with my preferred DI container. I particularly like how it is seamless enough that I can continue to use Twig-View.

ZF2 validator message keys

In Zend Framework 2, if you define your input filter via configuration, then you can override validation messages using a format along the lines of:

<validators>
    <notEmpty>
        <messages>
            <isEmpty>Please provide your telephone number</isEmpty>
        </messages>
    </notEmpty>
</validators>

Setting the message is easy enough, once you have the correct key name. This is a list of all the keys for the standard validators:

Validator Key name Default message
Alnum alnumInvalid Invalid type given. String, integer or float expected
Alnum notAlnum The input contains characters which are non alphabetic and no digits
Alnum alnumStringEmpty The input is an empty string
Alpha alphaInvalid Invalid type given. String expected
Alpha notAlpha The input contains non alphabetic characters
Alpha alphaStringEmpty The input is an empty string
Barcode barcodeInvalid Invalid type given. String expected
Barcode barcodeFailed The input failed checksum validation
Barcode barcodeInvalidChars The input contains invalid characters
Barcode barcodeInvalidLength The input should have a length of %length% characters
Between notBetween The input is not between '%min%' and '%max%', inclusively
Between notBetweenStrict The input is not strictly between '%min%' and '%max%'
Bitwise notAnd The input has no common bit set with '%control%'
Bitwise notAndStrict The input doesn't have the same bits set as '%control%'
Bitwise notXor The input has common bit set with '%control%'
Callback callbackInvalid An exception has been raised within the callback
Callback callbackValue The input is not valid
CreditCard creditcardChecksum The input seems to contain an invalid checksum
CreditCard creditcardContent The input must contain only digits
CreditCard creditcardInvalid Invalid type given. String expected
CreditCard creditcardLength The input contains an invalid amount of digits
CreditCard creditcardPrefix The input is not from an allowed institute
CreditCard creditcardService The input seems to be an invalid credit card number
CreditCard creditcardServiceFailure An exception has been raised while validating the input
Csrf notSame The form submitted did not originate from the expected site
Date dateInvalid Invalid type given. String, integer, array or DateTime expected
Date dateInvalidDate The input does not appear to be a valid date
Date dateFalseFormat The input does not fit the date format '%format%'
DateStep dateInvalid Invalid type given. String, integer, array or DateTime expected
DateStep dateInvalidDate The input does not appear to be a valid date
DateStep dateFalseFormat The input does not fit the date format '%format%'
DateStep dateStepNotStep The input is not a valid step
DateTime datetimeInvalid Invalid type given. String expected
DateTime datetimeInvalidDateTime The input does not appear to be a valid datetime
Digits notDigits The input must contain only digits
Digits digitsStringEmpty The input is an empty string
Digits digitsInvalid Invalid type given. String, integer or float expected
EmailAddress emailAddressInvalid Invalid type given. String expected
EmailAddress emailAddressInvalidFormat The input is not a valid email address. Use the basic format local-part@hostname
EmailAddress emailAddressInvalidHostname '%hostname%' is not a valid hostname for the email address
EmailAddress emailAddressInvalidMxRecord '%hostname%' does not appear to have any valid MX or A records for the email address
EmailAddress emailAddressInvalidSegment '%hostname%' is not in a routable network segment. The email address should not be resolved from public network
EmailAddress emailAddressDotAtom '%localPart%' can not be matched against dot-atom format
EmailAddress emailAddressQuotedString '%localPart%' can not be matched against quoted-string format
EmailAddress emailAddressInvalidLocalPart '%localPart%' is not a valid local part for the email address
EmailAddress emailAddressLengthExceeded The input exceeds the allowed length
Explode explodeInvalid Invalid type given
Float floatInvalid Invalid type given. String, integer or float expected
(Deprecated from ZF 2.4)
Float notFloat The input does not appear to be a float
(Deprecated from ZF 2.4)
GreaterThan notGreaterThan The input is not greater than '%min%'
GreaterThan notGreaterThanInclusive The input is not greater or equal than '%min%'
Hex hexInvalid Invalid type given. String expected
Hex notHex The input contains non-hexadecimal characters
Hostname hostnameCannotDecodePunycode The input appears to be a DNS hostname but the given punycode notation cannot be decoded
Hostname hostnameInvalid Invalid type given. String expected
Hostname hostnameDashCharacter The input appears to be a DNS hostname but contains a dash in an invalid position
Hostname hostnameInvalidHostname The input does not match the expected structure for a DNS hostname
Hostname hostnameInvalidHostnameSchema The input appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'
Hostname hostnameInvalidLocalName The input does not appear to be a valid local network name
Hostname hostnameInvalidUri The input does not appear to be a valid URI hostname
Hostname hostnameIpAddressNotAllowed The input appears to be an IP address, but IP addresses are not allowed
Hostname hostnameLocalNameNotAllowed The input appears to be a local network name but local network names are not allowed
Hostname hostnameUndecipherableTld The input appears to be a DNS hostname but cannot extract TLD part
Hostname hostnameUnknownTld The input appears to be a DNS hostname but cannot match TLD against known list
Iban ibanNotSupported Unknown country within the IBAN
Iban ibanSepaNotSupported Countries outside the Single Euro Payments Area (SEPA) are not supported
Iban ibanFalseFormat The input has a false IBAN format
Iban ibanCheckFailed The input has failed the IBAN check
Identical notSame The two given tokens do not match
Identical missingToken No token was provided to match against
InArray notInArray The input was not found in the haystack
Int intInvalid Invalid type given. String or integer expected
(Deprecated from ZF 2.4)
Int notInt The input does not appear to be an integer
(Deprecated from ZF 2.4)
Ip ipInvalid Invalid type given. String expected
Ip notIpAddress The input does not appear to be a valid IP address
Isbn isbnInvalid Invalid type given. String or integer expected
Isbn isbnNoIsbn The input is not a valid ISBN number
IsInstanceOf notInstanceOf The input is not an instance of '%className%'
IsFloat floatInvalid Invalid type given. String, integer or float expected
(From ZF 2.4)
IsFloat notFloat The input does not appear to be a float
(From ZF 2.4)
IsInt intInvalid Invalid type given. String or integer expected
(From ZF 2.4)
IsInt notInt The input does not appear to be an integer
(From ZF 2.4)
LessThan notLessThan The input is not less than '%max%'
LessThan notLessThanInclusive The input is not less or equal than '%max%'
NotEmpty notEmptyInvalid Value is required and can't be empty
NotEmpty isEmpty Invalid type given. String, integer, float, boolean or array expected
PhoneNumber phoneNumberNoMatch The input does not match a phone number format
PhoneNumber phoneNumberUnsupported The country provided is currently unsupported
PhoneNumber phoneNumberInvalid Invalid type given. String expected
PostCode postcodeInvalid Invalid type given. String or integer expected
PostCode postcodeNoMatch The input does not appear to be a postal code
PostCode postcodeService The input does not appear to be a postal code
PostCode postcodeServiceFailure An exception has been raised while validating the input
Regex regexInvalid Invalid type given. String, integer or float expected
Regex regexNotMatch The input does not match against pattern '%pattern%'
Regex regexErrorous There was an internal error while using the pattern '%pattern%'
Step typeInvalid Invalid value given. Scalar expected
Step stepInvalid The input is not a valid step
StringLength stringLengthInvalid Invalid type given. String expected
StringLength stringLengthTooShort The input is less than %min% characters long
StringLength stringLengthTooLong The input is more than %max% characters long
Uri uriInvalid Invalid type given. String expected
Uri notUri The input does not appear to be a valid Uri

Exclude elements from Zend\Form's getData()

If you need to exclude some elements from a Zend\Form's getData(), the easiest way is to use a validation group.

For example:

class SomeForm extends \Zend\Form\Form implements
    \Zend\InputFilter\InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'name',
            'options' => ['label' => 'Name'],
        ]);
        
        $this->add([
            'name' => 'email',
            'options' => ['label' => 'Email'],
        ]);

        $this->add([
            'name' => 'submit',
            'type' => 'button',
            'options' => [
                'label' => 'Filter',
            ],
        ]);

        $this->setValidationGroup(['name', 'email']);
    }

    public function getInputFilterSpecification()
    {
        // return input filter specification here
    }
}

The call to setValidationGroup() contains an array of all the elements you want to be validated. In this case we list all elements except the submit button. The side-effect is that when you called $form->getData(), only the values for those elements are returned which can sometimes be useful.

Generalise

Rather than having to re-list all the elements again, it's more usually to only want to exclude one or two elements. The easiest way to do this is to extend Zend\Form\Form and override isValid() to set up a validation group based on an option called 'exclude' that you apply to the relevant elements:

<?php
namespace My;

class Form extends \Zend\Form\Form
{
    /**
     * Override isValid() to set an validation group of all elements that do not
     * have an 'exclude' option, if at least one element has this option set.
     *
     * @return boolean
     */
    public function isValid()
    {
        if ($this->hasValidated) {
            return $this->isValid;
        }

        if ($this->getValidationGroup() === null) {
            // Add all non-excluded elements to the validation group
            $validationGroup = null;
            foreach ($this->getElements() as $element) {
                if ($element->getOption('exclude') !== true) {
                    $validationGroup[] = $element->getName();
                }
            }
            if ($validationGroup) {
                $this->setValidationGroup($validationGroup);
            }
        }
        
        return parent::isValid();
    }
}
?>

Then, to exclude an element, we extend SomeForm from \My\Form and just add a new 'exclude' option to the elements we wish to exclude. For example:

// in SomeForm::init()
$this->add([
    'name' => 'submit',
    'type' => 'button',
    'options' => [
        'label' => 'Filter',
        'exclude' => true,
    ],
]);

and this element will be automatically excluded when you call $form->getData().

Validating JSON with ZF2's Zend\Validator

Let's say that you have an admin form where the user can enter JSON and you'd like to validate that the JSON parses before allowing the user to submit. To do this, you can use the rather excellent jsonlint project by Jordi Boggiano. Obviously, add it via Compser :)

Usage is simple:

use Seld\JsonLint\JsonParser;
use Seld\JsonLint\ParsingException;

$parser = new JsonParser();

$result = $parser->lint($json);
if ($result instanceof ParsingException) {
    // $json is invalid JSON
}

We can wrap this up into a Zend Framework 2 validator quite easily:

<?php

namespace RKA\Validator;

use Zend\Validator\AbstractValidator;
use Seld\JsonLint\JsonParser;
use Seld\JsonLint\ParsingException;


class Json extends AbstractValidator
{
    const INVALID      = 'jsonInvalid';

    /**
     * Json parser
     *
     * @var \Seld\JsonLint\JsonParser
     */
    protected static $parser = null;

    /**
     * Validation failure message template definitions
     *
     * @var array
     */
    protected $messageTemplates = array(
        self::INVALID      => "Json is invalid: '%reason%'",
    );

    /**
     * Additional variables available for validation failure messages
     *
     * @var array
     */
    protected $messageVariables = array(
        'reason' => 'reason',
    );

    /**
     * @var string
     */
    protected $reason;

    /**
     * Returns true if and only if $value is valid JSON
     *
     * @param  string $value
     * @return bool
     */
    public function isValid($value)
    {
        $parser = new JsonParser();

        $result = $parser->lint($value);

        if ($result instanceof ParsingException) {
            $this->reason = $result->getMessage();
            $this->error(self::INVALID);
            return false;
        }

        return true;
    }
}

Simply add RKA\Validator\Json to the input filter of the element in question and we're done.

Using ZF2 Forms with Twig

Following on from looking at how to integrate Zend Framework 2 forms into Slim Framework, let's look at the changes required if you also happen to want to use Twig.

When it comes to rendering the form, we would want our template to look like this:

    <form method="POST" role="form">
        <div class="form-group">
            {{ formRow(form.get('email')) }}
        </div>
        {{ formElement(form.get('submit')) }}
    </form>

The ZF2 view helpers, formRow and formElement now look like Twig functions, however we don't want to have to rewrite all our ZF2 view helpers into Twig. Fortunately, Twig supports the concept of a undefined function callback is called whenever Twig encounters a function that it doesn't know how to call. We can use this to proxy through to the Zend\View system and get it to render the ZF2 view helpers.

Let's look at how we do this.

Starting with the work in the previous article, we merely need to change the view layer. That is, we don't need a custom PHP view (\RKA\View), but instead use the Slim-Views Twig component.

To add Twig to a Slim project we update composer.json and add "twig/twig": "1.16.*" and "slim/views": "0.1.*" to the require section and then run composer update.

We then update index.php to add the view class when instantiating the Slim and configure it.:

$app = new \Slim\Slim([
    'view' => new \Slim\Views\Twig()
]);

// Configure Twig
$view = $app->view();
$view->parserOptions = [
    'debug' => true,
    'cache' => false,
];
$view->parserExtensions = array(
    new \Slim\Views\TwigExtension(),
);

Now, whenever we call $app->render(), Twig will be used. The view templates have a .twig extension, so we rename home.php to home.twig and it's rendered like this:

$app->render('home.twig', [
    'form' => $form
]);

To integrate the ZF2 Form view helpers without having to rewrite them all as Twig extensions, we just need to register an undefined callback function that uses Zend Framework 2's PhpRenderer to render the view helper.

This is done with a few lines in index.php:

$viewHelperManager = $app->serviceManager->get('ViewHelperManager');
$renderer = new \Zend\View\Renderer\PhpRenderer();
$renderer->setHelperPluginManager($viewHelperManager);

$view->getInstance()->registerUndefinedFunctionCallback(
    function ($name) use ($viewHelperManager, $renderer) {
        if (!$viewHelperManager->has($name)) {
            return false;
        }

        $callable = [$renderer->plugin($name), '__invoke'];
        $options  = ['is_safe' => ['html']];
        return new \Twig_SimpleFunction(null, $callable, $options);
    }
);

Let's break this down.

Firstly, we create a PhpRenderer and set the helper plugin manager to ViewHelperManager that's already defined in the service manager.

We can access the underlying Twig_Environment object using $view->getInstance() and then call registerUndefinedFunctionCallback with a closure for our callback.

The callback code is this bit:

    function ($name) use ($viewHelperManager, $renderer) {
        if (!$viewHelperManager->has($name)) {
            return false;
        }

        $callable = [$renderer->plugin($name), '__invoke'];
        $options  = ['is_safe' => ['html']];
        return new \Twig_SimpleFunction(null, $callable, $options);
    }

Within the callback we check the ViewHelperManager to see if we have a view helper that we can call. If we don't then we return false so that Twig can try a difference undefined function callback in its list.

If we do know about this view helper, then we instantiate a Twig_SimpleFunction that will call the __invoke method of the view helper which we retrieve via the plugin method of the renderer. Note that we also tell Twig that our view helper is safe to use with HTML so it doesn't escape the HTML tags created by the view helper.

That's all there is to it.

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

Sending attachments in multipart emails with Zend\Mail

I've written before about how to send an HTML email with a text alternative in Zend\Mail, but recently needed to send an attachment with my multipart email.

With help from various sources on the Internet, this is how to do it.

use Zend\Mail\Message;
use Zend\Mime\Message as MimeMessage;
use Zend\Mime\Part as MimePart;
use Zend\Mime\Mime;
use Zend\Mail\Transport\Sendmail;

function sendEmail($to, $from, $subject, $html, $text, $attachments = null)
{
    $message = new Message();

    $message->addTo($to);
    $message->addFrom($from);
    $message->setSubject($subject);

    // HTML part
    $htmlPart           = new MimePart($html);
    $htmlPart->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
    $htmlPart->type     = "text/html; charset=UTF-8";

    // Plain text part
    $textPart           = new MimePart($text);
    $textPart->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
    $textPart->type     = "text/plain; charset=UTF-8";

    $body = new MimeMessage();
    if ($attachments) {
        // With attachments, we need a multipart/related email. First part
        // is itself a multipart/alternative message        
        $content = new MimeMessage();
        $content->addPart($textPart);
        $content->addPart($htmlPart);

        $contentPart = new MimePart($content->generateMessage());
        $contentPart->type = "multipart/alternative;\n boundary=\"" .
            $content->getMime()->boundary() . '"';

        $body->addPart($contentPart);
        $messageType = 'multipart/related';

        // Add each attachment
        foreach ($attachments as $thisAttachment) {
            $attachment = new MimePart($thisAttachment['content']);
            $attachment->filename    = $thisAttachment['filename'];
            $attachment->type        = Mime::TYPE_OCTETSTREAM;
            $attachment->encoding    = Mime::ENCODING_BASE64;
            $attachment->disposition = Mime::DISPOSITION_ATTACHMENT;

            $body->addPart($attachment);
        }

    } else {
        // No attachments, just add the two textual parts to the body
        $body->setParts(array($textPart, $htmlPart));
        $messageType = 'multipart/alternative';
    }

    // attach the body to the message and set the content-type
    $message->setBody($body);
    $message->getHeaders()->get('content-type')->setType($messageType);
    $message->setEncoding('UTF-8');

    $transport = new Sendmail();
    $transport->send($message);
}

Let's look at some key parts.

The text of the email

The HTML and plain text parts of the email are treated the same. We create a Zend\Mime\Part and set the encoding and type. For HTML, it looks like this:

    $htmlPart           = new MimePart($html);
    $htmlPart->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
    $htmlPart->type     = "text/html; charset=UTF-8";

Note that if your text is UTF-8, then you must set this in the part's type, as it is not passed through from the message level when set there.

Creating the multipart/alternative

If we have attachments, then we need to create a new multipart/alternative Zend\Mime\Message for the first part which contains the HTML and plain text parts:

        $content = new MimeMessage();
        $content->addPart($textPart);
        $content->addPart($htmlPart);

        $contentPart = new MimePart($content->generateMessage());
        $contentPart->type = "multipart/alternative;\n boundary=\"" .
            $content->getMime()->boundary() . '"';

        $body->addPart($contentPart);
        $messageType = 'multipart/related';

The important section here is that $contentPart is created with the generated message from the $content object which is the text of the two parts with the correct mime sections around them. We then need to set the type to mulipart/alternative and also define the boundary as the auto-generated identifier from $content.

Adding attachments

Attachments are similar to adding text:

            $attachment = new MimePart($thisAttachment['file_data']);
            $attachment->filename    = $thisAttachment['filename'];
            $attachment->type        = Mime::TYPE_OCTETSTREAM;
            $attachment->encoding    = Mime::ENCODING_BASE64;
            $attachment->disposition = Mime::DISPOSITION_ATTACHMENT;

            $body->addPart($attachment);

Again, you we create a Zend\Mime\Part for each attachment containing the raw file data. We also need to set the filename property along with the type, encoding and disposition.

Setting content-type

If we don't have attachments, then the message type is 'multipart/alternative', so that's set in the other half of the if/else. We can then add the body to the message and set its content-type:

    $message->setBody($body);
    $message->getHeaders()->get('content-type')->setType($messageType);
    $message->setEncoding('UTF-8');

We're all done, so we can send the email.

In this code, I'm using the Sendmail transport, but obviously Zend\Mail supports other options, including SMTP.

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.

Globally overriding validation messages for ZF2 forms

One thing that I always do when creating a Zend Framework 2 form is override the validation messages for a number of validators – EmailAddress in particular.

I recently decided that I should probably sort this one out once and be done with it. Turns out that it's quite easy assuming that you use the FormElementManger to instantiate your forms.

All that I need to do is create my own validator classes that extend the Zend Framework ones and just set new message templates. This is what EmailAddress looks like:

namespace RKA\Validator;

use Zend\Validator\EmailAddress as BaseEmailAddress;

class EmailAddress extends BaseEmailAddress
{
    protected $messageTemplates = array(
        self::INVALID            => "Invalid type given. String expected",
        self::INVALID_FORMAT     => "Invalid email address",
        self::INVALID_HOSTNAME   => "Invalid email address",
        self::INVALID_MX_RECORD  => "Invalid email address",
        self::INVALID_SEGMENT    => "Invalid email address",
        self::DOT_ATOM           => "Invalid email address",
        self::QUOTED_STRING      => "Invalid email address",
        self::INVALID_LOCAL_PART => "Invalid email address",
        self::LENGTH_EXCEEDED    => "Email address is too long",
    );
}

To ensure that the ValidatorPluginManager uses the new validator in place of the default one, we override in module.config.php:

    'validators' => [
        'invokables' => [
            'email-address' => 'RKA\Validator\EmailAddress',
            'string-length' => 'RKA\Validator\StringLength',
            // etc.
        ],
    ],

Now, whenever you create a form with input filter that uses FormAbstractServiceFactory or via a config array with the Form\Factory as set up in my last post, then the new validator is picked up (assuming you use the short name).

If you create your forms via a class, then you should use the FormElementManager like this.

Firstly, create your form:

namespace RKA\Form;

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

class ExampleForm extends Form implements InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'email',
            'type' => 'text',
            'options' => [
                'label' => 'Email address',
            ],
            'attributes' => [
                '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'],
                ],
            ],
        ];
    }
}

Note that in getInputFilterSpecification we use the service manager key names for each validator name. The ones that are shipped with ZF2 are listed in the ValidatorPluginManager.

Now register it with the FormElementManager in module.config.php:

    'form_elements' => [
        'invokables' => [
            'RKA\ExampleForm'  => 'RKA\Form\ExampleForm',
        ],
    ],

You can then instantiate it in your controller via the main service locator:

    // in my controller
    public function getExampleForm()
    {
        $formManager = $this->getServiceLocator()->get('FormElementManager');
        return $formManager->get('RKA\ExampleForm');
    }

In addition to forms, you can also register your own fieldsets and elements under the 'form_elements' key and then use them within your forms.