Pragmatism in the real world

Zend Framework Front Controller

Zend have finally released a preview of their new framework at http://framework.zend.com/.

I’ve started having a play and their implementation of a Front Controller is quite similar to what I’ve been playing with. This isn’t a total suprise as there aren’t that many ways to do a Front Controller :)

The documentation on the Zend/Controller directory is completely non existant, so I’ve worked out the basics by reading the source. Fortunately, the source isn’t too hard to read!

To get something going, I set up my directories like this:

htdocs/
/lib/
/lib/Zend.php
/lib/Zend/

/wwwroot
/wwwroot/.htaccess
/wwwroot/index.php
/wwwroot/controllers
/wwwroot/controllers/IndexController.php
/wwwroot/controllers/TestController.php

.htaccess turns on mod_rewrite to redirect every url to index.php (if the url doesn’t point to an existing file or directory):

RewriteEngine on

RewriteOptions Inherit
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule  (.*) index.php?apache_path=$1 [L,QSA]

wwwroot/index.php sets up the Front Controller…

< ?php
$path = '../lib/';
set_include_path(get_include_path() . PATH_SEPARATOR . $path);

include "../lib/Zend.php";

Zend::loadClass('Zend_Controller_Dispatcher');
Zend::loadClass('Zend_Controller_Router');
Zend::loadClass('Zend_Controller_Front');

$router = new Zend_Controller_Router();

$dispatcher = new Zend_Controller_Dispatcher();
$dispatcher->setControllerDirectory('./controllers');

$frontContoller = Zend_Controller_Front::getInstance();
$frontContoller->setControllerDirectory('./controllers');
$frontContoller->setRouter($router);
$frontContoller->setDispatcher($dispatcher);

$frontContoller->dispatch();
?>

Note that Zend::loadClass() and Zend::loadInterface() assume that the Zend/ directory is on the path…

You also need a controller, so I created wwwroot/controllers/IndexController.php:

< ?php
class IndexController extends Zend_Controller_Action
{
function index()
{
echo 'in index()';
}
function test()
{
echo 'in test()';
}
function noRoute()
{
echo 'in noRoute()';
}
}

Not exactly complicated, but it tells me what's going on.

Very quickly, I discovered that the Zend_Controller_Router assumes that my index.php lives in my webserver's document root. That isn't the case on my test machine, so I decided to write my own router. I chose to put this in lib/rka/Router.php and called the class RKA_Router so that I could take advantage of one of the Zend::loadClass();

Being lazy, I just cloned Zend_Controller_Router and altered it a little. This is my change:
lib/rka/Router.php:

< ?php

/** Zend_Controller_Router_Interface */
require_once 'Zend/Controller/Router/Interface.php';

/** Zend_Controller_Dispatcher_Interface */
require_once 'Zend/Controller/Dispatcher/Interface.php';

/** Zend_Controller_Router_Exception */
require_once 'Zend/Controller/Router/Exception.php';

/** Zend_Controller_Dispatcher_Action */
require_once 'Zend/Controller/Dispatcher/Action.php';

class RKA_Router implements Zend_Controller_Router_Interface
{

public function route(Zend_Controller_Dispatcher_Interface $dispatcher)
{
// RKA: what's the path to where we are?
$path_to_index = dirname($_SERVER['SCRIPT_NAME']);

// RKA: remove $path_to_index from $_SERVER['REQUEST_URI']
$path = str_replace($path_to_index, '', $_SERVER['REQUEST_URI']);
if (strstr($path, '?')) {
$path = substr($path, 0, strpos($path, '?'));
}
$path = explode('/', trim($path, '/'));

/**
* The controller is always the first piece of the URI, and
* the action is always the second:
*
* http://zend.com/controller-name/action-name/
*/
$controller = $path[0];
$action     = isset($path[1]) ? $path[1] : null;

/**
* If no controller has been set, IndexController::index()
* will be used.
*/
if (!strlen($controller)) {
$controller = 'index';
$action = 'index';
}

/**
* Any optional parameters after the action are stored in
* an array of key/value pairs:
*
* http://www.zend.com/controller-name/action-name/param-1/3/param-2/7
*
* $params = array(2) {
*              ["param-1"]=> string(1) "3"
*              ["param-2"]=> string(1) "7"
* }
*/
$params = array();
for ($i=2; $iisDispatchable($actionObj)) {
/**
* @todo error handling for 404's
*/
throw new Zend_Controller_Router_Exception('Request could not be mapped to a route.');
} else {
return $actionObj;
}
}
}
?>

The only bit I changed are commented with “//RKA”. Essentially, I work out the path to the index.php and then remove it from $_SERVER[‘REQUEST_URI’] so that I can have urls of the form http://localhost/test/wwwroot/{controller}/{action} rather than http://localhost/{controller}/{action}

I had to modify index.php by changing:

Zend::loadClass('Zend_Controller_Router');

to

Zend::loadClass('RKA_Router');

and obviously:

$router = new Zend_Controller_Router();

to

$router = new RKA_Router();

And that’s about it. The Zend Framework’s front controller works fine. Any public non-static function within a controller class is considered an action. So http://localhost/index/test will call the IndexController::test();

Next up is to work out how to tie this in with the Zend_View class!

(Update: WordPress is doing something funky with the double quotation marks in the PHP code, they aren’t really escaped!)

16 thoughts on “Zend Framework Front Controller

  1. I guess Zend::load_class() is meant to be used in an __autoload function. This would it also make easier to change the controlling classes like your router.

  2. @Gom:
    Ooh! I'll have a look. Thanks!

    @Nico:
    __autoload()… that's a good idea. I'll try that as it'll save having to keep track of stuff.

    Another thing I've noticed is that that Zend::loadFile() calls Zend::isReadable() which does an fopen() followed by an fclose(). I wonder if there's any performance issues with doing that for every file you want to include() ?

  3. Nice notes, thx. The controller looks very interresting.

    Btw: have a look at css overflow directive :)

  4. Thanks soenke! Can you tell that I'm a programmer and not a designer! It does show up that this template doesn't have enough "width" to it though…

  5. Autoload doesn't work with Zend::loadClass() because it calls class_exists() to check that the class isn't already loaded:

    if (class_exists($class)) {
    return;
    }

    This results in the __autoload() being called which calls Zend::loadFile() which then gets confused :)

    Workaround is to modify to:
    if(class_exists($class, false)) {
    return;
    }

  6. Am i looking at this correctly? Do i need a contoller function for every static link i place in a page. For example: several non-dynamic informational pages would each need it's own controller entry for a link to work. Seems like a lot of code to replace a href but i don't see a way around it with the mod_rewrite.

  7. You are looking at it wrong. The rewrite rule on this page will serve a file if it exists, but if it doesn't, then it will serve index.php.

    Incidentally, the rule in the ZF manual is different…

  8. Ok, i added an RewriteCond to exclude a directory and added all static pages in that directory which worked fine. Thanks.

Comments are closed.