Archive for Zend Framework

Zend Framework: Clearing/Resetting HeadLink, HeadMeta, HeadScript, HeadStyle and HeadTitle

Scenario

Assuming your application is running smoothly and all of the HeadX (Refers to HeadLink, HeadMeta, HeadScript, HeadStyle and HeadTitle collectively) have been setup. Suddenly, an exception occurs and your ErrorController has been activated.

You need to use the HeadX to render your error page but those HeadX needs to be cleared of all the previous unwanted setup before we can start using them to render the page. Somehow, there is no clear-related method in the HeadX to do this simply. If this situation, or somewhat related, applies to you, maybe the following article is for you.

Some background

You can skip this part actually. But if you are interested in the technical details, you can continue reading. This post is written based on Zend Framework v1.7.4.

Zend_View_Helper_Placeholder_Container_Standalone class

If you were to open up the Zend/View/Helper/HeadX.php, you will find that those classes actually extend Zend_View_Helper_Placeholder_Container_Standalone class (We shall term it as Standalone class from now on).

Zend_View_Helper_Abstract (implements . . .)
 +- Zend_View_Helper_Placeholder_Container_Standalone (implements . . .)
     |- Zend_View_Helper_HeadLink
     |- Zend_View_Helper_HeadMeta
     |- Zend_View_Helper_HeadScript
     |- Zend_View_Helper_HeadStyle
     +- Zend_View_Helper_HeadTitle

In this Standalone class, there are a few protected variables that are of interest to us as follows.

  1. $_container: This variable holds a reference to an instance of Zend_View_Helper_Placeholder_Container_Abstract class, which essentially is an extended ArrayObject class.

    ArrayObject
     +- Zend_View_Helper_Placeholder_Container_Abstract
         +- Zend_View_Helper_Placeholder_Container
    
  2. $_registry: This variable holds a reference to an instance of Zend_View_Helper_Placeholder_Registry class, which is sort of a Singleton using Zend_Registry.
  3. $_regKey: This variable is overridden by each of the respective HeadX classes and is assigned a unique key, in this case, the class name itself. You can open up the individual Zend/View/Helper/HeadX.php to find this key.

I have extracted the relevant code from the Standalone class as follows. As you can see with reference to the above mentioned protected variables, the methods are actually quite simple and the important method here is __construct. It is using a common placeholder registry to obtain a container reference for the respective HeadX class that we are using.

Using HeadLink as an example, you will find $_regKey is assigned with the key 'Zend_View_Helper_HeadLink' in Zend/View/Helper/HeadLink.php. Essentially, this key will retrieve a unique common container reference purely used by the HeadLink class.

public function __construct()
{
    $this->setRegistry(Zend_View_Helper_Placeholder_Registry::getRegistry());
    $this->setContainer($this->getRegistry()->getContainer($this->_regKey));
}

public function setRegistry(Zend_View_Helper_Placeholder_Registry $registry)
{
    $this->_registry = $registry;
    return $this;
}

public function getRegistry()
{
    return $this->_registry;
}

public function setContainer(Zend_View_Helper_Placeholder_Container_Abstract $container)
{
    $this->_container = $container;
    return $this;
}

public function getContainer()
{
    return $this->_container;
}

Zend_View_Helper_Placeholder_Registry class

From Zend_View_Helper_Placeholder_Registry class, I have extracted the relevant code used by Standalone class constructor. As you can see, the $_regKey in Standalone class is used to retrieve an unique instance of the container in getContainer method. And if container is not created yet, a new instance will be created by createContainer method. The container will be an instance of Zend_View_Helper_Placeholder_Container which extends Zend_View_Helper_Placeholder_Container_Abstract.

protected $_containerClass = 'Zend_View_Helper_Placeholder_Container';

public function getContainer($key)
{
    $key = (string) $key;
    if (isset($this->_items[$key])) {
        return $this->_items[$key];
    }

    $container = $this->createContainer($key);

    return $container;
}

public function createContainer($key, array $value = array())
{
    $key = (string) $key;

    $this->_items[$key] = new $this->_containerClass(array());
    return $this->_items[$key];
}

Zend_View_Helper_Placeholder_Container class

At this point, we now know placeholder registry is the central repository for the containers and Standalone class retrieves the reference from it. We also know that the container is an instance of Zend_View_Helper_Placeholder_Container class, which exposes methods from both it's _Abstrast parent class and ArrayObject class.

If you were to open up the Zend/View/Helper/HeadX.php files, you will find that operations (such as append, prepend) on the container, returned by getContainer method, is performed using methods exposed by Zend_View_Helper_Placeholder_Container class.

However, inside Zend/View/Helper/HeadTitle.php, you will find that it did not use getContainer method at all and we are able to call set, append and prepend on the instance of Standalone class itself, which does not have those methods. The trick to this is the following __call method in the Standalone class, which indirectly exposes container methods as well.

