How does MVC routing work?
An MVC Router
class (which is part of a broader Front Controller) breaks down an HTTP request's URL--specifically, the path component (and potentially the query string).
The Router
attempts to match the first one, or two, parts of the path component to a corresponding route combination (Controller
/ Action [method], or just a Controller
that executes a default action (method).
An action, or command, is simply a method off of a specific Controller
.
There is usually an abstract Controller
and many children of Controller
, one for each webpage (generally speaking).
Some might say that the Router
also passes arguments to the desired Controller
's method, if any are present in the URL.
Note: Object-oriented programming purists, following the Single Responsibility Principle, might argue that routing components of a URL and dispatching a Controller
class are two separate responsibilities. In that case, a Dispatcher
class would actually instantiate the Controller
and pass one of its methods any arguments derived from the client HTTP request.
Example 1: Controller
, but no action or arguments.
http://localhost/contact
On a GET request, this might display a form.
Controller = Contract
Action = Default (commonly an index()
method)
======================
Example 2: Controller
and action, but no arguments.
http://localhost/contact/send
On a POST request, this might kick of server-side validation and attempt to send a message.
Controller = Contract
Action = send
======================
Example 3: Controller
, action, and arguments.
http://localhost/contact/send/sync
On a POST request, this might kick of server-side validation and attempt to send a message. However, in this case, maybe JavaScript is not active. Thus, to support graceful degradation, you can tell the ContactController
to use a View
that supports screen redraw and responds with an HTTP header of Content-Type: text/html
, instead of Content-Type: application/json
. sync
would be passed as an argument to ContactConroller::send()
. Note, my sync
example was totally arbitrary and made up, but I thought it fit the bill!
Controller = Contract
Action = send
Arguments = [sync]
// Yes, pass arguments in an array!
A Router
class instantiates the requested, concrete child Controller
, calls the requested method from the controller instance, and passes the controller method its arguments (if any).
1) Your Router
class should first check to see if there is a concrete Controller
that it can instantiate (using the name as found in the URL, plus the word "Controller"). If the controller is found, test for the presence of the requested method (action).
2) If the Router
cannot find and load the necessary PHP at runtime (using an autoloader is advised) to instantiate a concrete Controller
child, it should then check an array (typically found in another class name Route
) to see if the requested URL matches, using regular expressions, any of the elements contained within. A basic skeleton of a Route
class follows.
Note: .*?
= Zero, or more, of any character, non-capturing.
class Route
{
private $routes = [
['url' => 'nieuws/economie/.*?', // regular expression.
'controller' => 'news',
'action' => 'economie'],
['url' => 'weerbericht/locatie/.*?', // regular expression.
'controller' => 'weather',
'action' => 'location']
];
public function __contstruct()
{
}
public function getRoutes()
{
return $this->routes;
}
}
Why use a regular expression? One is not likely to get reliable matching accomplished for data after the second forward slash in the URL.
/controller/method/param1/param2/...
, where param[x] could be anything!
Warning: It is good practice change the default regular expression pattern delimiter ('/') when targeting data contains the pattern delimiter (in this case, forward slashes '/'. Almost any non-valid URL character would be a great choice.
A method of the Router
class will iterate over the Route::routes
array to see if there is a regular expression match between the target URL and the string
value associated with a 2nd level url
index. If a match is found, the Router
then knows which concrete Controller
to instantiate and the subsequent method to call. Arguments will be passed to the method as necessary.
Always be wary of edge cases, such as URLs representing the following.
`/` // Should take you to the home page / HomeController by default
`''` // Should take you to the home page / HomeController by default
`/gibberish&^&*^&*%#&(*$%&*#` // Reject
The router class, from my framework. The code tells the story:
class Router
{
const default_action = 'index';
const default_controller = 'index';
protected $request = array();
public function __construct( $url )
{
$this->SetRoute( $url ? $url : self::default_controller );
}
/*
* The magic gets transforms $router->action into $router->GetAction();
*/
public function __get( $name )
{
if( method_exists( $this, 'Get' . $name ))
return $this->{'Get' . $name}();
else
return null;
}
public function SetRoute( $route )
{
$route = rtrim( $route, '/' );
$this->request = explode( '/', $route );
}
private function GetAction()
{
if( isset( $this->request[1] ))
return $this->request[1];
else
return self::default_action;
}
private function GetParams()
{
if( count( $this->request ) > 2 )
return array_slice ( $this->request, 2 );
else
return array();
}
private function GetPost()
{
return $_SERVER['REQUEST_METHOD'] == 'POST';
}
private function GetController()
{
if( isset( $this->request[0] ))
return $this->request[0];
else
return self::default_controller;
}
private function GetRequest()
{
return $this->request;
}