Tutorials

Zend Framework Blog Application Tutorial: Part 8: Creating and Editing Blog Entries with a dash of HTMLPurifier

Oct 21, 2008 insic 11 Comments

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!

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:

<?php

class Admin_EntryController extends Zend_Controller_Action
{

    public function addAction()
    {}

    public function listAction()
    {}

    public function editAction()
    {}

    public function deleteAction()
    {}

}

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:

<p>Where I am not understood, it shall be concluded that something very useful and profound is couched underneath.<br /><em>- Jonathan Swift</em></p>

<?php if($this->failedValidation): ?>
    <p class=“error”>Some problems were detected with the submitted form.</p>
<?php endif; ?>

<?php echo $this->entryForm ?>

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:

textarea {
    width: 76%;
    height: 30em;
}

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:

<?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’
            )
        );
    }
}

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:

<?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);
    }

}

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:

<?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);
    }

}

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:

<?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);
    }

}

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:

<?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’
            )
        );
    }
}

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

Step 4: Output Form from Add Action Template

Let’s revise our EntryController to pass our sparkling new form to the addAction() method’s associated template.

<?php

class Admin_EntryController extends Zend_Controller_Action
{

    public function addAction()
    {
        $form = new ZFBlog_Form_EntryAdd;

        if (!$this->getRequest()->isPost()) {
            $this->view->entryForm = $form;
            return;
        } elseif (!$form->isValid($_POST)) {
            $this->view->failedValidation = true;
            $this->view->entryForm = $form;
            return;
        }
    }

}

Now, keeping in mind we should keep track of what error messages need to be cleaned up into a shorter message (as I noted previously, I like short error messages attached to element labels), let’s add the Zend_Validate_Date default errors for replacement in our translation array in /translate/forms.php:

<?php

return array(
    Zend_Validate_NotEmpty::IS_EMPTY => ‘Required’,
    Zend_Validate_StringLength::TOO_SHORT => ‘Minimum Length of %min%’,
    Zend_Validate_StringLength::TOO_LONG => ‘Maximum Length of %max%’,
    Zend_Validate_Date::NOT_YYYY_MM_DD => ‘Must use YYYY-MM-DD format’,
    Zend_Validate_Date::INVALID => ‘Not valid date’,
    Zend_Validate_Date::FALSEFORMAT => ‘Invalid date format’,
);

Let’s give it a whirl! Open up http://zfblog/admin/entry/add in your browser (you may need to login as Joe Bloggs first with the username “joebloggs” and password “password” setup earlier) and marvel at the new form.

We’re still not done. If a valid form is submitted we’re going to need to store it to the database!

Step 5: Storing New Enties to the Database

We’ve already put in place an Entries Model when we were setting up our database, so saving entries is just a simple addition to our EntryController.

<?php

class Admin_EntryController extends Zend_Controller_Action
{

    public function addAction()
    {
        $form = new ZFBlog_Form_EntryAdd;

        if (!$this->getRequest()->isPost()) {
            $this->view->entryForm = $form;
            return;
        } elseif (!$form->isValid($_POST)) {
            $this->view->failedValidation = true;
            $this->view->entryForm = $form;
            return;
        }
       
        $values = $form->getValues();
        $table = new Entries;
        $data = array(
            ‘title’ => $values[‘title’],
            ‘date’ => $values[‘date’],
            ‘author’ => Zend_Auth::getInstance()->getIdentity()->username,
            ‘author_id’ => Zend_Auth::getInstance()->getIdentity()->id,
            ‘body’ => $values[‘entrybody’],
            ‘extended_body’ => $values[‘entrybodyextended’]
        );
        $table->insert($data);
        $this->view->entrySaved = true;
    }

}

I’ve left the class as is, but at some point I’ll refactor this. One of the few worthwhile aesthetic goals of any source code is to keep methods as short as possible. Almost half the above method is mapping form names to database field names. If used a more direct convention, we could have saved reading space and had a shorter method.

We now just need to add another of those persistently untidy confirmation messages to be cleaned up later to our Add Action’s template ;-).

<p>Where I am not understood, it shall be concluded that something very useful and profound is couched underneath.<br /><em>- Jonathan Swift</em></p>

<?php if($this->failedValidation): ?>
    <p class=“error”>Some problems were detected with the submitted form.</p>
<?php endif; ?>