public function __call($method, $args)
{
    $container = $this->getContainer();
    if (method_exists($container, $method)) {
        $return = call_user_func_array(array($container, $method), $args);
        if ($return === $container) {
            // If the container is returned, we really want the current object
            return $this;
        }
        return $return;
    }

    require_once 'Zend/View/Exception.php';
    throw new Zend_View_Exception('Method "' . $method . '" does not exist');
}

So with the above __call method, calling set, append and prepend methods, which does not exist in Standalone class, will automatically route to the container inside. This will mean that all methods expose by Zend_View_Helper_Placeholder_Container_Abstract and ArrayObject can be invoked on Standalone class itself.

The solution

If you follow the technical stuff above, you will understand that the ArrayObject methods are exposed on the HeadX classes as well. So to clear/reset HeadX, we just need the following line in .phtml file. It's just that simple. :)

$this->headX()->exchangeArray(array())

You might like to read up on a related post on clearing ArrayObject for more information.

=D As usual, if you have any comments or if there is any better way of doing this, remember to share them with me.

Comments (6)

Zend Framework: Extending Decorators Flexibility (Part 2)

In Part 1, we are able to split the content into parts and apply decorators on them separately. This can be achieved by using the following classes. I guess the render method of each class pretty much sums up what they do so I will not explain further.

<?php

/**
 * @see Zend_Form_Decorator_Abstract
 */
require_once 'Zend/Form/Decorator/Abstract.php';

/**
 * This class will store the content into this instance during rendering. You
 * can retrieve the stored content using the complementing class
 * App_Form_Decorator_Retrieve. When adding several instances of this decorator,
 * you will have to alias it. The alias given will be used as the name option of
 * the App_Form_Decorator_Retrieve decorator.
 *
 * Accepts the following option:
 * - clear: whether to clear the passed in content by returning an empty content.
 *
 * Add the following line to the init() function of the Zend_Form-extended class
 * and use 'Store' to instantiate this decorator class.
 *
 * $this->addPrefixPath('App_Form_Decorator', 'App/Form/Decorator', Zend_Form::DECORATOR);
 *
 * @category   App
 * @package    App_Form_Decorator
 */
class App_Form_Decorator_Store extends Zend_Form_Decorator_Abstract
{
    /**
     * @access protected
     * @var boolean
     */
    protected $_clear = null;
    /**
     * @access protected
     * @var string
     */
    protected $_content = null;

    /**
     * Specifies whether to clear the passed in content after it is stored in
     * this instance.
     *
     * @param  boolean  $clear  True if to clear the passed in content.
     * @return  App_Form_Decorator_Store  For method chaining.
     * @throws  App_Form_Decorator_Exception  If parameter is not valid.
     */
    public function setClear($clear)
    {
        if (!is_bool($clear))
        {
            require_once 'App/Form/Decorator/Exception.php';
            throw new App_Form_Decorator_Exception('Clear option is not defined');
        }

        $this->_clear = $clear;

        return $this;
    }

    /**
     * Returns whether to clear the passed in content after it is stored in
     * this instance.
     *
     * @return  boolean  True if to clear the passed in content.
     * @throws  App_Form_Decorator_Exception  If 'clear' option is not valid.
     */
    public function getClear()
    {
        if (is_null($this->_clear))
        {
            if (!is_null($clear = $this->getOption('clear')))
            {
                $this->setClear($clear);
                $this->removeOption('clear');
            }
            else
                $this->setClear(true);
        }

        return $this->_clear;
    }

    /**
     * Clears content stored in this instance.
     *
     * @return  App_Form_Decorator_Store  For method chaining.
     */
    public function clearContent()
    {
        $this->_content = null;

        return $this;
    }

    /**
     * Returns content stored in this instance.
     *
     * @param  boolean  $clear  True if to clear stored content in this instance.
     * @return  string  Stored content.
     */
    public function getContent($clear = false)
    {
        $content = $this->_content;

        if (is_null($content))
            $content = '';
        elseif ($clear)
            $this->clearContent();

        return $content;
    }

    /**
     * Store the passed in content for retrieval.
     *
     * @param  string  $content  Passed in content to store.
     * @return  string  Depending on the clear option, it is either empty content or as is.
     */
    public function render($content)
    {
        // Store the passed in content to this instance.
        $this->_content = $content;

        // Clear or return content.
        return ($this->getClear()) ? '' : $content;
    }
}
<?php

/**
 * @see Zend_Form_Decorator_Abstract
 */
require_once 'Zend/Form/Decorator/Abstract.php';

