Zend Forms

It’s being a while since my last Zend-related post. Now I’ll try to cover approach to Zend_Form usage. This component has really extensive functionality. You can use Zend_Form in a straightforward manner by creating form elements in your controller’s code:


protected function prepareForm() {
    $form = new Zend_Form();

    $query = new Zend_Form_Element_Text('query');
    $form->addElement($query);

    // ...

    $submit = new Zend_Form_Element_Submit('submit');
    $submit->setLabel('Search');
    $form->addElement($submit);

    return $form;
}

But there is another opportunity: declarative form creation. You can use property file to describe the form. For example:

[Form]
; general form metainformation
search.index.action = "/search/index"
search.index.method = "get"; 'query' element
search.index.elements.query.type = "text"
search.index.elements.query.order = 1
search.index.elements.query.options.required = true
search.index.elements.query.options.filters.lower.filter = "StringToLower"
search.index.elements.query.options.validators.strlen.validator = "StringLength"
search.index.elements.query.options.validators.strlen.options.min = "2"
search.index.elements.query.bo_method = "query"

; 'submit' element
search.index.elements.submit.type = "submit"
search.index.elements.submit.order = 4

That sounds really good. You can easily create a really complex form by virtually 2 lines of code


$config = new Zend_Config_Ini($configFile, 'File');
$form   = new Zend_Form();
$form->setConfig($config->form->name);

But the next step is ALWAYS to a) validate the form and b) pass form elements values to business object and pass it to some service class. That’s why I decided to automate this process. To solve problem a) we only need to extend our class from Zend_Form and create function that perofrms the routing task of form validation.

As for the second issue. We need to introduce new properties to Zend_Form configuration file:

  • bo_class – class of Business Object to create
  • bo_method – name of Business Object property that will be associated with element’s value.

This will allow us create plain old PHP object (defined by bo_class) with set of properties corresponding to bo_methods.

So the config will now look like:

[Form]
; general form metainformation
search.index.action = "/search/index"
search.index.method = "get"
search.index.bo_class = "Search"; 'query' element
search.index.elements.query.type = "text"
search.index.elements.query.order = 1
search.index.elements.query.options.required = true
search.index.elements.query.options.filters.lower.filter = "StringToLower"
search.index.elements.query.options.validators.strlen.validator = "StringLength"
search.index.elements.query.options.validators.strlen.options.min = "2"
search.index.elements.query.bo_method = "query"

; 'submit' element
search.index.elements.submit.type = "submit"
search.index.elements.submit.order = 4

So our example should be as following:


$form = KB_Form_Factory::getInstance()->getForm('Search_Index');
$bo = $form->process();
if( $bo != null ) {
    if( $bo->query == 'test') {

    }

}

So let’s see how we can achieve this. Discalimer: code may be a little bit long for you to look at, but please bear with it.


<?php
/*

Copyright (C) 2008 Kanjibox

@version 1.0
@see     http://www.kanjibox.com

*/

require_once 'Zend/Form.php';

/**
 * @see http://framework.zend.com/manual/en/zend.form.html
 */
class KB_Form extends Zend_Form {

    const BUSINESS_OBJECT = 'BUSINESS_OBJECT';

    protected $_bo_class;
    protected $_bo_methods;

    public function setOptions(array $options) {
        parent::setOptions($options);

        if (isset($options['bo_class'])) {
            $this->setBusinessObjectClass($options['bo_class']);
            unset($options['bo_class']);
        }
        return $this;
    }

    public function setPluginLoader(Zend_Loader_PluginLoader_Interface $loader, $type = null) {
        try {
            return parent::setPluginLoader($loader, $type);
        } catch (Zend_Form_Exception $e) {

            $type = strtoupper($type);

            if( $type == self::BUSINESS_OBJECT ) {
                $this->_loaders[$type] = $loader;
                return $this;
            } else {
                throw $e;
            }
        }
    }

    public function getPluginLoader($type = null) {
        try {
            return parent::getPluginLoader($type);
        } catch(Zend_Form_Exception $e) {
            if( $type == self::BUSINESS_OBJECT ) {
                $this->_loaders[$type] = new Zend_Loader_PluginLoader(
                        array('KB_BO_' => 'KB/BO/')
                    );

                return $this->_loaders[$type];
            } else {
                throw $e;
            }
        }
    }

    public function addPrefixPath($prefix, $path, $type = null)
    {
        try {
            return parent::addPrefixPath($prefix, $path, $type);
        } catch (Zend_Form_Exception $e) {
            $type = strtoupper($type);
            switch ($type) {
                case self::BUSINESS_OBJECT:
                    $loader = $this->getPluginLoader($type);
                    $loader->addPrefixPath($prefix, $path);
                    return $this;
                case null:
                    $prefix = rtrim($prefix, '_');
                    $path   = rtrim($path, DIRECTORY_SEPARATOR);

                    $cType        = ucfirst(strtolower($type));
                    $pluginPath   = $path . DIRECTORY_SEPARATOR . $cType . DIRECTORY_SEPARATOR;
                    $pluginPrefix = $prefix . '_' . $cType;
                    $loader       = $this->getPluginLoader($type);
                    $loader->addPrefixPath($pluginPrefix, $pluginPath);

                    return $this;
                default:
                    throw $e;
            }
        }
            
            
    }