<?php if($this->entrySaved): ?>
    <p class=“success”>New entry has been saved.</p>
<?php else: ?>
    <?php echo $this->entryForm ?>
<?php endif; ?>

Go ahead and try out the new entry addition. As noted in the introduction we’ll cover the actual display of Entries in the next Part, so for now check out the results on the database using PhpMyAdmin or similar. You notice paragraphs are correctly wrapped with <p> and links linkified as HTMLPurifier makes it’s presence felt. Tags and attributes that were not added to our whitelist will also be stripped.

Step 6: Editing Entries

Editing entries is a fairly simple addition to our zoo. In fact, it’s necessary. We already have in place a form class for inputting entries and it’s only a small step from there to add in the old values for editing.

The only hoop to jump through is reversing the two default formatting steps that HTMLPurifier performs before entries are saved to the database. This can be a confusing problem because many people, I suspect, assume populating Zend_Form with values will output those values exactly. Not so. If you have added Filters to a form object you have actually told Zend_Form that only values passing this filter are entirely valid, so guess what? Yes, populating data is filtered when added to a form object. Since here we filter all entry body data through HTMLPurifier, while the actual input from the user is not, we need to disable this filter before displaying the populated edit form.

There is another strategy you could also use. Instead of applying the filter to the form, apply it instead within the Model using overridden save() and update() methods. I’ve elected not to do this since I quite like having the filtering as part of the form object itself. It doesn’t change the fact that that hoop exists, but locating it in the Model could aid in reuse since it’s now decoupled from a form object and managed closer to the database. But anyway, that’s what Refactoring is for! So let’s forge ahead.

We’ll start with implementing the listAction() for our Entry Controller so we can view a list of all entries for editing/deletion. For the moment this does not include paging.

<?php

class Admin_EntryController extends Zend_Controller_Action
{

    public function addAction()
    {
        $form = new ZFBlog_Form_EntryAdd;

        if (!$this->getRequest()->isPost()) {
            $this->view->entryForm = $form;
            return;
        } elseif (!$form->isValid($_POST)) {
            $this->view->failedValidation = true;
            $this->view->entryForm = $form;
            return;
        }
       
        $values = $form->getValues();
        $table = new Entries;
        $data = array(
            ‘title’ => $values[‘title’],
            ‘date’ => $values[‘date’],
            ‘author’ => Zend_Auth::getInstance()->getIdentity()->username,
            ‘author_id’ => Zend_Auth::getInstance()->getIdentity()->id,
            ‘body’ => $values[‘entrybody’],
            ‘extended_body’ => $values[‘entrybodyextended’]
        );
        $table->insert($data);
        $this->view->entrySaved = true;
    }
   
    public function listAction()
    {
        $table = new Entries;
        // SELECT title, date, id FROM entries
        $rows = $table->fetchAll(
            $table->select()->from($table, array(‘title’,‘date’,‘id’))
        );
        $this->view->entries = $rows->toArray();
    }
   
    public function editAction()
    {}
   
    public function deleteAction()
    {}

}

The associated view simply iterates the array of entries over a foreach construct to replicate an entry list.

/application/admin/views/scripts/entry/list.phtml

<?php if (count($this->entries) > 0): ?>
    <ul class=“entry-list”>
    <?php foreach($this->entries as $entry): ?>
        <li>
            <a href=“/admin/entry/edit/id/<?php echo $entry['id'] ?>”>
                <?php echo $entry[‘title’] ?></a>
                – <a href=“/admin/entry/edit/id/<?php echo $entry['id'] ?>”>Edit</a>
                | <a href=“/admin/entry/delete/id/<?php echo $entry['id'] ?>”>Delete</a>
        </li>
    <?php endforeach; ?>
    </ul>
<?php else: ?>
    <p class=“error”>There are no entries for this blog.</p>
<?php endif; ?>

It looks pretty darn ugly, but will fit for now. Logged in as an Author, you can view the list of existing entries assuming you’ve added a few test ones while testing our new add entry functionality. Each will have a link to edit or delete that entry. We’ll cover the URL form I used shortly.

Let’s create a new form for editing entries. Luckily we have the Entry Add form, so all we need do is subclass this to make a few small adjustments! Add this form as /library/ZFBlog/Form/EntryEdit.php:

<?php

