Sat 4 Oct 2008
Here's another issue I couldn't find a simple answer to. I'm building a website in PHP, and I want a simple front controller. In other words, I want every bit of traffic to run through a central script that farms out the traffic by parsing the URL. And I want it to be silent – I don't want the user to know the redirects are happening. Now, I could do this with query strings – that's like http://mysite.com/index.php?page=foo&action=bar But that's ugly, and it gets complicated fast.
Instead, I want to do it in the user-friendly, semantically meaningful, SEO-ok way, like: http://mysite.com/foo/bar I want to use the path structure instead of the query string. This is a common pattern for Ruby and Python, but maybe less so in PHP. So, here's how I did it.
(Note: This isn't rocket science. Yet it was still hard to find. And I'm sure there are 1000 ways this could be done better, cleaner, faster, more. But I'm a functional coder. I don't care if it's the most elegant, I care if it works. And this works.)
There are two parts to the controller. The first is the .htaccess file. I'm using XAMPP on Windows Vista as my development environment. If you need help getting .htaccess to work there, check out my earlier post.
RewriteEngine on
RewriteCond $1 !^(index\.php|lib|parts|pages|images|robots\.txt)
RewriteRule ^(.*)$ /your/webroot/index.php/$1 [L]
Here's what this does. (Full disclosure: I adapted this from CodeIgniter's model…) The first line enables mod_rewrite. It's a must. The second line sets the conditions for the rewrite. It says, rewrite everything except what's listed in the parenthesis. If you have additional directories that you keep images, css files, or other things in, you just add them to the list in parens with a '|' between. Finally, the last line sends everything through the main controller, which is index.php, but without actually changing the URL in the address bar. This part is what makes the whole thing transparent to the user. Good stuff.
Ok, so now, we can type something like 'http://mysite.com/foo/bar' and it will silently redirect to 'http://mysite/com/index.php/foo/bar'. So now all we have to do is set up index.php to handle the incoming request. Now, there are lots of more object oriented ways to do this, but for my purposes, the simple procedural way works best: a switch statement.
< ?
//A file with all the common configuration, like web roots, security,
//database, language, etc.
require_once('lib/config.php')
//Your header, the same regardless of the page
require_once('parts/header.php')
//This is the content area that will change based on the URL.
//My div is called 'textarea'. Yours might be called something else.
//echo "";
//The switch statement
$url = substr($_SERVER['REQUEST_URI'], strlen(URLROOT));
switch($url){
case (''):
require_once('pages/root.php');
break;
case ('foo'):
require_once('pages/foo.php');
break;
case ('bar'):
require_once('pages/bar.php');
break;
default:
//the default is an error!
require_once('pages/error.php');
break;
}
//End your content div here
//echo '';
//Your footer, the same regardless of the page
require_once('parts/footer.php')
?>
That's it. No mystery. In that first line of PHP ($url = …), I get the path string that's in the address bar – so this will show what the user typed before we did the redirect. Then, in my config.php I've set a global variable called 'URLROOT' that corresponds to the path in my local environment. Using substr, I snip out only the part of the path that comes after the root, and feed that to my switch statement.
Like I said, I'm sure a more experienced coder has 1000 better ways to do this. But, give it a try. It worked for me!
15 Responses to “Simple PHP Front Controller”
Leave a Reply
Nice! Worked great for me, right out of the box.
I made a minor tweak so you won't have to add to the index.php file, but who knows, I may be introducing security holes.
$parts = array_slice(explode('/',$_SERVER["REQUEST_URI"]), 2);
if(file_exists('pages/'.$parts[0].'.php')) {
require_once 'pages/'.$parts[0].'.php';
} else {
require_once 'pages/default.php';
}
Great tutorial, great for me. Are you writing your own version of an MVC?
I've never had to build anything sophisticated enough to need a Model or View separation. I know there are all sorts of good reasons to use that architecture, but for my purposes just the 'C' in MVC is good enough.
I've recently coded up something similar, but it's object oriented. Instead of requiring files, it call method names.
Would love to get some thoughts on it.
http://www.davecomeau.net/blog/17/PHP+Class%3A+ObjectOrientedURLs
This is by far the simplest presentation of the PHP front controller I've found yet. Well done! This helps me out immensely. Thank you.
What you have done here is called Static invocation and only is suggested if you have only several files you want to load because it does not scale well. The better way to implement a Front controller pattern is using a dynamic invocation. All the major php frameworks use dynamic invocation.
Static vs Dynamic Invocation
http://www.phpwact.org/pattern/front_controller
Example of Dynamic Invocation
http://immike.net/blog/2007/05/17/presentation-patterns-the-front-controller/
Logan, I see your point. No one likes a massive switch statement. I imagine that most people reading this will enjoy the simplicity of a switch, but move quickly to the dynamic way once things get complicated.
Logan!!! Nice to see you here!!
Guys, I solved the simplistic front controller issue. You want to use what is called the Command Pattern w/ the Front Controller pattern.
It ends up looking like this:
index.php:
// Set up autoload so you don't have to manually require all your classes:
function __autoload($name)
{
if (strpos($name, 'Controller') !== false)
{
require 'controllers/' . $name . '.inc.php';
}
}
// This gets the template you want; defaults to index.
$view = isset($_GET['view']) ? $_GET['view'] : 'index';
// This gets the action;
$action = filter_input(INPUT_GET, 'action', FILTER_SANITIZE_STRING);
// Pass to controller:
$data = ControllerCommandFactory::execute($action);
// Extract $data to global namespace.
if (!is_null($data)) { extract($data); }
require 'views/header.tpl.php';
require $view_file;
require 'views/footer.tpl.php';
Then you have a file called controllers/ControllerCommand.inc.php:
interface ControllerCommand
{
public function execute($action);
}
class ControllerCommandFactory
{
const ERROR_INVALID_ACTION = 601;
// @codeCoverageIgnoreStart
private function __construct() { }
// @codeCoverageIgnoreStop
public static function execute($action)
{
if ($action == ") { return null; }
$controllers = array('UserController', 'SearchController', 'SecurityController');
foreach ($controllers as $c)
{
$controller = new $c;
if (($output = $controller->execute($action)) !== false)
{
return $output;
}
}
throw new Exception('No implementation found for ' . $action, self::ERROR_INVALID_ACTION);
}
}
As your program scales, you just have to remember to add new controllers to the ControllerFactory array **or** implement readfile() and get them dynamically.
Slightly better, more dynamic version of same thing.
Thanks Judd. I always say that the easiest way to do something is the best way to start.
Now that I get it, I can move easily to something harder.
Thanks!
Awesome post. this is the easiest post on front controller. Thanx Judd
Nice approach. I try to avoid OOP when the task is small and simple.
I make something similar but instead of a Switch I have an array (a name/value* white list) and an in_array() to do all the stuff.
Is it silly?
Thanks for share you opinion.
*name/value, alias/file, action/object, you name it.
This may just be my server settings, but with the rewrite in place, trimming the request uri as illustrated does not happen before the redirect. it's taken off before I get the REQUEST_URI, so the trimming is not needed. I am just adding a check for now to make my code a bit more portable to see if request uri contains the base script or not.
Judd, thanks for this, but it did not work for me – probably because I'm a beginner at PHP.
Not sure what you mean by "Then, in my config.php I've set a global variable called 'URLROOT' that corresponds to the path in my local environment. Using substr, I snip out only the part of the path that comes after the root, and feed that to my switch statement." – can you please demonstrate this?
Also, you have a couple of echo commands that are partially or fully commented out – one just before the switch statement:
//echo "
";
and one right after the end of the switch statement:
//End your content div here
//echo ";
Not sure what you meant to do here unless its:
echo "";
and
echo "";
Can you (or someone) expand on this please?
Thank you.
Got it working. It was my error. Syntax, as usual…
During the process, I did a lot of searching, and your solution was by far the easiest to implement. Thank you.