A Zend Framwork compound form element for dates

21st March 2011

A while ago I needed to ask a user for their date of birth on a Zend_Form. The design showed three separate select elements to do this:

Screen shot of a 3 select boxes for a date on a form

A little bit of googling found this site http://codecaine.co.za/posts/compound-elements-with-zend-form which has not unfortunately disappeared, so the code in this article owes a lot of the author of that article.

It turns out to be remarkably simple to create a single Zend Form element that is rendered as multiple form elements. We create an element object and a view helper object and we're done. Usage then looks like:

<?php

class Application_Form_Details extends Zend_Form
{
    public function init()
    {
        $this->addPrefixPath('App_Form''App/Form/');

        // other elements before 
        
        $this->addElement('date''date_of_birth', array(
            'label' => 'Date of birth:'
        ));
        
        // other elements after

        $this->addElement('submit''Go');
    }
}

Obviously, this form lives in application/forms/Detail.php and is rendered as usual in a view script. In our form definition, we have added an element called 'date' and with the addition of the addPrefixPath call have told the form that in addition to using the standard Zend Framework form elements, also look in library/App/Form. (Incidentally, we can also now override any supplied form element by simply dropping a replacement into the libraryApp/Form folder.)

The date form element lives in library/App/Form/Element/Date.php as Zend_Form knows to look in a subfolder for App/Form called Elements for any element objects and will look in the Decorator/ sub folder for decorator objects.

The Date element looks like this:

// Based on code from
// http://codecaine.co.za/posts/compound-elements-with-zend-form
class App_Form_Element_Date extends Zend_Form_Element_Xhtml
{
    public $helper 'formDate';

    public function isValid ($value$context null)
    {
        if (is_array($value)) {
            $value $value['year'] . '-' .
                $value['month'] . '-' .
                $value['day'];
            
            if($value == '--') {
                $value null;
            }
        }

        return parent::isValid($value$context);
    }

    public function getValue()
    {
        if(is_array($this->_value)) {
            $value $this->_value['year'] . '-' .
                $this->_value['month'] . '-' .
                $this->_value['day'];

            if($value == '--') {
                $value null;
            }
            $this->setValue($value);
        }

        return parent::getValue();
    }

}

There's quote a lot going on here, but it should be fairly clear. Firstly we specify the name of the view helper to use when rendering this element to be formDate which we will write. We know this element is going to consist of three select boxes and so these will end up being an array of posted data for day, month and year. As a result, we need to override isValid() to turn our array back into a string and then call up to the parent's isValid() in order to do the actual validation required. We also need to override getValue() in the same way to ensure that it is also a string. Again we call up to the parent's getValue() as that does filtering.

That's all there is to the element itself, so now we turn out attention to the view helper that will render the element. This lives in library/App/View/Helpers/FormDate.php and as per my view helpers post, we need to tell the view about that folder via application.ini:

autoloadernamespaces[] = "App_"
resources.view.helperPath.App_View_Helper "App/View/Helper"

The formDate view helper code looks like this:

<?php

// based on code from
// http://codecaine.co.za/posts/compound-elements-with-zend-form

