In my previous post I’ve added a simple binding functionality to Zend_Form. But once I’ve implemented it in real world with Doctrine PHP ORM as my BLL, I found that it can’t work seamlessly with my model’s properties of array type. In fact you’ll feel the same problem with Zend_Db_Table based models if you have fields representing arrays. The simplest solution, as I had only one such-typed property in my model, was to override My_Form::fill() method in concrete form. But as we speak about reusable code I implemented it in My_Form directly…
But before I’ll start let’s review previous 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 property in model, and I want to bind some element to $model->address['postal'], and another element 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 corresponding getModelData() for Zend_Db_Table) method able to use such bind paths. To do so I have created a helper to set values by such path. Standard 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 modified versions 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;
}
// ...
}
Complete source code of new version 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 probably, this post (and it’s first part) is not very interesting for those who use Zend_Db_Table.