Ответ 1
шаблон декоратора - это шаблон проектирования для добавления функциональности к существующим классам без изменения существующих классов. Вместо этого класс декоратора обертывается вокруг другого класса и обычно предоставляет тот же интерфейс, что и декорированный класс.
Основной пример:
interface Renderable
{
public function render();
}
class HelloWorld
implements Renderable
{
public function render()
{
return 'Hello world!';
}
}
class BoldDecorator
implements Renderable
{
protected $_decoratee;
public function __construct( Renderable $decoratee )
{
$this->_decoratee = $decoratee;
}
public function render()
{
return '<b>' . $this->_decoratee->render() . '</b>';
}
}
// wrapping (decorating) HelloWorld in a BoldDecorator
$decorator = new BoldDecorator( new HelloWorld() );
echo $decorator->render();
// will output
<b>Hello world!</b>
Теперь у вас может возникнуть соблазн подумать, что, поскольку классы Zend_Form_Decorator_*
являются декораторами и имеют метод render
, это автоматически означает, что вывод метода украшенного класса render
всегда будет обернут дополнительным контентом декоратором. Но при проверке нашего основного примера выше, мы можем легко видеть, что это не обязательно должно иметь место вообще, как иллюстрируется этим дополнительным (хотя и довольно бесполезным) примером:
class DivDecorator
implements Renderable
{
const PREPEND = 'prepend';
const APPEND = 'append';
const WRAP = 'wrap';
protected $_placement;
protected $_decoratee;
public function __construct( Renderable $decoratee, $placement = self::WRAP )
{
$this->_decoratee = $decoratee;
$this->_placement = $placement;
}
public function render()
{
$content = $this->_decoratee->render();
switch( $this->_placement )
{
case self::PREPEND:
$content = '<div></div>' . $content;
break;
case self::APPEND:
$content = $content . '<div></div>';
break;
case self::WRAP:
default:
$content = '<div>' . $content . '</div>';
}
return $content;
}
}
// wrapping (decorating) HelloWorld in a BoldDecorator and a DivDecorator (with DivDecorator::APPEND)
$decorator = new DivDecorator( new BoldDecorator( new HelloWorld() ), DivDecorator::APPEND );
echo $decorator->render();
// will output
<b>Hello world!</b><div></div>
Это на самом деле в основном то, как работают многие декораторы Zend_Form_Decorator_*
, если для них имеет смысл использовать эту функцию размещения.
Для декораторов, где это имеет смысл, вы можете управлять размещением с помощью setOption( 'placement', 'append' )
, например, или передав параметр 'placement' => 'append'
в массив параметров, например.
Для Zend_Form_Decorator_PrepareElements
, например, эта опция размещения бесполезна и поэтому игнорируется, поскольку она подготавливает элементы формы, которые будут использоваться декоратором ViewScript
, делая ее одним из декораторов, которые не касаются визуализированного содержимого украшенный элемент.
В зависимости от функциональности по умолчанию для отдельных декораторов либо содержимое украшенного класса обернуто, добавлено, добавлено, отброшено или, то что-то совершенно другое делается для украшенного класса, без добавления чего-либо непосредственно к контенту, перед тем как перейти по контенту к следующему декоратору. Рассмотрим этот простой пример:
class ErrorClassDecorator
implements Renderable
{
protected $_decoratee;
public function __construct( Renderable $decoratee )
{
$this->_decoratee = $decoratee;
}
public function render()
{
// imagine the following two fictional methods
if( $this->_decoratee->hasErrors() )
{
$this->_decoratee->setAttribute( 'class', 'errors' );
}
// we didn't touch the rendered content, we just set the css class to 'errors' above
return $this->_decoratee->render();
}
}
// wrapping (decorating) HelloWorld in a BoldDecorator and an ErrorClassDecorator
$decorator = new ErrorClassDecorator( new BoldDecorator( new HelloWorld() ) );
echo $decorator->render();
// might output something like
<b class="errors">Hello world!</b>
Теперь, когда вы устанавливаете декораторы для элемента Zend_Form_Element_*
, они будут завернуты и, следовательно, будут выполнены в том порядке, в котором они будут добавлены. Итак, по вашему примеру:
$decorate = array(
array('ViewHelper'),
array('Description'),
array('Errors', array('class'=>'error')),
array('Label', array('tag'=>'div', 'separator'=>' ')),
array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);
... в основном, происходит следующее (имена фактических классов сокращены для краткости):
$decorator = new HtmlTag( new Label( new Errors( new Description( new ViewHelper() ) ) ) );
echo $decorator->render();
Итак, изучая вывод вашего примера, мы должны уметь отличать поведение по умолчанию для отдельных декораторов:
// ViewHelper->render()
<input type="text" name="title" id="title" value="">
// Description->render()
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p> // placement: append
// Errors->render()
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p>
<ul class="error"> // placement: append
<li>Value is required and cant be empty</li>
</ul>
// Label->render()
<label for="title" class="required">Title</label> // placement: prepend
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p>
<ul class="error">
<li>Value is required and cant be empty</li>
</ul>
// HtmlTag->render()
<li class="element"> // placement: wrap
<label for="title" class="required">Title</label>
<input type="text" name="title" id="title" value="">
<p class="hint">No --- way</p>
<ul class="error">
<li>Value is required and cant be empty</li>
</ul>
</li>
И что вы знаете; это фактически стандартное размещение всех соответствующих декораторов.
Но теперь идет сложная часть, что нам нужно сделать, чтобы получить результат, который вы ищете? Чтобы обернуть label
и input
, мы не можем просто сделать это:
$decorate = array(
array('ViewHelper'),
array('Description'),
array('Errors', array('class'=>'error')),
array('Label', array('tag'=>'div', 'separator'=>' ')),
array('HtmlTag', array('tag' => 'div')), // default placement: wrap
array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);
... так как это приведет к завершению всего предыдущего содержимого (ViewHelper
, Description
, Errors
и label
) с div, правильно? Даже не... добавленный декоратор будет заменен следующим, так как декораторы заменяются следующим декоратором, если он имеет тот же класс. Вместо этого вам придется дать ему уникальный ключ:
$decorate = array(
array('ViewHelper'),
array('Description'),
array('Errors', array('class'=>'error')),
array('Label', array('tag'=>'div', 'separator'=>' ')),
array(array('divWrapper' => 'HtmlTag'), array('tag' => 'div')), // we'll call it divWrapper
array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);
Теперь мы по-прежнему сталкиваемся с проблемой, что divWrapper
будет обертывать все предыдущее содержимое (ViewHelper
, Description
, Errors
и label
). Поэтому нам нужно быть творческим здесь. Есть много способов добиться того, чего мы хотим. Я приведу один пример, который, вероятно, самый простой:
$decorate = array(
array('ViewHelper'),
array('Label', array('tag'=>'div', 'separator'=>' ')), // default placement: prepend
array(array('divWrapper' => 'HtmlTag'), array('tag' => 'div')), // default placement: wrap
array('Description'), // default placement: append
array('Errors', array('class'=>'error')), // default placement: append
array('HtmlTag', array('tag' => 'li', 'class'=>'element')), // default placement: wrap
);
Для более подробного описания декораторов Zend_Form
я бы рекомендовал прочитать статью Zend Framework ведущего разработчика Matthew Weier O'Phinney статьи о декораторах Zend Form