There’s nothing quite like having a functioning application emerge out of the controlled chaos we know as The Development Process. In Part 8 of the ongoing saga describing how to build a real world blog application using the Zend Framework we finally reach the point at which we concentrate on blog entries. At the end of this Part, we will be able to create and edit entries in preparation for Part 9 when we will explore displaying them to the world!

Previously: Example Zend Framework Blog Application Tutorial – Part 7: Authorisation with Zend_Acl and Revised Styling

The reason displaying entries is not addressed here is simple. Display requires a lot of Zend_View work which is deserving of an article to itself before we go too far. A few of you have already noted in comments about our suspicious lack of View Helper usage ;) . By now a few uncomfortable flaws should be becoming apparent in how some template content is becoming duplicated.

So, on with the show already!

Step 1: Adding an Entry Controller and Add Action Template

The first step to creating new entries will be writing an Admin_EntryController class (the prefix is needed since it’s situated in the Admin Module) with an addAction() method and matching template.

The template will utilise a new Zend_Form object for gathering the input used to create a new entry, as well as offering an editing View.

We’ll start with the Controller, added at /application/admin/controllers/EntryController.php:

[geshi lang=php]< ?php

class Admin_EntryController extends Zend_Controller_Action
{

public function addAction()
{}

public function listAction()
{}

public function editAction()
{}

public function deleteAction()
{}

}[/geshi]

The EntryController needs four basic methods. We intend creating, editing and deleting entries, as well as listing all entries to an Author to select those options.

We'll concentrate on the addAction() method first, so let's add an appropriate template at /application/admin/views/scripts/entry/add.phtml which has an old friend referred to:

[geshi lang=php]

Where I am not understood, it shall be concluded that something very useful and profound is couched underneath.
- Jonathan Swift

< ?php if($this->failedValidation): ?>

Some problems were detected with the submitted form.

< ?php endif; ?>

< ?php echo $this->entryForm ?>[/geshi]

Let’s accompany our blog entry forms with a little Jonathan Swift for inspiration :) . We once again meet a validation problem message identical to the one in our login form. As we’re duplicating this message we’re going to have to consider a means later on of isolating such commonly used feedback messages for reuse somewhere in Part 9.

To keep our styling synchronised with our intended form output, add the following to /public/css/style.css:

[geshi lang=css]textarea {
width: 76%;
height: 30em;
}[/geshi]

This should provide a decent styling for textareas for most browsers. Perhaps even Safari which I noticed recently is displaying some of my text fields poorly.

Step 2: Assembling the Entry Form with Zend_Form

We’ve already had a fairly detailed look at Zend_Form back in Part 6 when we wrote a subclass to contain some standard and specific decorator arrays for form elements, and created our Login Form example. The same principles used there also apply here with very few changes. Once again we are using the shorter array-based syntax over multiple method calls. One of the differences to take note of is that I’ve used two new options attribs and value which no form developer could live without!

We’ll start with a new class called ZFBlog_Form_EntryAdd located at /library/ZFBlog/Form/EntryAdd.php:

[geshi lang=php]< ?php

class ZFBlog_Form_EntryAdd extends ZFBlog_Form
{

public function init()
{
$this->setAction(‘/admin/entry/add’);

// Display Group #1 : Entry Data

$this->addElement(‘text’, ‘title’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Title:’,
‘attribs’ => array(
‘maxlength’ => 200,
‘size’ => 80
),
‘validators’ => array(
array(‘StringLength’, false, array(3,200))
),
‘required’ => true
));

$this->addElement(‘text’, ‘date’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Date:’,
‘attribs’ => array(
‘maxlength’ => 16,
‘size’ => 16
),
‘value’ => Zend_Date::now()->toString(‘yyyy-MM-dd HH:mm’),
‘validators’ => array(
array(‘Date’, false, array(‘yyyy-MM-dd HH:mm’, ‘en’))
),
‘required’ => true
));

$this->addElement(‘textarea’, ‘entrybody’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Entry Body:’,
‘required’ => true
));

$this->addElement(‘textarea’, ‘entrybodyextended’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Extended Body:’
));

$this->addDisplayGroup(
array(‘title’,'date’,'entrybody’,'entrybodyextended’), ‘entrydata’,
array(
‘disableLoadDefaultDecorators’ => true,
‘decorators’ => $this->_standardGroupDecorator,
‘legend’ => ‘Entry’
)
);

// Display Group #2 : Submit

$this->addElement(‘submit’, ‘submit’, array(
‘decorators’ => $this->_buttonElementDecorator,
‘label’ => ‘Save’
));

$this->addDisplayGroup(
array(‘submit’), ‘entrydatasubmit’,
array(
‘disableLoadDefaultDecorators’ => true,
‘decorators’ => $this->_buttonGroupDecorator,
‘class’ => ‘submit’
)
);
}
}[/geshi]

