Bind Zend_Form val­ues to the model. Part 2 (Array-type properties)

In my pre­vi­ous post I’ve added a simple bind­ing func­tion­al­ity to Zend_Form. But once I’ve imple­men­ted it in real world with Doc­trine PHP ORM as my BLL, I found that it can’t work seam­lessly with my model’s prop­er­ties of array type. In fact you’ll feel the same prob­lem with Zend_Db_Table based mod­els if you have fields rep­res­ent­ing arrays. The simplest solu­tion, as I had only one such-typed prop­erty in my model, was to over­ride My_Form::fill() method in con­crete form. But as we speak about reusable code I imple­men­ted it in My_Form dir­ectly…

But before I’ll start let’s review pre­vi­ous code and note some changes. First of all $modelProperty of bind() method now can be string or an array. Array type of $modelProperty is used for array typed properties:

class My_Form extends Zend_Form
{
    // ...
    /**
     * Add binding to the registry
     *
     * If you want to mangle value which would be passed to model's property
     * you can specify $callback function which will be called with value of
     * element passed as first argument and it's return value will be then
     * used instead of form element's value.
     *
     * Example:
     * <code>
     * function prepareActorsList($value)
     * {
     *     return serialize(explode(';', $value));
     * }
     * </code>
     *
     * If $modelProperty is an array, first element of this array will be used
     * as property name and second as path to build an array. So for example:
     * <code>
     * $form->bind('addr_1', array('address', array('legal', '0')));
     * $form->bind('addr_2', array('address', 'postal'));
     * $form->bind('phone', 'phone');
     * </code>
     * will bind:
     *     - 'addr_1' to <code>$model->address['legal'][0]</code>
     *     - 'addr_2' to <code>$model->address['postal']</code>
     *     - 'phone' to <code>$model->phone</code>
     *
     * @see    My_Form::_setValueByPath()
     * @param  string $elementName
     * @param  string|array $modelProperty
     * @param  mixed $callback (optional)
     * @return My_Form Self-reference
     */
    public function bind($elementName, $modelProperty, $callback = null)
    {
        $this->_bindings[$elementName] = array($modelProperty, $callback);
        return $this;
    }
    // ...
}

Also I have changed addElement() to reflect bind() changes:

class My_Form extends Zend_Form
{
    // ...
    /**
     * Wrapper to allow specify model binding.
     *
     * @see    My_Form::bind()
     * @see    Zend_Form::addElement()
     * @param  string|Zend_Form_Element $element
     * @param  string $name
     * @param  array|Zend_Config $options
     * @param  string|array $modelProperty
     * @param  mixed $callback (optional)
     * @return My_Form Self-reference
     */
    public function addElement($element, $name = null, $options = null,
                               $modelProperty = null, $callback = null)
    {
        parent::addElement($element, $name, $options);

        if (null !== $modelProperty) {
            $this->bind($name, $modelProperty, $callback);
        }

        return $this;
    // ...
}

Now, if I have an address array prop­erty in model, and I want to bind some ele­ment to $model->address['postal'], and another ele­ment to be bounded to $model->address['legal']['first'] I can bind it with:

$form->bind('address', array('address', 'postal'));
$form->bind('address', array('address', array('legal', 'first')));

We need to make fill() (and cor­res­pond­ing getModelData() for Zend_Db_Table) method able to use such bind paths. To do so I have cre­ated a helper to set val­ues by such path. Stand­ard array_merge() and array_merge_recursive() can’t be used. As first one merge only first level and second one do not merge the way I needed. Here it is:

class My_Form extends Zend_Form
{
    // ...
    /**
     * Sets value of node specified by $path
     *
     * Path is an array containing node name and next node as an array (for next
     * nesting level) or as a string if next node is last one, e.g.:
     * <code>
     * $path = array('parent', array('child', 'grandchild'));
     * $arr = $this->_setValueByPath(array(), $path);
     * </code>
     *
     * equals to:
     * <code>
     * $arr['parent']['child']['grandchild'] = null;
     * </code>
     *
     * @param  mixed $array Base array to set in
     * @param  mixed $path Path definition
     * @param  mixed $value (optional) Value of last node
     * @return array
     */
    protected function _setValueByPath($array, $path, $value = null)
    {
        list($key, $path) = (array) $path;
        $array[$key] = (null !== $path)
                     ? $this->_setValueByPath($array[$key], $path, $value)
                     : $value;
        return $array;
    }
    // ...
}

And finally here are mod­i­fied ver­sions of fill() and getModelData():

class My_Form extends Zend_Form
{
    // ...
    /**
     * Fills $model's fields with filtered values from form.
     *
     * @param  Doctrine_Record $model
     * @return Doctrine_Record
     */
    public function fill(Doctrine_Record $model)
    {
        foreach ($this->getElements() as $name => $element) {
            if ( ! array_key_exists($name, $this->_bindings)) {
                continue;
            }

            list($prop, $callback) = $this->_bindings[$name];

            $value  = (null !== $callback)
                    ? call_user_func($callback, $element->getValue())
                    : $element->getValue();

            if (is_array($prop)) {
                list($prop, $path) = $prop;
                $base  = (is_array($model->$prop)) ? $model->$prop : array();
                $value = $this->_setValueByPath($base, $path, $value);
            }

            $model->$prop = $value;
        }

        return $model;
    }

    /**
     * Prepares an array of data suitable for Zend_Db_Table model
     *
     * Same as {@link My_Form::fill()} but creates an array with form elements'
     * data suitable for passing to the methods of {@link Zend_Db_Table} like
     * {@link Zend_Db_Table::insert()} etc.
     *
     * @return array
     */
    public function getModelData()
    {
        $data = array();

        foreach ($this->getElements() as $name => $element) {
            if ( ! array_key_exists($name, $this->_bindings)) {
                continue;
            }

            list($prop, $callback) = $this->_bindings[$name];

            $value  = (null !== $callback)
                    ? call_user_func($callback, $element->getValue())
                    : $element->getValue();

            $data   = $this->_setValueByPath($data, $prop, $value);
        }

        return $data;
    }
    // ...
}

Com­plete source code of new ver­sion of My_Form you can grab in the attached to this post sample application.

UPD[2010/02/02] For Zend_Db_Table you can use $form->getValues() in almost every case, so prob­ably, this post (and it’s first part) is not very inter­est­ing for those who use Zend_Db_Table.

  • del.icio.us
  • Google Bookmarks
  • Identi.ca
  • Twitter
  • Technorati
  • Digg
  • Slashdot
  • Facebook
  • MisterWong
  • Reddit
  • StumbleUpon
  • Mixx
  • HelloTxt
  • LinkedIn
  • PDF
  • email
  • Print
This entry was posted in Zend Framework and tagged , , , . Bookmark the permalink.

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>