ビューのレンダリングにZend_Formのデコレーターを使うための基本的な考え方に関するメモ

Zend FrameworkMVCでビュースクリプトレンダリングする際にZend_Formのデコレーター風に実装できないかを検討していたのですが、比較的簡単にできました。ビュースクリプトをZend_Form_Elementの第1デコレーターに指定することで、基本的なMVC構成とビュースクリプトの記述を生かしつつ、 複数のコントローラーで共通のデコレーターを使い回すことができます。
もちろん、Zend_FormもしくはZend_Form_Elementを通すことでバリデーターその他のZend_Formの機能をそのまま利用できるので、いろいろと便利に使えます。

具体的には

コントローラーでrenderメソッドを下記のようにします。(ViewRendererと共に使うときは適宜修正が必要です)
この例では、ビュースクリプトレンダリングし、Element中にエラーがあればそれを表示し、全体をdivでラップします。このラップするというページ全体で共通の表示ロジックをコントローラーのビュースクリプトから分離できるところが小さなメリットのうちの一つです。

<?php
    /**
     * Render a view
     *
     * Renders a view. By default, views are found in the view script path as
     * <controller>/<action>.phtml. You may change the script suffix by
     * resetting {@link $viewSuffix}. You may omit the controller directory
     * prefix by specifying boolean true for $noController.
     *
     * By default, the rendered contents are appended to the response. You may
     * specify the named body content segment to set by specifying a $name.
     *
     * @see Zend_Controller_Response_Abstract::appendBody()
     * @param  string|null $action Defaults to action registered in request object
     * @param  string|null $name Response object named path segment to use; defaults to null
     * @param  bool $noController  Defaults to false; i.e. use controller name as subdir in which to search for view script
     * @return void
     */
    public function render($action = null, $name = null, $noController = false)
    {
        if (!$this->getInvokeArg('noViewRenderer') && $this->_helper->hasHelper('viewRenderer')) {
            return $this->_helper->viewRenderer->render($action, $name, $noController);
        }

        $view   = $this->initView();
        $script = $this->getViewScript($action, $noController);

        $decorators = array (
              'content' =>
              array (
                'decorator' =>
                array (
                  'content' => 'ViewScript',
                ),
              ),
              'errors' =>
              array (
                'decorator' =>
                array (
                  'errors' => 'Errors',
                ),
              ),
              'container' =>
              array (
                'decorator' =>
                array (
                  'container' => 'HtmlTag',
                ),
                'options' =>
                array (
                  'tag' => 'div',
                  'id' => $this->_getParam('controller', 'no-name'),
                ),
              ),
            );

        $element = new Zend_Form_Element($this->_getParam('controller', 'no-name'));
        $element->setAttrib('viewScript', $script);
        $element->setView($this->view);
        $element->setDecorators($decorators);

        $this->getResponse()->appendBody(
            $element,
            $name
        );
    }

ポイントは

  • レスポンスオブジェクトは自動的に文字列化して格納するので、$elementをレスポンスに直接投入してよい。
  • viewScriptデコレーターはsetViewScriptもしくはattribのviewScriptを見るのでそこにビュースクリプトパスを与えておく。
  • デコレーターにはエイリアス名を指定しておき、後から調整できるようにしておく
  • できればレスポンスオブジェクトはカスタマイズして、文字列化して格納ではなく、出力時に文字列化するようなものに変更しておく。

などなどです。
上記サンプルで、デコレーターを直接手書きで入れているのは、結果を確認するためですが、実際にはデコレーターに関してはサイト全体で管理したい内容になってくるので、下記のようなdecoratorプラグインリソース(Zend_Application)を作成してサイト全体の設定を設定ファイルに分離して管理するようにしました。上記サンプル中、インスタンスの作成やsetDecoratorしている部分はプラグインリソースに書きだすことでよりシンプルで見通しがよくなると思います。
それから、Zend_Form_Elementで作っていますが、これもコントローラーの質によってZend_Formにしたり、DisplayGroupにしたりといった工夫が可能かと思います。

Decoratorリソース

<?php
class Flower_Bootstrap_Resource_Decorator extends Zend_Application_Resource_ResourceAbstract
{

    /**
     * Decorators for rendering
     * @var array
     */
    protected $_decorators = array();

    protected $_decoratorsSets = array();

    public function init()
    {
        $options = $this->getOptions();
        if (isset($options['decorators'])) {
            $this->addDecorators($options['decorators']);
        }
        if (isset($options['sets'])) {
        	foreach((array) $options['sets'] as $key => $set) {
        		if (is_array($set)) {
        			ksort($set);
        		}
        		$this->_decoratorsSets[$key] = $set;
        	}
        }

        return $this;
    }

    /**
     * Retrieve all decorators
     *
     * @return array
     */
    public function getDecorators($type = 'default')
    {
    	if (!isset($this->_decoratorsSets[$type])) {
    		$type = 'default';
    	}
    	$result = array();
    	foreach ($this->_decoratorsSets[$type] as $name) {
    	    //TODO:ここでインスタンスを取らずにオプションをセットするだけというのもありだと思う。
    	    //全体で共通のインスタンスではなく、エレメント毎に個別の設定を加えたいときなど。
    		$decorator = $this->getDecorator($name);
    		if ($decorator instanceof Zend_Form_Decorator_Interface) {
    			$result[] = array('decorator' => array($name => $decorator));
    		}
    	}
        return $result;
    }