class ZFBlog_Form_EntryEdit extends ZFBlog_Form_EntryAdd
{
   
    public function init()
    {
        parent::init();
        $this->setAction(‘/admin/entry/edit’);
       
        // What entry id are we editing?!
        $this->addElement(‘hidden’, ‘id’, array(
            ‘decorators’ => $this->_noElementDecorator,
            ‘validators’ => array(
                ‘Digits’
            ),
            ‘required’ => true
        ));
       
        $this->getDisplayGroup(‘entrydata’)->setLegend(‘Edit Entry’);
        $this->getDisplayGroup(‘entrydata’)->addElement(
            $this->getElement(‘id’)
        );
    }
   
}

Isn’t a little reuse worthwhile? ;-)

Before displaying our Edit Form we need to populate it with the data we’re editing. This involves capturing the entry from the database, reversing any HTMLPurifier filtering, disabling the Form’s filter chain, and only then re-populating the form. We need to be careful the workflow only disables filters for form display – they should remain in force for any submitted edits.

<?php

class Admin_EntryController extends Zend_Controller_Action
{

    public function addAction()
    {
        $form = new ZFBlog_Form_EntryAdd;

        if (!$this->getRequest()->isPost()) {
            $this->view->entryForm = $form;
            return;
        } elseif (!$form->isValid($_POST)) {
            $this->view->failedValidation = true;
            $this->view->entryForm = $form;
            return;
        }
       
        $values = $form->getValues();
        $table = new Entries;
        $data = array(
            ‘title’ => $values[‘title’],
            ‘date’ => $values[‘date’],
            ‘author’ => Zend_Auth::getInstance()->getIdentity()->username,
            ‘author_id’ => Zend_Auth::getInstance()->getIdentity()->id,
            ‘body’ => $values[‘entrybody’],
            ‘extended_body’ => $values[‘entrybodyextended’]
        );
        $table->insert($data);
        $this->view->entrySaved = true;
    }
   
    public function listAction()
    {
        $table = new Entries;
        $rows = $table->fetchAll(
            $table->select()->from($table, array(‘title’,‘date’,‘id’))
        );
        $this->view->entries = $rows->toArray();
    }
   
    public function editAction()
    {
        $form = new ZFBlog_Form_EntryEdit;
        if (!$this->getRequest()->isPost()) {
            $table = new Entries;
            $rowset = $table->find( $this->_getParam(‘id’) );
            if (count($rowset) == 0) {
                $this->view->failedFind = true;
                return;
            }
            $form->setElementFilters(array()); // disable all element filters
            $this->_repopulateForm($form, $rowset->current());
            $this->view->entryForm = $form;
            return;
        } elseif (!$form->isValid($_POST)) {
            $this->view->failedValidation = true;
            $this->view->entryForm = $form;
            return;
        }
       
        $values = $form->getValues();
        $table = new Entries;
        $data = array(
            ‘title’ => $values[‘title’],
            ‘date’ => $values[‘date’],
            ‘body’ => $values[‘entrybody’],
            ‘extended_body’ => $values[‘entrybodyextended’]
        );
        $table->update($data,
            $table->getAdapter()->quoteInto(‘id = ?’, $values[‘id’])
        );
        $this->view->entrySaved = true;
    }
   
    public function deleteAction()
    {
       
    }
   
    protected function _repopulateForm($form, $entry)
    {
        $values = array(
            ‘title’ => $this->_reverseAutoFormat($entry->title),
            ‘date’ => $entry->date,
            ‘entrybody’ => $this->_reverseAutoFormat($entry->body),
            ‘entrybodyextended’ =>
                $this->_reverseAutoFormat($entry->extended_body),
            ‘id’ => $this->_getParam(‘id’)
        );
        $form->populate($values);
    }
   
    protected function _reverseAutoFormat($string)
    {
        $string = preg_replace(“/\<p\>/”, , $string);
        $string = preg_replace(“/\<\/p\>/”, \n\n, $string);
        $string = preg_replace(“/\<a[^>]*\>/”, , $string);
        $string = preg_replace(“/\<\/a\>/”, , $string);
        $string = html_entity_decode($string, ENT_QUOTES, ‘UTF-8′);
        return $string;
    }

}

We’re knee deep in complexity now… To keep the editAction() method cleaner, I’ve refactored a few steps into protected helper methods.