I threw in a sprinkling of Zend_Date above to format the current date to a format compatible with a MySQL database. Zend_Date is also used in the background by the Zend_Validator_Date class which is why the date formatting is identical to both. Note that the formatting is not the PHP regular style used by the date() function, but instead uses ISO format specifiers. This offers a great deal of flexibility if you want something other than MySQL formatted dates.

Finally note that there is no “required” flag for the “entrybodyextended” element since an extended body is optional.

We’re not quite done yet. Our two textareas, as discussed way back in Part 1, are being designed to accept HTML input since I really can’t be bothered to play with custom formatting tags and such. This obviously raises the risk that I, another author, or someone who’s guessed my password may, either by mistake or intent, add some Cross-Site Scripting (XSS) into the mix and introduce a devastating security exploit. We can’t have that now!

Step 3: Filtering Entries using a HTMLPurifier based Custom Filter

What we will do now is attach a custom filter to the textareas containing our Entry data that cleans up any HTML input, removes XSS, and as a bonus converts any non-HTML input into formatted HTML. This, for example, covers our text paragraphs by wrapping them in <p> tags.

My favourite library for achieving this is HTMLPurifier which I consider one of those much under utilised libraries in PHP. To my knowledge, there is nothing out there to beat its comprehensive feature list. Its finest feature is that it actually understands HTML across multiple standards. Input is tokenised, parsed, passed through a whitelist (the opposite of a detection blacklist), reformed as perfectly correct valid output, and then it can optionally wrap stuff like plain text in paragraphs. All for your preferred DTD. Either you’re using HTMLPurifier, or you are using something second or third rate, and I have no qualms whatsoever in stating that as a fact. I cannot praise this library more highly.

To start, you’ll need to download a copy of HTMLPurifer 3.1.0rc1 (which is the latest release at the time of writing) and copy the contents of the package’s /library directory into our blog application’s /library. Since HTMLPurifer follows the PEAR Convention for class naming and file location, we need make no changes to our include_path. If you prefer, you can also install HTMLPurifer from its PEAR channel as described on the download page. Although it’s a release candidate I haven’t run into any problems using it other than my unexplainable habit of mispelling HTMLPurifier as HTMLPurifer which has led to a few frustrating hair pulling moments!

HTMLPurifier’s perfection is not without a performance cost. To improve performance it will utilise a HTML definition cache. Add a new base directory at /cache/htmlpurifier and grant permissions sufficient to let the webserver write files there. Don’t get too caught up over performance as security isn’t an area you want to needlessly play Scrooge with ;) . The performance cost is really only noticeable if using it for post-processing of output being sent to visitors. The cache coupled with the fact HTMLPurifier is used only in the backend Administration eliminates any such cost for the purposes of our application.

Let’s introduce our custom filter classes. I’m saving them using a mirror directory structure of the Zend Framework as usual. Here a standard one I usually use with HTMLPurifier saved to /library/ZFBlog/Filter/HTMLPurifier.php:

[geshi lang=php]< ?php

class ZFBlog_Filter_HTMLPurifier implements Zend_Filter_Interface
{

protected $_htmlPurifier = null;

public function __construct($options = null)
{
$config = null;
if (!is_null($options)) {
$config = HTMLPurifier_Config::createDefault();
foreach ($options as $option) {
$config->set($option[0], $option[1], $option[2]);
}
}
$this->_htmlPurifier = new HTMLPurifier($config);
}

public function filter($value)
{
return $this->_htmlPurifier->purify($value);
}

}[/geshi]

HTMLPurifier options have three distinct elements we can pass to this filter as an array. We could stop here, passing filter options for every form, but this is a very general filter class whose sole purpose is to pass options into HTMLPurifier, so let’s add a subclass of this specifically for HTML textual input with predefined options at /library/ZFBlog/Filter/HtmlBody.php:

[geshi lang=php]< ?php

class ZFBlog_Filter_HtmlBody extends ZFBlog_Filter_HTMLPurifier
{

public function __construct($newOptions = null)
{
$options = array(
array('Cache', 'SerializerPath',
Bootstrap::$root . '/cache/htmlpurifier'
),
array('HTML', 'Doctype', 'XHTML 1.0 Strict'),
array('HTML', 'Allowed',
'p,em,h1,h2,h3,h4,h5,strong,a[href],ul,ol,li,code,pre,'
.'blockquote,img[src|alt|height|width],sub,sup'
),
array('AutoFormat', 'Linkify', 'true'),
array('AutoFormat', 'AutoParagraph', 'true')
);

if (!is_null($newOptions)) {
// I'll let HTMLPurifier overwrite original options
// with new ones rather than filter them myself
$options = array_merge($options, $newOptions);
}

parent::__construct($options);
}

}[/geshi]