    /**
     * Retrieve a registered decorator
     *
     * @param  string $name
     * @return false|Zend_Form_Decorator_Abstract
     */
    public function getDecorator($name)
    {
        if (isset($this->_decorators[$name])) {
	        if (is_array($this->_decorators[$name])) {
                $instance = $this->_loadDecorator($this->_decorators[$name], $name);
            }

            return $this->_decorators[$name];
        }

    }

    /**
     * Add many decorators at once
     *
     * @param  array $decorators
     * @return Zend_Form
     */
    public function addDecorators(array $decorators)
    {
        foreach ($decorators as $decoratorInfo) {
            if (is_string($decoratorInfo)) {
                $this->addDecorator($decoratorInfo);
            } elseif ($decoratorInfo instanceof Zend_Form_Decorator_Interface) {
                $this->addDecorator($decoratorInfo);
            } elseif (is_array($decoratorInfo)) {
                $argc    = count($decoratorInfo);
                $options = array();
                if (isset($decoratorInfo['decorator'])) {
                    $decorator = $decoratorInfo['decorator'];
                    if (isset($decoratorInfo['options'])) {
                        $options = $decoratorInfo['options'];
                    }
                    $this->addDecorator($decorator, $options);
                } else {
                    switch (true) {
                        case (0 == $argc):
                            break;
                        case (1 <= $argc):
                            $decorator  = array_shift($decoratorInfo);
                        case (2 <= $argc):
                            $options = array_shift($decoratorInfo);
                        default:
                            $this->addDecorator($decorator, $options);
                            break;
                    }
                }
            } else {
                require_once 'Zend/Form/Exception.php';
                throw new Zend_Form_Exception('Invalid decorator passed to addDecorators()');
            }
        }

        return $this;
    }

    /**
     * Add a decorator for rendering the element
     *
     * @param  string|Zend_Form_Decorator_Interface $decorator
     * @param  array|Zend_Config $options Options with which to initialize decorator
     * @return Zend_Form
     */
    public function addDecorator($decorator, $options = null)
    {
        if ($decorator instanceof Zend_Form_Decorator_Interface) {
            $name = get_class($decorator);
        } elseif (is_string($decorator)) {
            $name      = $decorator;
            $decorator = array(
                'decorator' => $name,
                'options'   => $options,
            );
        } elseif (is_array($decorator)) {
            foreach ($decorator as $name => $spec) {
                break;
            }
            if (is_numeric($name)) {
                require_once 'Zend/Form/Exception.php';
                throw new Zend_Form_Exception('Invalid alias provided to addDecorator; must be alphanumeric string');
            }
            if (is_string($spec)) {
                $decorator = array(
                    'decorator' => $spec,
                    'options'   => $options,
                );
            } elseif ($spec instanceof Zend_Form_Decorator_Interface) {
                $decorator = $spec;
            }
        } else {
            require_once 'Zend/Form/Exception.php';
            throw new Zend_Form_Exception('Invalid decorator provided to addDecorator; must be string or Zend_Form_Decorator_Interface');
        }

        $this->_decorators[$name] = $decorator;

        return $this;
    }

    /**
     * Lazy-load a decorator
     *
     * @param  array $decorator Decorator type and options
     * @param  mixed $name Decorator name or alias
     * @return Zend_Form_Decorator_Interface
     */
    protected function _loadDecorator(array $decorator, $name)
    {
        $sameName = false;
        if ($name == $decorator['decorator']) {
            $sameName = true;
        }

        $instance = $this->_getDecorator($decorator['decorator'], $decorator['options']);
        if ($sameName) {
            $newName            = get_class($instance);
            $decoratorNames     = array_keys($this->_decorators);
            $order              = array_flip($decoratorNames);
            $order[$newName]    = $order[$name];
            $decoratorsExchange = array();
            unset($order[$name]);
            asort($order);
            foreach ($order as $key => $index) {
                if ($key == $newName) {
                    $decoratorsExchange[$key] = $instance;
                    continue;
                }
                $decoratorsExchange[$key] = $this->_decorators[$key];
            }
            $this->_decorators = $decoratorsExchange;
        } else {
            $this->_decorators[$name] = $instance;
        }

        return $instance;
    }

    /**
     * Instantiate a decorator based on class name or class name fragment
     *
     * @param  string $name
     * @param  null|array $options
     * @return Zend_Form_Decorator_Interface
     */
    protected function _getDecorator($name, $options)
    {
        $class = $this->getPluginLoader()->load($name);
        if (null === $options) {
            $decorator = new $class;
        } else {
            $decorator = new $class($options);
        }

        return $decorator;
    }

    /**
     * Retrieve plugin loader for given type
     *
     * $type may be one of:
     * - decorator
     * - element
     *
     * If a plugin loader does not exist for the given type, defaults are
     * created.
     *
     * @param  string $type
     * @return Zend_Loader_PluginLoader_Interface
     */
    public function getPluginLoader()
    {
        if (!isset($this->_pluginLoader)) {
            $this->_pluginLoader = new Zend_Loader_PluginLoader(
                array('Zend_Form_Decorator'    => 'Zend/Form/Decorator/',
                      'Flower_Form_Decorator' => 'Flower/Form/Decorator/')
            );
        }

        return $this->_pluginLoader;
    }
}

設定ファイルでは、Zend_Form準拠のデコレーター一覧、およびそれらのインスタンスリストからどの組み合わせを利用するかを指定します。これで、Zend_Formで文字列指定やデフォルトデコレーターのようにインスタンスを量産することなく、かつサイト全体で同じ設定を使い、必要に応じて変更を伝搬させることができるようになります。
個人的にはもう少し進めて、dispatchループが終了するまでにコンテンツ全体をZend_Formにビルドしていくような形をパターン化する予定なので、もうひとひねりするつもりではいます。