If you developed any application using ZF2 you may become frustrated of the tedious work of creating boilerplate code for handling common tasks like a simple form which will be validated then saved in a database. The Zend manual recommends creating a table class, an entity class a form class, a validator class, along with the common MVC prerequisites like controller, action, view plus the Zend config stuff for paths, etc. Coming from a convention over configuration world of cakePHP this seems ridiculous.

Here is how you can speed up your workflow while still benefit from all enterprise features and flexibility you like in ZF2:

Annotations are special docblocks which store metadata in PHP classes. These information are available at runtime, unlike regular comment blocks, which are not. Note the difference:

/* Multiline comment
   which is discarded by the PHP interpreter
*/

vs

/** Docblock
 *  which is cached by the PHP interpreter and made available
 *  with the help of the reflection API
 */

There is no support in the PHP core for annotations, but there are some engines using the reflection API which can be used successfully with annotations. Common choices are the one present in symfony and phpdocumentor. ZF2 includes support for annotation by using it’s AnnotationBuilder class and doctrine/common (a symfony package).

You can add the required package to your project by using composer:
Edit composer.json

"require": {
        "php": ">=5.4",
        "zendframework/zendframework": "2.3",
        "doctrine/common":"2.2.3"
    }

then run
php composer.phar install

I am using a TableGateway factory to return a generic table instance or custom table instances if they exists. The table service will take care of the CRUD operations and hydrate the result set.

We start with a base entity:

form)) {
            $builder    = new AnnotationBuilder();
            $this->form = $builder->createForm($this);
            $this->form->bind($this);
        }

        return $this->form;
    }

    /**
     * Hydrates the entity using the ClassMethodsExtendedHydrator
     *
     * @param array $data
     * @param null $object
     * @return object
     */
    public function hydrate(array $data)
    {
        if ($this->hydratorClass === null) {
            $this->hydratorClass = new ClassMethodsExtendedHydrator();
        }

        return $this->hydratorClass->hydrate($data, $this);
    }

    /**
     * Extract an array representation of the object
     *
     * @return array
     */
    public function extract()
    {
        if ($this->hydratorClass === null) {
            $this->hydratorClass = new ClassMethodsExtendedHydrator();
        }

        $composite = new FilterComposite();
        $composite->addFilter(
            "messages",
            new MethodMatchFilter("getMessages"),
            FilterComposite::CONDITION_AND
        );
        $composite->addFilter(
            "form",
            new MethodMatchFilter("getForm"),
            FilterComposite::CONDITION_AND
        );

        $this->hydratorClass->addFilter("excludes", $composite, FilterComposite::CONDITION_AND);

        return $this->extractRecursive($this);
    }

    /**
     * Hydrates & validates the object
     *
     * @return bool true for no validation errors, false otherwise
     */

    public function validate($data = null)
    {
        $this->setFormData($data);

        return $this->getForm()->getInputFilter()->isValid();
    }

    /**
     * Get validation messages, if any
     * Must be called after validate
     *
     * @return array|\Traversable
     */
    public function getMessages()
    {
        return $this->getForm()->getInputFilter()->getMessages();
    }

    /**
     * Returns all of the entity's virtual field, a certain virtual field value, or null if the required virtual
     * field cannot be found
     * @param $name
     * @return mixed
     */
    public function getVF($name = null)
    {
        if ($name === null) {
            return $this->VF;
        } else if (isset($this->VF[$name])) {
            return $this->VF[$name];
        }

        return null;
    }

    /**
     * Sets a virtual field's value or adds it if it doesn't exist
     * @param $name
     * @param $value
     */
    public function setVF($name, $value)
    {
        $this->VF[$name] = $value;
    }

    /**
     * Hydrate object with $data
     *
     * @param array|Object $data
     *
     */
    protected function setFormData($data)
    {
        if (empty($data)) {
            $data = $this->extract();
        }

        if (is_object($data)) {
            $data = get_object_vars($data);
        }

        $this->getForm()->getInputFilter()->setData($data);
    }

    /**
     * @param $object
     *
     * @return array
     */
    protected function extractRecursive($object)
    {
        $result    = array();

        $extracted = is_object($object) && $object instanceof Entity ? $this->hydratorClass->extract($object) : $object;

        foreach ($extracted as $key => $val) {
            $val = (is_object($val) && $val instanceof Entity) ? $this->extractRecursive($val) : $val;
            if (!is_object($val)) {
                $result[$key] = $val;
            }
        }

        return $result;
    }
}

To demonstrate, I will use two entities: User and Address. User contains another object called Address.
I’ve put some examples to annotate validations, filters and define how the form looks.
As you can see, the validators, filters and their options are the one shipped with Zend Framework.

