I am new to MVC so this is my first attempt and I am sure you guys can give me improvement on this, thanks for any tips or help!
Below is what I have come up with for a router/dispatcher system for my personal framework I am working on, it is my first attempt at using the MVC pattern.
The first block of code is just my .htaccess file which routes all request through my index.php file.
The second block of code is my array of "Routes" which will tell the Router object, which class and method to call as well as any ID or paging numbers if they exists.
Third block of code is the router class.
Fourth block is just running the class
So the router class has to use regex to match the URI with a route in the route map, in theory, this just sounds like bad performance when there is a list of 50+ routes that the regex has to run on, should I be doing this differently? The main reason I use the regex is to match page numbers and ID numbers when they exists in the route.
Also please do not just tell me to use a framework, I am doing this to learn it better, I learn better this way and just prefer to not use an existing framework at this time, I have studies all the main ones and some less common ones for ideas already.
1) So the main question, does anything just not look right?
2) Is there a better way to detect what is in the URI than using the regex on an array like I am doing, consider it on a high traffic site?
3) Since everything is routed through the index.php file with this, how would I go about handling AJAX requests?
Sorry if this is confusing, I am a little confused mtyself!
.htaccess file
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?uri=$1 [NC,L,QSA]
Map array()
/**
* Map URI to class/method and ID and Page numbers
* Must be an array
*/
$uri_route_map = array(
//forums
'forums/' => array(
'controller' => 'forums',
'method' => 'index',
'id_number' => '',
'page_number' => ''),
'forums/viewforum/(?<id_number>\d+)' => array(
'controller' => 'forums',
'method' => 'viewforum',
'id_number' => isset($id_number),
'page_number' => ''),
'forums/viewthread/(?<id_number>\d+)' => array(
'controller' => 'forums',
'method' => 'viewthread',
'id_number' => isset($id_number),
'page_number' => ''),
'forums/viewthread/(?<id_number>\d+)/page-(?<page_number>\d+)' => array(
'controller' => 'forums',
'method' => 'viewthread',
'id_number' => isset($id_number),
'page_number' => isset($page_number)),
// user routes
// account routes
// blog routes
// mail routes
// various other routes
);
Router class that reads and matches the Map array above
/**
* Run URI against our Map array to get class/method/id-page numbers
*/
class Router
{
private $_controller = '';
private $_method = '';
public $page_number = '';
public $id_number = '';
public function __construct($uri, array $uri_route_map)
{
foreach ($uri_route_map as $rUri => $rRoute)
{
if (preg_match("#^{$rUri}$#Ui", $uri, $uri_digits))
{
//if page number and ID number in uri then set it locally
$this->page_number = (isset($uri_digits['page_number']) ? $uri_digits['page_number'] : null);
$this->id_number = (isset($uri_digits['id_number']) ? $uri_digits['id_number'] : null);
$this->_controller = $rRoute['controller'];
$this->_method = $rRoute['method'];
// just for debug and testing while working on it / will be removed from final code
echo '<hr> $page_number = ' . $this->page_number . '<br><br>';
echo '<hr> $id_number = ' . $this->id_number . '<br><br>';
echo '<hr> $controller = ' . $this->_controller . '<br><br>';
echo '<hr> $method = ' . $this->_method . '<br><br>';
break;
}else{
$this->page_number = '';
$this->id_number = '';
$this->_controller = '404';
$this->_method = '404';
}
}
}
public function getController()
{
return $this->_controller;
}
public function getMethod()
{
return $this->_method;
}
public function getPageNumber()
{
return $this->page_number;
}
public function getIDNumber()
{
return $this->id_number;
}
/**
* Call our class and method from values in the URI
*/
public function dispatch()
{
if (file_exists('controller' . $this->_controller . '.php'))
{
include ('controller' . $this->_controller . '.php');
$controllerName = 'Controller' . $this->_controller;
$controller = new $controllerName($this->getIDNumber(),$this->getPageNumber());
$method = $this->_method;
if (method_exists($this->_controller, $this->_method))
{
return $controller->$method();
} else {
// method does not exist
}
} else {
// Controller does not exist
}
}
}
Run it
/**
* Testing the class
*/
$uri = isset($_GET['uri']) ? $_GET['uri'] : null;
$router = new Router($uri, $uri_route_map);
$router->dispatch();
?>
1) & 2) I will not say ,it's not right but why not using default routes ? Most of the time a route like
is good enought for a majority of your page.
You probably could do something like that to define default routes :
And then if you need more specific routes you can define them in an array or whatever you want. In your routeryou just have to check if the current URI match a specific routes or the default routes. By doing that you will decrease the number of routes to match and increase the speed of your router.
3) Your router is probably instancied by your index, without index no root, so unfortunately you probably can't avoid using it. That why it's very important to avoid expensive action in your index. Typically don't init your database connection in index if all your pages don't need it.
Don't forget to download some famous framework and look their code. It's the better way to learn. By doing that you will probably find a lot of good practices and answers.
1) Is it works? If yes, then yes. Since above code only contain array, regex and validation for that, i dont think theres a problem(s) with your code. As long as it works. But if you ask, 'is that code is scalable?' then the answer would be various, and its all depend at your MVC Framework purposes (for example, is that framework for general uses, eg : blog, or its particullary for REST API provider. And so on...)
2) Yes. Kohana, Zend, CI and other popular (and highly optimized) PHP framework use that(array+regex on router).
3) I think you could just give it a flag in route block/section, and make that flag available as a global variable. So that, in your controller, you can decide which response to send for different request type (ajax/non-ajax) by checking that flag (for example you can provide
$this->is_ajax
as global method which available in Controller scope).1) Look alright to me. The code looks a bit messy though.
2) Yes there is a better way. You're doing the regex because you want to match parts of the URL that you don't know. Why not do
$parts = explode("/", $uri)
then see if you can find the page you're looking for? You will need to define how many parameters you're expecting for each page or you wont know whether to pickforums
with parametersarray("viewform", 123)
orforums/viewforum
with parametersarray(123)
.explode
feels loads better than a regex. It also adds the benefit of improved error handling. What if the argument passed toviewforum
is not a number? Surely you can do better than"404"
;)3) Make a seperate ajax handler. Ajax is hidden from view anyway so you don't need to bother with providing semantic URLs.
Example:
(untested)
Plugins
Because of $uri_route_map you can't 'dynamically' register more plugins or pages or 'routes'. I'd add a function to add more routes dynamically to the
Router
.Additionally you could consider some auto-discovery scheme that, for instance, will check the folder
plugins/
for folders with a file called "manifest.php" that, when called, will optionally add more routes to Router.1),2) I dont think that is good idea to put id_number and page_number in Router, because in future you can encounter many other parameters for url. Better just use controller and method and define in controller what to do with other parameters or create other class Request which deals with request info.
3) For ajax use url like ajax/module/action. And create ajax controller which do basic ajax security stuff, like checking against XSRF and then decides which controllers to run and action to call.
If I may add couple of points:
Remove
id_number
andpage_number
from the router - just pass everything that was matched to a controller, after all, it's a controller job to process that data, not router'sDon't pass
$uri
to a constructor, pass it to adispatch()
instead.Why those
isset()
-s in$uri_route_map
? Obviously they would befalse
, since$uri_route_map
is defined beforeRouter()
object is instantiated.Would recommend adding more logic to matching routine - in your current case
sitename/forums
will not match anything resulting in 404 (no trailing slash)You can also define default parameters in your
$uri_route_map
, and thenarray_merge
them with parameters matched. So, for example, when no page number is specifiedpage_number
will be equal to 1If you are worried about perfomance on high traffic website, you can cache routes. After all,
forums/viewforum/100
will always point to the same controller/method.And why are you worried about sending AJAX requests to your index.php file? What's the problem with that?
1) So the main question, does anything just not look right?
Personally, I see this becoming more complicated as your site grows. An MVC framework, as I was taught, should pretty much be "Set it and forget it" – you're separating the request handler (controller) from the database querying and business end (model) and from the display elements (view).
[NB: You may need other core aspects. My standard framework includes some core elements that carry the session through the various parts, as well as handle fundamental aspects of the site work – For instance, while the models are responsible for making the right database calls as directed by the controller, there are core functions in a
sql.class.php
file that give me a standardized set of methods for making those calls and delivering or caching the results as needed.]Your dispatch method is on the right track with this – you're extracting from the URI the name of the controller (Forums, Profiles, etc.). Do you need a uri map? I feel you're creating an unnecessary situation in which you have to update this map each time, rather than simply creating a new controller when you need new functionality, and registering it with the database. I'm not saying you're wrong per se, I just don't feel I'd have done it that way.
2) Is there a better way to detect what is in the URI than using the regex on an array like I am doing, consider it on a high traffic site?
Control the outcome (no pun intended, since it's the controller that does the work here). Consider this approach, and see how it works for you:
Your index.php file (aka "Main Controller") grabs the URI and explodes the values along "/" into bits. bit[0] is the controller ID – this says "I want to use the controller named bit[0]=>value". This is done as:
Personally, being a bit of a neat freak when it comes to directory structures, I use bit[0] to identify the directory in which controller.php is located, as I might have sub controllers.
It's this controller file that I use to parse other bits. For this, I'll use an example:
Assume that bit[0] carried the value "forums". I might pass, if it's set, bit[1] to a switch statement. By default, I always want to list, but I might specifically direct it to "list", "view", or "post" in bit[1]. This will tell me in the controller class which method to call. The method will then tell me to call the associated "forums" model if I need to perform queries and cache the forum listing, for instance.
The extraneous "bits" may do one of two things: they may be passed as simple arguments to the method as to what data to request from the model, or bit[1] may be complex enough to warrant a sub controller, and the subsequent bits will be passed to that controller to determine the appropriate action, as was done with the forums controller.
Regex, being slow, should be avoided when possible. Since we may have a URI of
/forums/view/102305
we can assume that theforums
controller will be passing102305
to the method associated with theview
argument (the method being something likeprivate function displayPost( $id )
where$id
is102305
). No regex is needed since we can simply explode the values along a common anticipated delimiter.3) Since everything is routed through the index.php file with this, how would I go about handling AJAX requests?
Not terribly difficult. If the controller is set to, say, AJAX, you could rebuild the URL and direct access it. You could write exclusions in the .htaccess file (
RewriteRule ^(AJAX)($|/) - [L]
). Or (not ideal, but a sneaky workaround) is to append../
to your AJAX URI to push the URI back to root – it's no longer attempting to accessindex.php
so the rewrite rule doesn't apply.Edit
Let's assume that we're using a URI of
/forums/id-1234/page-4
per your example. Again, let's assume as I mentioned above thatforums
refers to the controller to be used, and every other/
delimits arguments (what I like to call "drill downs"). So, in our forum controller file (let's call itforumcontroller.php
, we might have something like this (extremely simplified) constructor:The point is that you're in control of what is being passed and how it gets handled. Control the URIs and you can simplify their processing.
Edit 2 Reading through again, there's a few concepts that I think will help you and that you should familiarize yourself with:
View the "Factory" pattern here (My $registry is, at it's heart, a set of factories): http://php.net/manual/en/language.oop5.patterns.php
A good breakdown of MVC graphically: http://best-practice-software-engineering.ifs.tuwien.ac.at/patterns/images/mvc3.jpg
More on Factory methods: http://www.devshed.com/c/a/PHP/Design-Patterns-in-PHP-Factory-Method-and-Abstract-Factory/
One other note, and this is a personal observation after having worked with Joomla, Drupal, Wordpress, and various different enterprise CMS and BBS solutions – Design solely with you in mind. When you start trying to become "something for everybody", you end up with a lot of unnecessary bloat that's getting loaded with each page and used 1 time out of 100. MVC is a design pattern, and using it as a pattern will tell you to get rid of excess in every aspect, including URIs. Processing
/controller/arg1-Identifier/arg2-offset
is unnecessary, and you can easily get away with/controller/id/offset
(e.g./forums/1234/4
). If you want to make it SEO friendly, add the thread title, not a tag identifying the ID (e.g./forums/1234-This-Is-A-Topic/4
).Now, let's also point out the obvious about my edit above – This is a controller intended solely for the forum element. Each element of your site (i.e. forums, galleries, profiles, etc.) should have it's own controller. Why? Because each is doing completely different things on its pages. So make use of that – you don't need to use a URI map so long as you understand that you're directing to the controller, and the controller is delegating responsibilities to the model and any sub controllers that might be needed.
I really hope that helps.