class App_View_Helper_FormDate extends Zend_View_Helper_FormElement
{
    public function formDate ($name$value null$attribs null)
    {
        // separate value into day, month and year
        $day '';
        $month '';
        $year '';
        if (is_array($value)) {
            $day $value['day'];
            $month $value['month'];
            $year $value['year'];
        } elseif (strtotime($value)) {
            list($year$month$day) = explode('-'date('Y-m-d'strtotime($value)));
        }

        // build select options
        $dayAttribs = isset($attribs['dayAttribs']) ? $attribs['dayAttribs'] : array();
        $monthAttribs = isset($attribs['monthAttribs']) ? $attribs['monthAttribs'] : array();
        $yearAttribs = isset($attribs['yearAttribs']) ? $attribs['yearAttribs'] : array();

        $dayMultiOptions = array('' => '');
        for ($i 1$i 32$i ++)
        {
            $index str_pad($i2'0'STR_PAD_LEFT);
            $dayMultiOptions[$index] = str_pad($i2'0'STR_PAD_LEFT);
        }
        $monthMultiOptions = array('' => '');
        for ($i 1$i 13$i ++)
        {
            $index str_pad($i2'0'STR_PAD_LEFT);
            $monthMultiOptions[$index] = date('F'mktime(nullnullnull$i01));
        }

        $startYear date('Y');
        if (isset($attribs['startYear'])) {
            $startYear $attribs['startYear'];
            unset($attribs['startYear']);
        }

        $stopYear $startYear 10;
        if (isset($attribs['stopYear'])) {
            $stopYear $attribs['stopYear'];
            unset($attribs['stopYear']);
        }

        $yearMultiOptions = array('' => '');

        if ($stopYear $startYear) {
            for ($i $startYear$i >= $stopYear$i--) {
                $yearMultiOptions[$i] = $i;
            }
        } else {
            for ($i $startYear$i <= $stopYear$i++) {
                $yearMultiOptions[$i] = $i;
            }
        }

        // return the 3 selects separated by &nbsp;
        return
            $this->view->formSelect(
                $name '[day]',
                $day,
                $dayAttribs,
                $dayMultiOptions) . '&nbsp;' .
            $this->view->formSelect(
                $name '[month]',
                $month,
                $monthAttribs,
                $monthMultiOptions) . '&nbsp;' .
            $this->view->formSelect(
                $name '[year]',
                $year,
                $yearAttribs,
                $yearMultiOptions
            );
    }
}

Again, there's a fair amount of code, but I expect that it's pretty self-explanatory. Essentially, we have a lot of set up in order to render three select boxes; one for the day, month and year. I decided to use month names but it's easy enough to change to numbers. In terms of configuration, you need to be able to specify the start and stop year numbers. These are then passed in as options when calling addElement.

That's about it. Two separate files is all you need to create a Zend_Form element object that is implemented via compound elements. It also follows that you can do the same for any other conceptual element that you want to be rendered as multiple elements in the page.

Zend Framework View Helpers

14th March 2011

I can't seem to find an article here that consolidates my thoughts on Zend Framework's view helper system, so I thought I'd better correct that. Zend Framework's Zend_View component supports helper methods known as view helpers. They are used like this in a view script:

<?php echo $this->myHelper('myParam1'); ?>

Behind the scenes, this is implemented as a method within a class something like this:

<?php

class Zend_View_Helper_MyHelper extends Zend_View_Helper_Abstract
{
    public function myHelper($myParam1)
    {
        $html '';
        // some logic that fills in $html.
        return $html;
    }
}

Note that by convention, view helpers return the string and then the view script echos it and the Zend_View_Helper_ section of the class name is known as the prefix.

A typical Zend Framework project using Zend_Application, such as that generated using the zf command line tool, will have a folder called helpers within the views folder for each module. There will also be a helpers folder within the layouts folder too. If you place your view helper in one of these helpers folders, then the prefix is Zend_View_Helper_.

We can also tell the view object about our own view helper folder. Typically this will live in the library/App/View/Helper/ folder and so, you probably want a prefix of App_View_Helper_. To do this, we simply add this line to application.ini:

resources.view.helperPath.App_View_Helper_ "App/View/Helper/"

The view helper now lives in App/View/Helper/MyHelper.php and looks like this:

<?php

class App_View_Helper_MyHelper extends Zend_View_Helper_Abstract
{
    public function myHelper($myParam1)
    {
        $html '';
        // some logic that fills in $html.
        return $html;
    }
}

Due to the way Zend Framework's plugin loader works, our view script doesn't change at all as the view object's plugin loader will look across all registered paths to find the view helper specified in the view script.

I tend to prefer using the App folder over the helpers folder within layouts as I like to have one place to go to for my "site-wide" classes and so my App folder contains front controller plugins, validators, filters, form elements as well.

As you have extended Zend_View_Helper_Abstract for creating your own helper, you have access to the view object itself too. This is helpful for accessing other view helpers, such as escape. My personal preference is for my view helpers to manage their own escaping as requires as that way I can include html tags within my helper.

For example, suppose we want to create a table from an array that looks like this:

$data = array();
$data[] = array('Name''Email');
$data[] = array('Alison''alison@example.com');
$data[] = array('Bert''bert@example.com');
$data[] = array('Charlie''charlie@example.com');

Our view script may look like this:

<?php echo $this->tabulate($this->data, array('class'=>'tabulated')); ?>

(assuming our designer wants a class name attached to the table for styling.)

The view helper itself would then look something like:

<?php

class App_View_Helper_Tabulate extends Zend_View_Helper_Abstract
{
    public function tabulate ($data$attribs = array())
    {
        $attribString '';
        foreach ($attribs as $key => $value) {
            $attribString .= ' ' $key .'="' $value '"';
        }

        $header array_shift($data);
        $html "<table $attribString>\n<tr>\n";
        foreach ($header as $cell) {
            $escapedCell $this->view->escape($cell);
            $html .= "<th>$escapedCell</th>\n";
        }
        $html .= "</tr>\n";
        foreach ($data as $row) {
            $html .= "<tr>\n";
            foreach ($row as $cell) {
                $escapedCell $this->view->escape($cell);
                $html .= "<td>$escapedCell</td>\n";
            }
            $html .= "</tr>\n";
        }

        $html .= '</table>';
        return $html;
    }
}

As you can see, we cannot escape the output of the view helper, so the view helper has to do its own escaping via the view property.

If you end up using the same set of view helpers on each project you do, then consider putting then into a vendor library. This is the same concept as using the App folder, but you use a different name (I use Akrabat) and store them in a separate vcs repository. This allows you easy reuse. You add a new line to application.ini to pick them up:

resources.view.helperPath.Akrabat_View_Helper_ "Akrabat/View/Helper/"

and you're done. Note that the order of the helperPath in application.ini is important. You want the vendor path before the App path so that you can override your vendor ones specifically within a project if you need to.

resources.view.helperPath.Akrabat_View_Helper_ "Akrabat/View/Helper/"
resources.view.helperPath.App_View_Helper_ "App/View/Helper/"

Plugin helper path order is one of the more frustrating things about the plugin loaders used within Zend Framework and affects form elements, validators, filters and view helpers to name but a few. So if you discover that a view helper isn't being called when you think it ought to be, then check that it's not being overridden further down the chain. You can of course use this to your advantage and override a default Zend Framework view helper by simply creating an App version with the same name.

Using your own View object with Zend_Application

16th February 2011

Let's say that you want to use your own view object within your Zend Framework application.

Creating the view object is easy enough in library/App/View.php:

class App_View extends Zend_View
{
    // custom methods here
}

along with adding the App_ namespace to the the autoloader in application.ini:

autoloadernamespaces[] = "App_"

All we need to now is get Zend_Application to bootstrap with our new view class. There are two ways of doing this: within Bootstrap.php or using a custom resource.

_initView() in Bootstrap.php

At first blush, the code looks quite easy. In application/Bootstrap.php, we add our own method that creates the view object and assigns it to the viewRenderer:


class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initView()
    {
        $view = new App_View();

        $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer();
        $viewRenderer->setView($view);
        Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);
        return $view;
    }
}

As we have named the method _initView(), our method will take precedence over the built in View resource and be used instead. However, this implementation will ignore any view options that are configured in application.ini using the resources.view key, so a better method is this:

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initView()
    {
        $resources $this->getOption('resources');
        $options = array();
        if (isset($resources['view'])) {
            $options $resources['view'];
        }
        $view = new App_View($options);

        if (isset($options['doctype'])) {
            $view->doctype()->setDoctype(strtoupper($options['doctype']));
            if (isset($options['charset']) && $view->doctype()->isHtml5()) {
                $view->headMeta()->setCharset($options['charset']);
            }
        }
        if (isset($options['contentType'])) {
            $view->headMeta()->appendHttpEquiv('Content-Type'$options['contentType']);
        }
        
        $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer();
        $viewRenderer->setView($view);
        Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);
        return $view;
    }

}

This version takes into account your configuration settings and behaves the same as the View resource provided by Zend Framework. The only difference is that we're now using App_View.