id;
    }

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return mixed
     */
    public function getUserName()
    {
        return $this->UserName;
    }

    /**
     * @param mixed $UserName
     */
    public function setUserName($UserName)
    {
        $this->UserName = $UserName;
    }

    /**
     * @return mixed
     */
    public function getEmail()
    {
        return $this->Email;
    }

    /**
     * @param mixed $Email
     */
    public function setEmail($Email)
    {
        $this->Email = $Email;
    }

    /**
     * @return mixed
     */
    public function getPassword()
    {
        return $this->Password;
    }

    /**
     * @param mixed $Password
     */
    public function setPassword($Password)
    {
        $this->Password = $Password;
    }

    /**
     * @return mixed
     */
    public function getConfirmPassword()
    {
        return $this->ConfirmPassword;
    }

    /**
     * @param mixed $ConfirmPassword
     */
    public function setConfirmPassword($ConfirmPassword)
    {
        $this->ConfirmPassword = $ConfirmPassword;
    }

    /**
     * @return mixed
     */
    public function getGender()
    {
        return $this->Gender;
    }

    /**
     * @param mixed $Gender
     */
    public function setGender($Gender)
    {
        $this->Gender = $Gender;
    }

    /**
     * @return mixed
     */
    public function getAddress()
    {
        return $this->Address;
    }

    /**
     * @param mixed $Address
     */
    public function setAddress($Address)
    {
        $this->Address = $Address;
    }
}

Address Entity:

Address1;
    }

    /**
     * @param mixed $Address1
     */
    public function setAddress1($Address1)
    {
        $this->Address1 = $Address1;
    }

    /**
     * @return mixed
     */
    public function getCounty()
    {
        return $this->County;
    }

    /**
     * @param mixed $County
     */
    public function setCounty($County)
    {
        $this->County = $County;
    }

    /**
     * @return mixed
     */
    public function getCity()
    {
        return $this->City;
    }

    /**
     * @param mixed $City
     */
    public function setCity($City)
    {
        $this->City = $City;
    }

    /**
     * @return mixed
     */
    public function getZipCode()
    {
        return $this->ZipCode;
    }

    /**
     * @param mixed $ZipCode
     */
    public function setZipCode($ZipCode)
    {
        $this->ZipCode = $ZipCode;
    }
}

Basic usage

Controller action

You obtain the form from the entity with an already bound object, then work this the form as usual.

Extracting an array representation on the object including composed objects:

$user->extract();
    public function testAction()
    {
        $user = new User();
        $form = $user->getForm();

        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());

            // this is how you remove a rule
            //$form->getInputFilter()->remove('UserName');
            if ($form->isValid()) {
                $data = $form->getData();
                // save data. $data is an object of type User now
                $this->getTable('User')->insert($data->extract());
            } else {
                $this->flashMessenger()->setNamespace('error')->addMessage($form->getMessages());
            }
        }

        return array('form' => $form);
    }b

View – test.phtml

flashMessages();

$form->prepare();

echo $this->form()->openTag($form);

echo $this->formRow($form->get('UserName'));
echo $this->formRow($form->get('Email'));
echo $this->formRow($form->get('Password'));
echo $this->formRow($form->get('ConfirmPassword'));
echo $this->formRow($form->get('Gender'));

echo $this->formCollection($form->get('Address'));
?>
    
    

form()->closeTag($form);
?>

Here is a basic method to populate the entities with data:

$user = new User();

$user->setUserName('johndoe');
$user->setEmail('[email protected]');

$address = new Address();
$address->setAddress1("My House");
$address->setCounty(1);

$user->setAddress($address);

You can hydrate providing an object or an array

$user->hydrate(array(
            'DepartmentId' => 1,
            'ContactDetail' => array('Gender' => 'Male')
        ));

Then you can validate your entity like this:

// validate internal data
if (!$user->validate()) {
    Debug::dump($user->getMessages());
}

// or hydrate and validate from array or object
$user->validate($data);

A very nice feature is to automatically hydrate the composed objects as well when using queries with joins. This can be done automatically if you attach this custom hydrator to the result set prototype and prepare the query for this behavior.

Any composed objects will be populated. Other joins will populate a special property called VF (from virtual fields). You can get virtual fields later by using getVF($name = null).

For example if we join ContactDetail it will populate the properties from ContactDetail as well and if we have also an aggregate expression like COUNT(*) then you will find this value in the virtual fields. The purpose of virtual fields is to store any data outside the scope of the entity.

