I am new to this, zend decoration malarchy, but i have two significant questions that i cant get my head around. Question one is followed by some example
$decorate = array(
array('ViewHelper'),
array('Description'),
array('Errors', array('class'=>'error')),
array('Label', array('tag'=>'div', 'separator'=>' ')),
array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);
...
$name = new Zend_Form_Element_Text('title');
$name->setLabel('Title')
->setDescription("No --- way");
$name->setDecorator($decorate);
Which outputs
<li class="element">
<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 can't be empty</li>
</ul>
</li>
How do i wrap the label
and the input
around a div tag? So the output is as follows:
<li class="element">
<div>
<label for="title" class="required">Title</label>
<input type="text" name="title" id="title" value="">
</div>
<p class="hint">No --- way</p>
<ul class="error">
<li>Value is required and can't be empty</li>
</ul>
</li>
What is up with the order of the elements
in the $decorate
array? They MAKE NO SENSE!
The decorator pattern is a design pattern for adding functionality to existing classes without altering those existing classes. In stead, a decorator class wraps itself around another class, and generally exposes the same interface as the decorated class.
Basic example:
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>
Now, you might be tempted to think that because the Zend_Form_Decorator_*
classes are decorators, and have a render
method, this automatically means the output of the decorated class' render
method will always be wrapped with additional content by the decorator. But on inspection of our basic example above, we can easily see this doesn't necessarily have to be the case at all of course, as illustrated by this additional (albeit fairly useless) example:
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>
This is in fact basically how a lot of Zend_Form_Decorator_*
decorators work, if it makes sense for them to have this placement functionality.
For decorators where it makes sense, you can control the placement with the setOption( 'placement', 'append' )
for instance, or by passing the option 'placement' => 'append'
to the options array, for instance.
For Zend_Form_Decorator_PrepareElements
, for instance, this placement option is useless and therefor ignored, as it prepares form elements to be used by a ViewScript
decorator, making it one of the decorators that doesn't touch the rendered content of the decorated element.
Depending on the default functionality of the individual decorators, either the content of the decorated class is wrapped, appended, prepended, discarded or something completely different is done to the decorated class, without adding something directly to the content, before passing along the content to the next decorator. Consider this simple example:
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>
Now, when you set the decorators for a Zend_Form_Element_*
element, they will be wrapped, and consequently executed, in the order in which they are added. So, going by your example:
$decorate = array(
array('ViewHelper'),
array('Description'),
array('Errors', array('class'=>'error')),
array('Label', array('tag'=>'div', 'separator'=>' ')),
array('HtmlTag', array('tag' => 'li', 'class'=>'element')),
);
... basically what happens is the following (actual class names truncated for brevity):
$decorator = new HtmlTag( new Label( new Errors( new Description( new ViewHelper() ) ) ) );
echo $decorator->render();
So, on examining your example output, we should be able to distill the default placement behaviour of the individual decorators:
// 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>
And what do you know; this actually is the default placement of all respective decorators.
But now comes the hard part, what do we need to do to get the result you are looking for? In order to wrap the label
and input
we can't simply do this:
$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')),
);
... as this will wrap all preceding content (ViewHelper
, Description
, Errors
and Label
) with a div, right? Not even... the added decorator will be replaced by the next one, as decorators are replaced by a following decorator if it is of the same class. In stead you would have to give it a unique key:
$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')),
);
Now, we're still faced with the problem that divWrapper
will wrap all preceding content (ViewHelper
, Description
, Errors
and Label
). So we need to be creative here. There's numerous ways to achieve what we want. I'll give one example, that probably is the easiest:
$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
);
For more explanation about Zend_Form
decorators I'd recommend reading Zend Framework's lead developer Matthew Weier O'Phinney's article about Zend Form Decorators
Question #1
Change the decorators order and add an HtmlTag helper this way :
$decorate = array(
array('ViewHelper'),
array('Label', array('tag'=>'div', 'separator'=>' ')),
array('HtmlTag', array('tag' => 'div')),
array('Description'),
array('Errors', array('class'=>'error')),
array('HtmlTag', array('tag' => 'li', 'class'=>'element'))
);
Question #2
Decorators are a chain, the output of each one is passed to the input of the next one, in order to be 'decorated' by it.
By default, they append content (description, errors), prepend content (label..) and or wrap something around (HtmlTag). But these are default behaviors, and you can change it for most of them :
array('HtmlTag', array('tag' => 'span', placement=>'APPEND'));
//this would append <span></span> to the output of the previous decorator instead of wrapping it inside the <span>
Let's have a more closer look to what happens in your chain :
ViewHelper
renders your form element using it's default viewHelper, declared in the form element's class.
Label
prepends the label to the previous output
HtmlTag
wraps a <div>
around
Description
appends the elements description
Errors
appends error messages, if any
HtmlTag
wraps all this in an <li>
EDIT
I wrote this answer without testing anything, so there might be some little inaccuracies here and there. Dear reader, if you see some just drop a comment and i'll update.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With