Custom resource

Another option is to override Zend_Application_Resource_View with our own view resource. In this case, we create a class called App_Resource_View stored in library/App/Resource/View.php. We only need to override one method, getView():

class App_Resource_View extends Zend_Application_Resource_View
{
    public function getView()
    {
        if (null === $this->_view) {
            $options $this->getOptions();
            $this->_view = new App_View($options);

            if (isset($options['doctype'])) {
                $this->_view->doctype()->setDoctype(strtoupper($options['doctype']));
                if (isset($options['charset']) && $this->_view->doctype()->isHtml5()) {
                    $this->_view->headMeta()->setCharset($options['charset']);
                }
            }
            if (isset($options['contentType'])) {
                $this->_view->headMeta()->appendHttpEquiv('Content-Type'$options['contentType']);
            }
        }
        return $this->_view;
    }
}

Essentially, all I have done is replace the class of the view object to be App_View and left everything else alone so that it behaves the same as the default View resource.

To get Zend_Application to load our custom resource, we just add one line to application.ini:

pluginPaths.App_Resource "App/Resource"

We now have a reusable resource that will load our own View class and can easily take it from project to project.

Remove index.php from your URL

17th January 2011

One thing you may have noticed with Zend Framework projects that use the baseUrl() view helper to reference CSS and other static files is that it doesn't work if the URL contains contains index.php.

Let's explain, by using code from the tutorial.

In the layout.phtml file, we link to a CSS file like this:

<?php echo $this->headLink()->prependStylesheet($this->baseUrl().'/css/site.css'); ?> 

This code uses a baseUrl() view helper that looks like this

<?php 
class Zend_View_Helper_BaseUrl 
{ 
    function baseUrl() 
    { 
        $fc Zend_Controller_Front::getInstance(); 
        return $fc->getBaseUrl(); 
    } 
}

When you go to http://localhost/zf-tutorial/ you get an page that looks like this:

Screen short of Zend Framework tutorial

However, if you go to http://localhost/zf-tutorial/index.php, you get this:

zf-tutorial_no_css.jpg

This happens because the baseURL used in the href of the CSS link in the second case is /zf-tutorial/public/index.php/css/site.css rather than the correct /zf-tutorial/public/css/site.css.

Solution using mod_rewrite

One way to fix this is to use a mod_rewrite rule in your .htaccess file:


RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /([^/]+/)*index.php 
RewriteRule ^index.php(.*)$ /zf-tutorial/public$[R=301,L]

I did have to put the full subdirectory into the rewrite rule to get it to work.

The quick and dirty solution

At the top of your index.php file take out the index.php from the REQUEST_URI:


$_SERVER["REQUEST_URI"] = str_replace('index.php','',$_SERVER["REQUEST_URI"]);

This obviously works, but feels a bit "hacky"! There's probably other ways to solve it too, but once I found the rewrite rule, I stuck with it.

Handling exceptions in a Front Controller plugin

20th December 2010

If you have a Zend Framework Front Controller plugin which throws an exception, then the action is still executed and then the error action is then called, so that the displayed output shows two actions rendered, with two layouts also rendered. This is almost certainly not what you want or what you expected.

This is how to handle errors in a Front Controller plugin:

  1. Prefer preDispatch() over dispatchLoopStartup() as it is called from within the dispatch loop
  2. Catch the exception and the modify the request so that the error controller's error action is dispatched.
  3. Create an error handler object so that the error action works as expected.

This is the code:

<?php

class Application_Plugin_Foo extends Zend_Controller_Plugin_Abstract
{
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    { 
        try {

            // do something that throws an exception

        } catch (Exception $e) {
            // Repoint the request to the default error handler
            $request->setModuleName('default');
            $request->setControllerName('error');
            $request->setActionName('error');

            // Set up the error handler
            $error = new Zend_Controller_Plugin_ErrorHandler();
            $error->type Zend_Controller_Plugin_ErrorHandler::EXCEPTION_OTHER;
            $error->request = clone($request);
            $error->exception $e;
            $request->setParam('error_handler'$error);
        }
    }

}

That's it.