ClassMethodsExtendedHydrator:

 $value) {
            $propertyFqn = $objectClass . '::$' . $property;
            $currentPosition = strpos($property, '__');

            if (!isset($this->hydrationMethodsCache[$propertyFqn])) {
                $setterName = 'set' . ucfirst($this->hydrateName($property, $data));

                $this->hydrationMethodsCache[$propertyFqn] = is_callable(array($object, $setterName))
                    ? $setterName
                    : false;
            }

            if ($this->hydrationMethodsCache[$propertyFqn]) {
                $object->{$this->hydrationMethodsCache[$propertyFqn]}($this->hydrateValue($property, $value, $data));
            } else {

                // if there is no setter for the current key it will be placed under VF or a child entity if that exists and can be used
                if ($currentPosition > 0) {
                    $currentAlias = substr($property, 0, $currentPosition);
                    $property = substr($property, $currentPosition + 2);
                } else {
                    $currentAlias = 'VF';
                }

                $virtualFields[$currentAlias][$property] = $value;
            }
        }

        if (isset($virtualFields['VF'])) {
            foreach ($virtualFields['VF'] as $key => $value) {
                $object->setVF($key, $value);
            }
            unset($virtualFields['VF']);
        }

        foreach ($virtualFields as $entityName => $entityData) {
            $setterName = 'set' . $entityName;
            if (is_callable(array($object, $setterName))) {
                $object->$setterName($entityData);
            } else {
                $object->setVF($entityName, $entityData);
            }
        }

        return $object;
    }
}

Then prepare the query to format the column name in order to let the hydrator to detect it.

// ...
/**
     * Helper method that adds a table to a join query
     * @param array $table
     * @param Select $select
     * @return Select
     */
    protected function addTable($table, $select)
    {
        $prototypeName   = $this->getTableGateway()->getPrototype();
        $entityPrototype = null;
        if ($prototypeName) {
            $obj             = new $prototypeName;
            $entityPrototype = is_subclass_of($obj, 'Utils\Model\Entity');
        }

        $tableName    = isset($table['alias'])   ? $table['alias']   : $table['table_name'];
        $tableColumns = isset($table['columns']) ? $table['columns'] : array();

        /**
         * if the prototype is an entityV2 we use __ as an alias for the columns that we get from
         * a join
         */
        if ($tableColumns && $entityPrototype) {
            foreach ($tableColumns as $key => $value) {
                $tableColumns[$tableName . '__' . $value] = $value;
                unset($tableColumns[$key]);
            }
        }

        return $select->join(
            array(
                $tableName => $table['table_name']
            ),
            $table['join_condition'],
            $tableColumns,
            (isset($table['join']) ? $table['join'] : null)
        );
    }

For further reference, here is a list of available annotations:

  • AllowEmpty: mark an input as allowing an empty value. This annotation does not require a value.
  • Attributes: specify the form, fieldset, or element attributes. This annotation requires an associative array of
    values, in a JSON object format: @Attributes({"class":"zend_form","type":"text"}).
  • ComposedObject: specify another object with annotations to parse. Typically, this is used if a property
    references another object, which will then be added to your form as an additional fieldset. Expects a string
    value indicating the class for the object being composed @ComposedObject("Namespace\Model\ComposedObject") or an array to compose a collection: @ComposedObject({
    "target_object":"Namespace\Model\ComposedCollection", "is_collection":"true", "options":{"count":2}})

    target_object is the element to compose, is_collection flags this as a collection and options can take an array
    of options to pass into the collection.
  • ErrorMessage: specify the error message to return for an element in the case of a failed validation. Expects a
    string value.
  • Exclude: mark a property to exclude from the form or fieldset. This annotation does not require a value.
  • Filter: provide a specification for a filter to use on a given element. Expects an associative array of values,
    with a “name” key pointing to a string filter name, and an “options” key pointing to an associative array of
    filter options for the constructor: @Filter({"name": "Boolean", "options": {"casting":true}}). This annotation
    may be specified multiple times.
  • Flags: flags to pass to the fieldset or form composing an element or fieldset; these are usually used to
    specify the name or priority. The annotation expects an associative array: @Flags({"priority": 100}).
  • Hydrator: specify the hydrator class to use for this given form or fieldset. A string value is expected.
  • InputFilter: specify the input filter class to use for this given form or fieldset. A string value is expected.
  • Input: specify the input class to use for this given element. A string value is expected.
  • Instance: specify an object class instance to bind to the form or fieldset.
  • Name: specify the name of the current element, fieldset, or form. A string value is expected.
  • Object: specify an object class instance to bind to the form or fieldset.
    (Note: this is deprecated in 2.4.0; use Instance instead.)
  • Options: options to pass to the fieldset or form that are used to inform behavior – things that are not
    attributes; e.g. labels, CAPTCHA adapters, etc. The annotation expects an associative array: @Options({"label":
    "Username:"})
    .
  • Required: indicate whether an element is required. A boolean value is expected. By default, all elements are
    required, so this annotation is mainly present to allow disabling a requirement.
  • Type: indicate the class to use for the current element, fieldset, or form. A string value is expected.
  • Validator: provide a specification for a validator to use on a given element. Expects an associative array of
    values, with a “name” key pointing to a string validator name, and an “options” key pointing to an associative
    array of validator options for the constructor: @Validator({"name": "StringLength", "options": {"min":3, "max":
    25}})
    . This annotation may be specified multiple times.

Share This:

Join the Conversation

1 Comment

Leave a comment

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