Blog
Zend_Form's flaw in an MVC environment
Posted by on the 19th April 2008 @ 3:51pm
Zend_Form, the latest and greatest addition to the Zend Framework in version 1.5, is an infusion of the best bits of Zend_Filter_Input, and the Zend_View_Helper system. At first glance it looks like the ideal system for setting up from the simplest to the most complex forms, and this is how most people will see it.
I recently delved into the use of it, and was instantly shocked at a fatal flaw in its design. Where am I supposed to put it?
I try to code in as best practices as I can, and so even when I'm not using the Zend MVC implementation, I'm still trying to keep to MVC style principles. Essentially I always try to separate control logic from view layout.
Zend_Form is about as bad as it gets at separating this. Initialisation involves specifying Zend_Form_Element's which directly map to their associated Zend_View_Helper's.
// controller
$form = new Zend_Form();
$form
->setAction('/submit')
->setMethod('post')
->addElement('text', 'user-name', array(
'required' => true,
'default' => 'Fred',
'validators' => array(
array('stringLength', 1, 20),
)
))
->addElement('textarea', 'user-comment', array(
'required' => true,
'validators' => array(
array('stringLength', 1, 255),
)
))
->addElement('submit', 'action', array(
'label' => 'Submit',
));
if ($_POST['action']) {
if ($form->isValid()) {
// insert comment
}
}
// view
echo $form->render();
This snipett of code will create a simple form with a textbox and textarea in. If submitted, the form will be validated, and the comment will be added if it passes.
The assumption is that you put the form initialisation in the controller, as that is where validation and database insertation should be performed. However, that means the controller has to define the HTML tag type of each element. e.g. in this example, that user-name is an <input type="text" />, and user-comment is a <textarea></textarea>.
This view layout information, not control logic. The only place this should be put is in the view script, however the above is the only way, hence this control is not suited to MVC practices.
Now considering the Zend Framework trys to keep to MVC principles, why is it that Zend_Form completely disregards this? I would assume that this may have been due to lack of communication between teams.
My first thought was, how can this be done better MVC style? The trouble is it may be possible that a display element would be complex, and that it needs control logic associated with it. An element may be a scalar, or contain multiple predefined options.
So, this could be represented by declaring elements as primitive types, such as:
- string
- integer
- enum
- set
There could also be more complex types which define a bit more control behaviour:
- date
An example of this in use would be:
// controller
$form = new My_Form();
$form
->setAction('/submit')
->setMethod('post')
->addElement('string', 'user-name', array(
'required' => true,
'validators' => array(
array('stringLength', 1, 20),
))
->addElement('string', 'user-comment', array(
'required' => true,
'validators' => array(
array('stringLength', 1, 255),
));
if ($form->posted()) {
if ($form->isValid()) {
// insert comment
}
}
// view
$form->setElementHelper('string', 'text'); // already set as default
$form->addElement('submit', 'action', array(
'label' => 'Submit',
));
$form->getElement('user-comment')->setHelper('textarea');
echo $form->render();
This would create exactly the same form, but conform to MVC standards.
Zend_Cache_Frontend_Page - FAIL
Posted by on the 6th April 2008 @ 3:14pm
In my attempts to create the most Digg resilient website, I decided there was only one thing for it, full page-based caching on the server.
My first foray into this was trying to implement Zend's Zend_Cache_Frontend_Page.
However, I found a few show-stopping bugs in this, when involving browser-based cache HTTP status (304 Not Modified), and for http redirects. The problem is, the frontend caches the page whatever the HTTP status is sent. For instance, notifying to the browser:
header('HTTP/1.1 304 Not Modified');
exit;
If this was run while the cache was invalidated, the cache would store a blank page. Any requests after this would serve HTTP/1.1 200 OK with a blank page.
It seems that the cache should ideally only cache the page on HTTP status 200 OK, otherwise the example above, and any other status' (stati?) will be cached.
So I had a look into fixing the bug. It turns out, as far as I know, there is no way to retrieve the sent HTTP status (headers_list doesn't appear to return it). A fix would have to involve an addition to the PHP binary.
Alternatively, there would have to be another way to invoke the HTTP status, whilst storing it in a PHP variable for future lookup.
Having said that, Zend_Cache is a very nice and easy to use system for caching data through abstracted backends. Page caching is only one of their frontends.
Hierarchical data in MySQL using nested sets
Posted by on the 1st April 2008 @ 9:39pm
As you may know, MySQL is a relational database system. It consists of flat tables, which can be joined together in queries. Relations between these tables can only be specified in a way that is one-to-one/one-to-many.
This suits most situations, but when you start getting to hierarchical data, such as multiple level categories (as used on this site), these types of relations start to become non-optimal.
Take for instance the adjacency list method. Each entry in the table is given a column with the id of its parent node:
| id | name | parent_id |
| 1 | parent | 0 |
| 2 | child 1 | 1 |
| 3 | sub-child 1 | 2 |
In order to find all descendants of a node, there would need to be a query once for the parent, then once per node:
function getNodes($db, $parentId) {
$sql = 'SELECT * FROM nodes WHERE parent_id = ?';
$nodes = array()
foreach ($db->fetchAll($sql, array($parentId), Zend_Db::FETCH_OBJ) as $node) {
$node->subnodes = getNodes($db, $node->id);
$nodes[] = $node;
}
return $nodes;
}
An alternative method is to use nested sets. This takes advantage of MySQL's index range selection by using two columns to specify a range in which children exist:
| id | name | left | right |
| 1 | parent | 1 | 6 |
| 2 | child 1 | 2 | 5 |
| 3 | subchild 1 | 3 | 4 |
In this case all descendants of a node can be found between the parent node's left and right columns.
Combining this with the adjacent list method, you can optimise the conversion of a MySQL result set into a multi-level array of objects:
function getNodes($db, $parentId) {
$root = (object) array(
'id' => $parentId,
);
$stack = array($root);
$sql = 'SELECT nodes.*
FROM nodes, nodes AS parent
WHERE nodes.left > parent.left AND nodes.left < parent.right
AND parent.id = ?
ORDER BY nodes.left';
foreach ($db->fetchAll($sql, array($parentId), Zend_Db::FETCH_OBJ) as $node) {
while ($node->parent_id != $stack[count($stack)-1]->id) {
array_pop($stack);
}
$stack[count($stack)-1]->subnodes[] = $node;
array_push($stack, $node);
}
return $root->subnodes;
}
Of course this method is at the expense of insert/modification of nodes in the table, which would need more code to do, but usually these happen much less often than selects in production environments.