I think I've come up with a relatively simple (and hopefully elegant) way to combine a FrontController using Intercepting Filters for handling basic pre/post-processing logic common to all pages (output buffering, script timing, page caching, logging, etc.) and URL-based PageControllers (1 URL = 1 Command/Action) for page-specific logic.

Code posted below. Hopefully this will be of use to someone out there.

example_page.php:
PHP Code:
<?php

include_once('frontcontroller.inc.php');

// do page specific stuff (e.g. PageController), for example:
$user =& new User($_GET['id']);

?>
<html>
<head><title>User Info</title></head>
<body>
    <h1><?=$user->getName()?></h1>
    <p><?=$user->getProfile()?></p>
</body>
</html>
(You could also leave out the include_once('frontcontroller.inc.php') part if you are using Apache and instead use the auto_prepend_file directive in a .htaccess file.)

Yes, I am combining the Controller and the View (which is perfectly acceptable). This is on purpose. You could put the $user =& new User($_GET['id']); line (and anything else) in its own UserController class if you'd like to acheive better separation, but simplicity is one of my design goals here.

frontcontroller.inc.php:
PHP Code:
$t microtime();

require_once(
'FilterChain.class.php');
$fc =& new FilterChain();

require_once(
'OutputBufferingFilter.class.php');
$fc->addFilter(new OutputBufferingFilter());

require_once(
'TimingFilter.class.php');
$fc->addFilter(new TimingFilter($t));

require_once(
'PageControllerFilter.class.php');
$fc->addFilter(new PageControllerFilter());

$fc->process();
exit; 
Notice the exit; line? PageControllerFilter (see below) will include() the originally requested page. This facilitates having Filters that have pre-processing AND post-processing.

You can put any other stuff that should be global to your application in frontcontroller.inc.php (like define()'s, ini_set()'s, etc.)

FilterChain.class.php:
PHP Code:
class FilterChain
{

    var 
$filters;

    function 
FilterChain()
    {
        
$this->filters = array();
    }
    
    function 
addFilter(&$filter)
    {
        
$this->filters[] =& $filter;
    }
    
    function 
next()
    {
        
$f =& next($this->filters);
        if (
is_a($f'InterceptingFilter'))
        {
            
$f->run($this);
        }
    }
    
    function 
process()
    {
        
$f =& reset($this->filters);
        
$f->run($this);
    }
    

InterceptingFilter.class.php:
PHP Code:
class InterceptingFilter
{

    function 
InterceptingFilter()
    {
    }
    
    function 
run(&$filterChain)
    {
        
$filterChain->process();
    }
    

OutputBufferingFilter.class.php:
PHP Code:
class OutputBufferingFilter extends InterceptingFilter
{

    function 
OutputBufferingFilter()
    {
        
parent::InterceptingFilter();
    }
    
    function 
run(&$fc)
    {
        
ob_start();
        
$fc->next();
        
ob_end_flush();
        echo 
'<div id="outputbufferingfilter">buffered</div>';
    }
    

TimingFilter.class.php:
PHP Code:
class TimingFilter extends InterceptingFilter
{

    var 
$t;
    
    function 
TimingFilter($t NULL)
    {
        
parent::InterceptingFilter();
        
$this->$t;
    }
    
    function 
run(&$fc)
    {
        
$t0 explode(' ', (is_null($this->t) ? microtime() : $this->t));
        
$fc->next();
        
$t1 explode(' 'microtime());
        
$t  sprintf('%.6f', ($t1[1]-$t0[1])+($t1[0]-$t0[0]));
        echo 
'<div id="timingfilter">'$t'</div>';
    }
    

PageControllerFilter.class.php:
PHP Code:
class PageControllerFilter extends InterceptingFilter
{

    function 
PageControllerFilter()
    {
        
parent::InterceptingFilter();
    }
    
    function 
run(&$fc)
    {
        include(
$_SERVER['PATH_TRANSLATED']);
    }
    

PageController include()'s the originally requested page, then the post-processing for each of the filters occurs (the stuff after $fc->next() in the run() methods).

BTW, the echo lines in the run() methods are just for testing and can be commented out or deleted.

That's it, there's not much to it (less than there appears anyway), any feedback would be greatly appreciated.