WARNING: This post was written a long time ago. It exists here as a record and may not reflect my current views or opinions. Also, any code, technologies or examples may be out of date.

Recently I started playing with Zend Framework 2 Form Annotations. These certainly make building forms much simpler. If you have not heard about this yet see Matthew Weier O’Phinney’s post and the ZF2 Documentation</a>.

After getting it up and running, I found out about about the @ComposedObject annotation. It seemed very useful, however, it took a bit of fiddling to get it up and running. Here’s how I did it.

The @ComposedObject annotation lets you create a fieldset inside a form which was created from one annotated class, by using the form annotations from another class.

To set this up I created 2 classes User and Address:

<?php

namespace Album\Model;

use Zend\Form\Annotation as Form;
use Album\Model\Address;

/**
 * @Form\Hydrator("Zend\Stdlib\Hydrator\ArraySerializable")
 * @Form\Name("address")
 */
class User
{
    /**
     * @Form\Attributes({"type":"text" })
     * @Form\Options({"label":"Name:"})
     */
    public $name;
    
    /**
     * @Form\Type("Zend\Form\Element\Email")
     * @Form\Options({"label":"Email:"})
     */
    public $email;
    
    /**
     * @Form\ComposedObject("Album\Model\Address")
     */
    public $address;
}
<?php

namespace Album\Model;

use Zend\Form\Annotation as Form;

/**
 * @Annotation\Hydrator("Zend\Stdlib\Hydrator\ArraySerializable")
 * @Annotation\Name("address")
 */
class Address
{
    /**
     * @Form\Attributes({"type":"text" })
     * @Form\Options({"label":"Line 1:"})
     */
    public $line1;
    
    /**
     * @Form\Attributes({"type":"text" })
     * @Form\Options({"label":"Line 2:"})
     */
    public $line2;

    /**
     * @Form\Attributes({"type":"text" })
     * @Form\Options({"label":"City:"})
     */
    public $city;
    
    /**
     * @Form\Attributes({"type":"text" })
     * @Form\Options({"label":"Post Code/Zip:"})
     */
    public $postcode;
}

The Address class is pretty straight forward, however, if you look at the annotations for the User class, you will see that the address property is annotated as being a ComposedObject of type Album\Model\Address.

In the controller you can now add the code to create the form:

<?php

// ...

public function formtestAction()
{
    // Create the User object and fill it with some test data
    $user = new User;
    $user->name = "Tom";
    $user->email = "tom@abc.xyz";
    
    $address = new Address;
    $address->line1 = "My House";
    $address->line2 = "Something Street";
    $address->city = "Great Town";
    $address->postcode = "AB23 4CD";
        
    $user->address = $address;
    
    // Generate the form from the User class Annotations
    $builder = new AnnotationBuilder();
    $form = $builder->createForm($user);
        
    // Add a submit button to the form
    $form->add(array(
        'name'      => 'submit',
        'attributes'=> array(
                'type'  => 'submit',
                'value' => 'Go',
                'id'    => 'submitbutton',
        ),
    ));
        
    // Bind the User object to the form
    $form->bind($user);
    
    // Handle form submittions
    $request = $this->getRequest();
    if ($request->isPost()) {
        $form->setData($request->getPost());
        if ($form->isValid()) {
            print_r($user);
        }
    }
        
    return array('form' => $form);
}

// ...
    

And a view script to display it:

<?php
$title = 'User form';
$this->headTitle($title);
?>
<h1><?php echo $this->escapeHtml($title); ?></h1>

<?php
$form = $this->form;
$form->setAttribute('action', $this->url('album', array('action' => 'formtest')));
echo $this->form()->openTag($form);
?>
    <?php echo $this->formRow($form->get('name')); ?>
    <?php echo $this->formRow($form->get('email')); ?>
    <fieldset>
        <legend>Address:</legend>
        <?php echo $this->formCollection($form->get('address')); ?>
    </fieldset>
    <?php echo $this->formInput($form->get('submit')); ?>
<?php echo $this->form()->closeTag($form); ?>

Note that a whole fieldset can be displayed with the formCollection() view helper.

At this point, when you view this action in the browser the form fields should all be present. However, all the fields will be empty rather than containing the data from the object. The reason for this is the User and Address classes have been annotated to use the ArraySerializable hydrator which requires getArrayCopy and exchangeArray methods to be defined.

The reason for choosing ArraySerializable instead of another hydrator, is that, in order for fieldsets to be hydrated they need the extracted data from the composed object to be presented as a sub-array. The other hydrators will just return the object as an element in the array, but ArraySerializable lets us define the array that is returned.

For Address these 2 methods are simple since it just contains simple fields:

<?php

class Address
{

    // ... member definitions & annotations

    public function getArrayCopy()
    {
        return get_object_vars($this);
    }
    
    function exchangeArray($data)
    {
        $this->line1    = (isset($data['line1'])) ? $data['line2'] : null;
        $this->line2    = (isset($data['line2'])) ? $data['line1'] : null;
        $this->city     = (isset($data['city'])) ? $data['city'] : null;
        $this->postcode = (isset($data['postcode'])) ? $data['postcode'] : null;
    }
}

However, for User we need to call the methods of Address to get back and write a serialized sub array:

<?php

class User
{

    // ... member definitions & annotations

    public function getArrayCopy()
    {
        $data = get_object_vars($this);
        
        if (is_object($this->address)) {
            $data["address"] = $this->address->getArrayCopy();
        }
        
        return $data;
    }
    
    function exchangeArray($data)
    {
        $this->name     = (isset($data['name'])) ? $data['name'] : null;
        $this->email    = (isset($data['email'])) ? $data['email'] : null;
        
        if (isset($data["address"]))
        {
            if (!is_object($this->address)) $this->address = new Address;
            $this->address->exchangeArray($data["address"]);
        }
    }
}

And now it should all be working!