Zend_Form_Element_Captcha でデコレータ毎にレンダリングする。

Zend_Form はマジックメソッド __toString() が実装されており、インスタンスを echo すると、フォームが dl タグで整形されて出力されます。
また、要素毎に細かく出力を調整しながらレンダリングしていくことも可能です。

Zend_Form に登録された username 要素のラベルだけを出力する場合は、以下のようになります。

<form action="" method="POST">
<?php echo $this->form->username->renderLabel(); ?>
</form>

このように、登録したデコレータで出力出来るような機能が実装されています。
Zend_Form_Element の実装部分を以下に記載いたします。

<?php
// Zend/Form/Element.php

    /**
     * Overloading: allow rendering specific decorators
     *
     * Call renderDecoratorName() to render a specific decorator.
     *
     * @param  string $method
     * @param  array $args
     * @return string
     * @throws Zend_Form_Exception for invalid decorator or invalid method call
     */
    public function __call($method, $args)
    {
        if ('render' == substr($method, 0, 6)) {
            $this->_isPartialRendering = true;
            $this->render();
            $this->_isPartialRendering = false;
            $decoratorName = substr($method, 6);
            if (false !== ($decorator = $this->getDecorator($decoratorName))) {
                $decorator->setElement($this);
                $seed = '';
                if (0 < count($args)) {
                    $seed = array_shift($args);
                }
                return $decorator->render($seed);
            }

            require_once 'Zend/Form/Element/Exception.php';
            throw new Zend_Form_Element_Exception(sprintf('Decorator by name %s does not exist', $decoratorName));
        }

        require_once 'Zend/Form/Element/Exception.php';
        throw new Zend_Form_Element_Exception(sprintf('Method %s does not exist', $method));
    }

マジックメソッド __call() を利用して、render{デコレータ名}() として呼び出してやることが可能となっています。
しかし、この方法で利用できるのは登録されたデコレータのみとなります。

Zend_Form_Element_Captcha はちょっと特殊

今回の主題となる、Zend_Form_Element_Captcha で利用するデコレータは Zend_Form_Decorator_Captcha なのですが、これがなかなか曲者で、こちらが明示的に登録するのではなく、出力する時点*1で、自動的に登録されるように設計されています。

実装部分のコードを以下に記します。

<?php
// Zend/Form/Element/Captcha.php

    /**
     * Render form element
     *
     * @param  Zend_View_Interface $view
     * @return string
     */
    public function render(Zend_View_Interface $view = null)
    {
        $captcha    = $this->getCaptcha();
        $captcha->setName($this->getFullyQualifiedName());

        $decorators = $this->getDecorators();

        $decorator  = $captcha->getDecorator();
        if (!empty($decorator)) {
            array_unshift($decorators, $decorator);
        }

        $decorator = array('Captcha', array('captcha' => $captcha));
        array_unshift($decorators, $decorator);

        $this->setDecorators($decorators);

        $this->setValue($this->getCaptcha()->generate());

        return parent::render($view);
    }

上記のコードを見ても分かる通り、render() メソッドが呼ばれたタイミングでデコレータを登録していますね。
これと同じ処理を Zend_Form_Element_Captcha 要素の登録時に行うわけです。

・・・と先日まで思っていたのですが、実はそんな必要はなく、デコレータを登録せずとも renderCaptcha() を呼ぶだけで利用できてしまいます。
なぜか?
先のデコレータ出力用のマジックメソッド __call() の処理を改めて見てください。
render() メソッドを実行している行がありますね?
つまり、デコレータインスタンスを呼びだす直前に Zend_Form_Decorator_Captcha デコレータを設定されるわけです。
返り値は受け取っていないので、出力には影響しません。

[2011/10/25 訂正] すみません、出力に影響がありました。render() メソッドが呼ばれる度、$this->setValue($this->getCaptcha()->generate()); が実行されてしまうので、例えば Zend_Captcha_Image を利用するような実装がされていれば、マジックメソッド __call() からレンダリングされるたびに画像が生成されてしまいます。
また、value 値も設定していますので、バリデーションが通らなくなってしまいます。
対策としては、継承クラスを作成し、render() メソッドを上書きすると良いかと思います。
以下にサンプルを載せておきます。

<?php
/** @see Zend_Form_Element_Captcha */
require_once 'Zend/Form/Element/Captcha.php';

class Peta_Form_Element_Captcha extends Zend_Form_Element_Captcha
{
    /**
     * Render form element
     *
     * @param  Zend_View_Interface $view
     * @return string
     */
    public function render(Zend_View_Interface $view = null)
    {
        if ($this->getUnfilteredValue() === null) {
            $captcha    = $this->getCaptcha();
            $captcha->setName($this->getFullyQualifiedName());

            $decorators = $this->getDecorators();

            $decorator  = $captcha->getDecorator();
            if (!empty($decorator)) {
                array_unshift($decorators, $decorator);
            }

            $decorator = array('Captcha', array('captcha' => $captcha));
            array_unshift($decorators, $decorator);

            $this->setDecorators($decorators);

            $this->setValue($this->getCaptcha()->generate());
        }

        return  parent::render($view);
    }
}

ちなみに、renderCaptcha() だけでは、CAPTCHA が表示されるのみで、入力項目が表示されません。
実装されているか否かによりますが、上記コード中の $this->getCaptcha() で帰ってくるオブジェクトの getDecorator() メソッドにデコレータを返すような実装がなされておれば、そのデコレータをレンダリングしてやらなければなりません。
CAPTCHA に Zend_Capture_Figlet を利用する場合は、Zend_Form_Decorator_Captcha_Word がデコレータとして登録されますので、 renderCaptcha_Word() というメソッドを呼ぶ必要があります。

いやぁ、ホント、ZendFrameworkはソースを読まないと分からない事多いですね・・・。
まぁ、読みやすいですし、読めば分かることも多いので、一度皆さんも読んで見ることをお勧めします。
勉強にもなりますしね!

*1:render() メソッドが呼ばれた時点