/**
 * This class will retrieve the stored content from complementing class
 * App_Form_Decorator_Store. When adding several instances of this decorator,
 * you will have to alias it. The alias is purely used to ensure the instances
 * will not overwrite each other. To retrieve the stored content, you will need
 * to pass the alias used in instantiating the App_Form_Decorator_Store
 * decorator to the 'name' option.
 *
 * Accepts the following options:
 * - name: The alias used when creating the App_Form_Decorator_Store instance.
 * - remove: Boolean value determining whether to remove the stored content
 *           after retrieval.
 * - separator: string with which to separate passed in content and retrieved
 *              content.
 * - placement: whether to append, prepend or replace with the retrieved
 *              content.
 *
 * Add the following line to the init() function of the Zend_Form-extended class
 * and use 'Retrieve' to instantiate this decorator class.
 *
 * $this->addPrefixPath('App_Form_Decorator', 'App/Form/Decorator', Zend_Form::DECORATOR);
 *
 * @category   App
 * @package    App_Form_Decorator
 */
class App_Form_Decorator_Retrieve extends Zend_Form_Decorator_Abstract
{
    /**
     * @access protected
     * @var string
     */
    protected $_name = null;
    /**
     * @access protected
     * @var boolean
     */
    protected $_remove = null;

    /**
     * Returns stored content from the App_Form_Decorator_Store instance.
     *
     * @param  boolean  $clear  True if to clear stored content after retrieval.
     * @return  string  Stored content.
     * @throws  App_Form_Decorator_Exception  If wrong decorator instance is retrieved.
     */
    protected function _getContent($clear)
    {
        $store = $this->getElement()->getDecorator($this->getName());

        if (!$store instanceof App_Form_Decorator_Store)
        {
            require_once 'App/Form/Decorator/Exception.php';
            throw new App_Form_Decorator_Exception('"' . $this->getName() . '" is not a App_Form_Decorator_Store decorator');
        }

        return $store->getContent($clear);
    }

    /**
     * Specifies the name/alias of the element decorator to retrieve.
     *
     * @param  string  $name  Name/alias of the element decorator to retrieve.
     * @return  App_Form_Decorator_Retrieve  For method chaining.
     * @throws  App_Form_Decorator_Exception  If parameter is not valid.
     */
    public function setName($name)
    {
        if (!is_string($name))
        {
            require_once 'App/Form/Decorator/Exception.php';
            throw new App_Form_Decorator_Exception('Element decorator name is not defined');
        }

        if (is_numeric($name))
        {
            require_once 'App/Form/Decorator/Exception.php';
            throw new App_Form_Decorator_Exception('Element decorator name must be alphanumeric');
        }

        $this->_name = $name;

        return $this;
    }

    /**
     * Returns name/alias of the element decorator to retrieve.
     *
     * @return  string  Name/alias of the element decorator to retrieve.
     * @throws  App_Form_Decorator_Exception  If 'name' option is not valid.
     */
    public function getName()
    {
        if (is_null($this->_name))
        {
            if (is_null($name = $this->getOption('name')))
            {
                require_once 'App/Form/Decorator/Exception.php';
                throw new App_Form_Decorator_Exception('Element decorator name is not defined in the options');
            }

            $this->setName($name);
            $this->removeOption('name');
        }

        return $this->_name;
    }

    /**
     * Specifies whether to remove the stored content in
     * App_Form_Decorator_Store instance after retrieval.
     *
     * @param  boolean  $remove  True if remove the stored content after retrieval.
     * @return  App_Form_Decorator_Store  For method chaining.
     * @throws  App_Form_Decorator_Exception  If parameter is not valid.
     */
    public function setRemove($remove)
    {
        if (!is_bool($remove))
        {
            require_once 'App/Form/Decorator/Exception.php';
            throw new App_Form_Decorator_Exception('Remove option is not defined');
        }

        $this->_remove = $remove;

        return $this;
    }

    /**
     * Returns whether to remove the stored content in
     * App_Form_Decorator_Store instance after retrieval.
     *
     * @return  boolean  True if remove the stored content after retrieval.
     * @throws  App_Form_Decorator_Exception  If 'remove' option is not valid.
     */
    public function getRemove()
    {
        if (is_null($this->_remove))
        {
            if (!is_null($remove = $this->getOption('remove')))
            {
                $this->setRemove($remove);
                $this->removeOption('remove');
            }
            else
                $this->setRemove(true);
        }

        return $this->_remove;
    }

    /**
     * Retrieves the stored content.
     *
     * @param  string  $content  Passed in content.
     * @return  string  Rendered content.
     */
    public function render($content)
    {
        switch ($this->getPlacement())
        {
            case self::APPEND:
                return $content . $this->getSeparator() . $this->_getContent($this->getRemove());

            case self::PREPEND:
                return $this->_getContent($this->getRemove()) . $this->getSeparator() . $content;

            default:
                return $this->_getContent($this->getRemove());
        }
    }
}