    public function __counstruct($options = null) {
        parent::__construct(options);
    }


    public function setBusinessObjectClass($bo_class) {
        $this->_bo_class = $bo_class;
    }

    public function process() {
        $data = null;
        if ($this->getAttrib('method') == 'post') {
            $data = $_POST;
        } else if ($this->getAttrib('method') == 'get') {
            $data = $_GET;
        }
        
        if ($this->isValid($data)) {
            $values = array();
            foreach($this->getValues() as $key => $value) {
                if( isset($this->_bo_methods[$key]) ) {
                    $methodName = $this->_bo_methods[$key];
                    $values[$methodName] = $value;
                }
            }
            
            if( isset($this->_bo_class) ) {
                try {
                    $class = $this->getPluginLoader(self::BUSINESS_OBJECT)->load($this->_bo_class);
                    $bo    = new $class($values);

                    return $bo;
                } catch(Exception $e) {
                    $log = KB_LogFactory::getInstance()->getLog(__CLASS__);
                    $log->error('Cannot create business object.');
                    $log->logException($e);
                    
                    return new KB_BusinessObject($values);
                }
            } else {
                return new KB_BusinessObject($values);
            }
        } else {
            $log = KB_LogFactory::getInstance()->getLog(__CLASS__);
            foreach( $this->getErrors() as $error ) {
                $log->error( $error );
            }
        }
        return null;
    }

    public function addElements(array $elements) {
        parent::addElements($elements);
        
        foreach ($elements as $key => $spec) {
            $name = null;
            if (!is_numeric($key)) {
                $name = $key;
            }

            if (is_array($spec)) {
                $argc      = count($spec);
                $options   = array();    
                
                if (isset($spec['type']) ) {
                    if( isset($spec['name']) ) {
                        $name = $spec['name'];
                    }
                    if( isset($spec['bo_method'])) {
                        $this->_bo_methods[$name] = $spec['bo_method'];
                    }
                } else {
                    switch ($argc) {
                        case 0:
                            continue;
                        case (1 <= $argc):
                            continue;
                        case (2 <= $argc):
                            if (null === $name) {
                                $name = array_shift($spec);
                            } else {
                                $options = array_shift($spec);
                            }
                        case (3 <= $argc):
                            if (empty($options)) {
                                $options = array_shift($spec);
                            }
                        default:
                            if( isset($options['bo_method'])) {
                                $this->_bo_methods[$name] = $options['bo_method'];
                            }
                    }
                }
            }
        }
        return $this;
    }

}

?>


<?php
/*

Copyright (C) 2008 Kanjibox

@version 1.0
@see     http://www.kanjibox.com

*/

class KB_BusinessObject implements Countable, Iterator
{
    protected $_index;
    protected $_count;
    protected $_data;

    private $__allowModifications;

    public function __construct(array $array) {
        $this->_index = 0;
        $this->_data = array();
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                $this->_data[$key] = new self($value, $this->__allowModifications);
            } else {
                $this->_data[$key] = $value;
            }
        }
        $this->_count = count($this->_data);

        // allow modifications only during initialization
        $this->__allowModifications = true;
        $this->init();
        $this->__allowModifications = false;
    }

    public function init() {
            
    }

    public function get($name, $default = null) {
        $result = $default;
        if (array_key_exists($name, $this->_data)) {
            $result = $this->_data[$name];
        }
        return $result;
    }

    public function __get($name)
    {
        return $this->get($name);
    }

    public function __set($name, $value) {
        if ($this->__allowModifications) {
            if (is_array($value)) {
                $this->_data[$name] = new self($value);
            } else {
                $this->_data[$name] = $value;
            }
            $this->_count = count($this->_data);
        } else {
            throw new KB_Exception('BusinessObject is read only', KB_Exception::KB_BO);
        }
    }

    public function __clone() {
        $array = array();
        foreach ($this->_data as $key => $value) {
            if ($value instanceof BusinessObject) {
                $array[$key] = clone $value;
            } else {
                $array[$key] = $value;
            }
        }
        $this->_data = $array;
    }

    public function toArray()
    {
        $array = array();
        foreach ($this->_data as $key => $value) {
            if ($value instanceof BusinessObject) {
                $array[$key] = $value->toArray();
            } else {
                $array[$key] = $value;
            }
        }
        return $array;
    }

    public function __isset($name)
    {
        return isset($this->_data[$name]);
    }

    public function __unset($name) {
        if ($this->__allowModifications) {
            unset($this->_data[$name]);
            $this->_count = count($this->_data);
        } else {
            throw new KB_Exception('BusinessObject is read only', KB_Exception::KB_BO);
        }
    }

    public function count() {
        return $this->_count;
    }

    public function current() {
        return current($this->_data);
    }

    public function key() {
        return key($this->_data);
    }

    public function next() {
        next($this->_data);
        $this->_index++;
    }

    public function rewind() {
        reset($this->_data);
        $this->_index = 0;
    }

    public function valid() {
        return $this->_index < $this->_count;
    }

}

?>

Tags: ,

One Response to “Zend Forms”

  1. Kanjibox blog » Blog Archive » Zend Form revisited Says:

    […] time ago I introduced declarative creation of Zend Form instances. But what should you do if you have two or more forms that have common parts? Yes, you […]

Leave a Reply