So I’ve started studying MVC (real MVC, not framework MVC) a bit more in-depth, and I’m attempting to develop a small framework. I’m working by reading other frameworks such as Symphony and Zend, seeing how they do their job, and attempt to implement it myself.
The place where I got stuck was the URL routing system:
<?php
namespace Application\Common;
class RouteBuilder {
public function create($name, $parameters) {
$route = new Route($name);
$route->resource = array_keys($parameters)[0];
$route->defaults = $parameters["defaults"];
$notation = $parameters["notation"];
$notation = preg_replace("/\[(.*)\]/", "(:?$1)?", $notation);
foreach ($parameters["conditions"] as $param => $condition) {
$notation = \str_replace($param, $condition, $notation);
}
$notation = preg_replace("/:([a-z]+)/i", "(?P<$1>[^/.,;?\n]+)", $notation);
//@TODO: Continue pattern replacement!!
}
}
/* How a single entry looks like
* "main": {
"notation": "/:action",
"defaults": {
"resource" : "Authentication",
},
"conditions": {
":action" : "(login)|(register)"
}
},
*/
I just can’t get my head wrapped around it properly. What is the application workflow from here?
The pattern is generated, probably a Route object to be kept under the Request object or something, then what? How does it work?
P.S. Looking for a real, well explained answer here. I really want to understand the subject. I would appreciate if someone took the time to write a real elaborate answer.
An MVC
Routerclass (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
Routerattempts to match the first one, or two, parts of the path component to a corresponding route combination (Controller/ Action [method], or just aControllerthat executes a default action (method).An action, or command, is simply a method off of a specific
Controller.There is usually an
abstract Controllerand many children ofController, one for each webpage (generally speaking).Some might say that the
Routeralso passes arguments to the desiredController‘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
Controllerclass are two separate responsibilities. In that case, aDispatcherclass would actually instantiate theControllerand pass one of its methods any arguments derived from the client HTTP request.Example 1:
Controller, but no action or arguments.http://localhost/contactOn a GET request, this might display a form.Controller = Contract
Action = Default (commonly an
index()method)======================
Example 2:
Controllerand action, but no arguments.http://localhost/contact/sendOn 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/syncOn 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 theContactControllerto use aViewthat supports screen redraw and responds with an HTTP header ofContent-Type: text/html, instead ofContent-Type: application/json.syncwould be passed as an argument toContactConroller::send(). Note, mysyncexample 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
Routerclass instantiates the requested, concrete childController, calls the requested method from the controller instance, and passes the controller method its arguments (if any).1) Your
Routerclass should first check to see if there is a concreteControllerthat 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
Routercannot find and load the necessary PHP at runtime (using an autoloader is advised) to instantiate a concreteControllerchild, it should then check an array (typically found in another class nameRoute) to see if the requested URL matches, using regular expressions, any of the elements contained within. A basic skeleton of aRouteclass follows.Note:
.*?= Zero, or more, of any character, non-capturing.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
Routerclass will iterate over theRoute::routesarray to see if there is a regular expression match between the target URL and thestringvalue associated with a 2nd levelurlindex. If a match is found, theRouterthen knows which concreteControllerto 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.