And finally, the App_Form_Decorator_Exception class for completeness.

<?php

/**
 * @see Zend_Exception
 */
require_once 'Zend/Exception.php';

/**
 * @category   App
 * @package    App_Form_Decorator
 */
class App_Form_Decorator_Exception extends Zend_Exception
{
}

To use them, you just need to put these three classes into a folder App/Form/Decorator. You need to add this folder to the include_path similar to what you did for Zend library. Remember to enable autoloading using the Zend_Loader class. Other information can be found in the class remarks.

=D If there is any optimization or bug, please feel free to point them out so that I can update my copy as well.
oh Oh... One more thing, you can also used the same concept to create decorator that does formatting, such as indentation, of the rendered content if you haven't thought of that.

Leave a Comment

Zend Framework: Extending Decorators Flexibility (Part 1)

During form rendering, the decorators are executed in the order added and rendering are done on the same content throughout. If you were to open up Zend/Form.php, you will find inside an elegant render method as follows that summarizes what I’ve mentioned earlier.

public function render(Zend_View_Interface $view = null)
{
    if (null !== $view) {
        $this->setView($view);
    }

    $content = '';
    foreach ($this->getDecorators() as $decorator) {
        $decorator->setElement($this);
        $content = $decorator->render($content);
    }
    return $content;
}

Here comes the issue. We cannot apply decorators selectively to a part of the content by breaking it up. You can only prepend, append or wrap the whole, not portion of, content with a new decorator. This poses a challenge if you need to create custom tags around both Label and Text Field in a Zend_Form_Element_Text for example.

If you, like me, have been doing lots of research on Zend Form Decorators and is hitting the wall, I guess the following might be a solution that will give flexibility to the way decorators can be used. I hope this will help someone in a way or another.

Before I start presenting the possible solution, let me go through what we want to achieve, what we can achieve using the closest available method on the Internet and my proposed solution that introduces flexibility to what we want to achieve.

What we want to achieve

As an example, here's what we want to achieve, a class attribute of value label for the first td tag on line 2. This will allow us to style it with CSS class label.

<tr>
    <td class="label">
        <label for="username" class="optional">Username:</label>
    </td>
    <td class="element">
        <input type="text" name="username" id="username" value="" />
    </td>
</tr>

What we can achieve using the closest available method

Here are the decorators applied to Zend_Form_Element_Text instance. On line 5, we have added an option 'class' => 'label' to the Label decorator.

$element->setDecorators(array(
    'ViewHelper',
    'Errors',
    array(array('data' => 'HtmlTag'), array('tag' => 'td', 'class' => 'element')),
    array('Label', array('tag' => 'td', 'class' => 'label')),
    array(array('row' => 'HtmlTag'), array('tag' => 'tr')),
));

The following is achieved using the above code. As you can see, class label is applied to label tag on line 3, and not td tag on line 2. This is not what we want.

<tr>
    <td>
        <label for="username" class="label optional">Username:</label>
    </td>
    <td class="element">
        <input type="text" name="username" id="username" value="" />
    </td>
</tr>

Proposed solution

We will create two new decorator class: Store and Retrieve. It will be used as follows.

$username->setDecorators(array(
    // Create element and store it.
    'ViewHelper',
    'Errors',
    array(array('td-element' => 'HtmlTag'), array('tag' => 'td', 'class' => 'element')),
    array(array('store-element' => 'Store'), array('clear' => true)),
    
    // Create label and store it.
    'Label',
    array(array('td-label' => 'HtmlTag'), array('tag' => 'td', 'class' => 'label')),
    array(array('store-label' => 'Store'), array('clear' => true)),
    
    // Retrieve both the label and element.
    array(array('retrieve-label' => 'Retrieve'), array('name' => 'store-label', 'remove' => true)),
    array(array('retrieve-element' => 'Retrieve'), array('name' => 'store-element', 'remove' => true)),
    
    // Wrap the retrieved label and element with tr tag.
    array(array('row' => 'HtmlTag'), array('tag' => 'tr')),
));

The following is achieved using the above code as what we have set out initially. We are now able to work on part of the content at a time.

<tr>
    <td class="label">
        <label for="username" class="optional">Username:</label>
    </td>
    <td class="element">
        <input type="text" name="username" id="username" value="" />
    </td>
</tr>

In Part 2, we will present the two newly derived decorator class.

Excellent References:
http://framework.zend.com/manual/en/zend.form.standardDecorators.html
http://devzone.zend.com/article/3450-Decorators-with-Zend_Form
http://zendguru.wordpress.com/2008/10/23/zend-form-decorators/

Comments (1)

Follow

Get every new post delivered to your Inbox.