The workflow is quite straightforward. If no form is submitted, we populate a new form with reverse-filtered data so it’s back in the same form we expect the author had originally inputted it. If we are dealing with a form submission, we simply check validity, and update the older entry on the database.

The main tricky bit is that _reverseAutoFormat() method. I’m certainly not comfortable with it since it makes a lot of assumptions – the worst one being to entity decode the data. It’s a reasonable default, but chances are there will be some input the user expressly encoded for a reason. The content between tags springs immediately to mind. In the future I would seriously consider a class expressly for reverse filtering that uses something more flexible like the DOM.

Here's the respective view template for our editAction():

<?php if($this->failedValidation): ?>
    <p class="error">Some problems were detected with the submitted form.</p>
<?php endif; ?>

<?php if($this->entrySaved): ?>
    <p class="success">Entry has been saved.</p>
<?php elseif($this->failedFind): ?>
    <p class="error">The entry could not be found in the database.</p>
<?php else: ?>
    <?php echo $this->entryForm ?>
<?php endif; ?>

Conclusion
Blog entries. You can't live without them, and despite their apparent simplicity they can be complex to program for. We're only at the start of a chain of work that will see us building a layer of entry data processing possibilities. The main ones covered here is creating, cleaning and editing. At some point I'll want to parse out coloured PHP syntax, perhaps insert some Microformatting. Maybe throw in more autoformatting. The strategies governing these and converting between presentation forms and user editable forms are well worth a careful examination.

In the next Part to this series we'll select the simplest strategy of them all. We'll retain original input on the database, and simply post-process the entry for any such filtering. This will add heavily to page view performance but we do have options to limit that! Part 9 will also sort out a point a few comments have noted - our Views which work well, and appear healthy, are actually littered with some poor practices. If you want some homework, examine the Bootstrap class carefully and consider how it would impact the application attempting to output XML (e.g. Atom). That's just one example. One of the benefits of these problems, is that they make for a good introduction into understanding how Zend_View can make life a lot easier.

All Contents Copyrighted to Pádraic Brady.

About the author: insic

Subscribe in my RSS Feed for more updates on Web Design and Development related articles. Follow me on twitter or drop a message to my inbox.

  • Michel

    Again a great tutorial! Looking forward to see the next part!

    Michel

  • Oliver Dueck

    Am I missing something, or was the Entries class never built in this series of tutorials? The end of step 5 in this article seems to imply that it was but I can’t find any other references to it.

  • http://blog.insicdesigns.com insic2.0

    @Oliver, Actually you are commenting in the part where the class Admin_EntryController are created. Maybe you don’t read this part yet.

  • SaM

    m8, great tuts, really enjoying them.

    Question tho: how to implement the new HTMLPurifier (3.2)?

  • http://none Thijs

    This tutorial doesn’t show that you have to add an include path to the index.php file.

    add the following to the includes: $root . ‘/application/models’ . PATH_SEPARATOR

    there and is should include the entry model (and with that the Zend_DB) and it should work fine.

  • Gilles

    Thanks for this excellent tutorial !

    The UTF8 exemple is clear and we never find it in US blogs / tutorials.

    Just a question about it :

    - when I send a comment from a view :
    if ($this->failedValidation)…
    …class=”error”… La saisie n’a pas été validée ….

    The screen output is : La saisie n’a pas ?t? valid?e

    Where does this problem comes from ?
    Eclipse text file encoding is Cp1252. Must I convert it in UTF8 text encoding ?

    Thanks in advance,
    Gilles

  • http://blog.insicdesigns.com insic2.0

    @Gilles, It must be an encoding problem. Regards

  • Gilles

    Encoding Error :

    I’ve changed my Eclipse’s project encoding charset.

    Project->Properties->Text File Encoding (from Cp1252 to UTF-8)

    … it works !

  • Jesús Flores

    Ohh I love you. ^^

    Great tutorial !!! I’ve discovered another great blog and Whattt can we see !!! it’s driven by a beautiful geek girl ;)

    kisses !!

  • bluesea

    Thank you for your  excellent tutorial !
    But I have problem with HTMLPurifier . I don’t know how to install and config it .
    I try many times to solve the problem fellow the install instructions in HTMLPurifier web page ,but I failed.
    Can you tell me how to solve the problem?
    Thanks! 

  • bluesea

    I’v solved the problem.