This new subclass passes specific options to HTMLPurifier. We provide the path to the cache directory created previously, inform the library our output should conform to XHTML 1.0 Strict, add a whitelist of allowed tags and attributes, and finally enable two optional formatting helpers to auto paragraph output (wrapped with &ltp> tags) and transform URLs into hyperlinks. If ZFBlog_Filter_HtmlBody needs further adjustment we can pass it options when attaching this filter to our form elements.

This really is how HTMLPurifer works. By using sensible defaults, configuration before use is extremely simple.

With our two custom filters in tow, we now need to make sure the Zend Framework can actually find them! We’ve done this previously actually when registering a custom decorator path with Zend_Form. Let’s repeat the process. Here’s an updated ZFBlog_Form class from /library/ZFBlog/Form.php:

[geshi lang=php]< ?php

class ZFBlog_Form extends Zend_Form
{

protected $_standardElementDecorator = array(
'ViewHelper',
array('LabelError', array('escape'=>false)),
array(‘HtmlTag’, array(‘tag’=>’li’))
);

protected $_buttonElementDecorator = array(
‘ViewHelper’
);

protected $_standardGroupDecorator = array(
‘FormElements’,
array(‘HtmlTag’, array(‘tag’=>’ol’)),
‘Fieldset’
);

protected $_buttonGroupDecorator = array(
‘FormElements’,
‘Fieldset’
);

protected $_noElementDecorator = array(
‘ViewHelper’
);

public function __construct($options = null)
{
// Path setting for custom classes MUST ALWAYS be first!
$this->addElementPrefixPath(‘ZFBlog_Form_Decorator’, ‘ZFBlog/Form/Decorator/’, ‘decorator’);
$this->addElementPrefixPath(‘ZFBlog_Filter’, ‘ZFBlog/Filter/’, ‘filter’);

$this->_setupTranslation();

parent::__construct($options);

$this->setAttrib(‘accept-charset’, ‘UTF-8′);
$this->setDecorators(array(
‘FormElements’,
‘Form’
));
}

protected function _setupTranslation()
{
if (self::getDefaultTranslator()) {
return;
}
$path = Bootstrap::$root . ‘/translate/forms.php’;
$translate = new Zend_Translate(‘array’, $path, ‘en’);
self::setDefaultTranslator($translate);
}

}[/geshi]

Now Zend_Form can use our custom filters. The last thing we do is attach our new HtmlBody custom filter to our new form along with a few other filters for good measure:

[geshi lang=php]< ?php

class ZFBlog_Form_EntryAdd extends ZFBlog_Form
{

public function init()
{
$this->setAction(‘/admin/entry/add’);

// Display Group #1 : Entry Data

$this->addElement(‘text’, ‘title’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Title:’,
‘attribs’ => array(
‘maxlength’ => 200,
‘size’ => 80
),
‘validators’ => array(
array(‘StringLength’, false, array(3,200))
),
‘filters’ => array(‘StringTrim’),
‘required’ => true
));

$this->addElement(‘text’, ‘date’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Date:’,
‘attribs’ => array(
‘maxlength’ => 16,
‘size’ => 16
),
‘value’ => Zend_Date::now()->toString(‘yyyy-MM-dd HH:mm’),
‘validators’ => array(
array(‘Date’, false, array(‘yyyy-MM-dd HH:mm’, ‘en’))
),
‘required’ => true
));

$this->addElement(‘textarea’, ‘entrybody’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Entry Body:’,
‘filters’ => array(‘HtmlBody’),
‘required’ => true
));

$this->addElement(‘textarea’, ‘entrybodyextended’, array(
‘decorators’ => $this->_standardElementDecorator,
‘label’ => ‘Extended Body:’,
‘filters’ => array(‘HtmlBody’)
));

$this->addDisplayGroup(
array(‘title’,'date’,'entrybody’,'entrybodyextended’), ‘entrydata’,
array(
‘disableLoadDefaultDecorators’ => true,
‘decorators’ => $this->_standardGroupDecorator,
‘legend’ => ‘New Entry’
)
);

// Display Group #2 : Submit

$this->addElement(‘submit’, ‘submit’, array(
‘decorators’ => $this->_buttonElementDecorator,
‘label’ => ‘Save’
));

$this->addDisplayGroup(
array(‘submit’), ‘entrydatasubmit’,
array(
‘disableLoadDefaultDecorators’ => true,
‘decorators’ => $this->_buttonGroupDecorator,
‘class’ => ‘submit’
)
);
}
}[/geshi]

Let’s get this form attached to a View now.

More >