21

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.

PeeHaa
  • 71,436
  • 58
  • 190
  • 262
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308

2 Answers2

31

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 Dispatcherclass 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
Anthony Rutledge
  • 6,980
  • 2
  • 39
  • 44
w00
  • 26,172
  • 30
  • 101
  • 147
  • If your version is a simplified example, what advantages does the approach I've seen (i.e. my example) have over yours? Also, what if no match is found? – Madara's Ghost Sep 14 '12 at 21:09
  • @MadaraUchiha The difference between mine and yours is, is that you can specify how the notation should look like. I see you can add certain conditions for the action methods. And some other things. So with other words, your example is more configurable. --- I'm not sure where you got the code from (Symphony?), so i can't really tell what all the options really mean. But the general idea is the very same as my example. Also, if no match is found then a 404 page (404Controller) should be shown. Because the requested page simply doesn't exist. – w00 Sep 15 '12 at 07:46
  • Well, shouldn't it default to "resource/action/params"? – Madara's Ghost Sep 15 '12 at 07:53
  • @MadaraUchiha Normally in other frameworks the `default` option is the controller/action that should be called when the URI contains no data. Otherwise, when the URI contains data, but isn't found in the `RouteBuilder`, then you should really throw a 404 error. I really wouldn't recommend any other way. – w00 Sep 15 '12 at 08:09
  • Either something about this is inaccurate or I'm confused about something: In a test case of a clean apache server without mod rewrite a request like localhost/news does not reroute to localhost/index.php?route=news but results in a 404 error? – Assimilater Sep 23 '13 at 05:41
  • @Assimilater That's why the second line says: "Which is actually **(if you're not using mod rewrite)** something like this:". But if you think this is unclear that the first mentioned URL is a `mod_rewrite` url then you could always propose an edit to clearify things – w00 Sep 25 '13 at 15:09
  • The confusion was more it seemed to imply that apache somehow transformed that request into a GET parameter, which doesn't happen. Made an edit proposition. – Assimilater Sep 26 '13 at 19:47
2

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;
  }
JvdBerg
  • 21,777
  • 8
  • 38
  • 55
  • 2
    I'm not very comfortable using your code. It contains a lot of global space I don't really want in my code. Also, have you considered using comments? That code is not self-explanatory. I'll have a look nonetheless. – Madara's Ghost Sep 14 '12 at 21:07
  • 1
    @JvdBerg http://stackoverflow.com/questions/5166087/php-global-in-functions/5166527#5166527. Also, your Router shouldnt know anything about the SAPI. Pass the URL in from the outside. – Gordon Sep 15 '12 at 08:15
  • Thanks Gordon. I changed the router class, the SAPI and globals are moved to the bootstrapper. Added some comments – JvdBerg Sep 15 '12 at 11:53
  • Not enough error checking. Little, if any security. Makes too many assumptions. – Anthony Rutledge Sep 08 '18 at 15:12
  • 1
    One line of comment is still not enough to explain what is happening to a novice. – Vincent May 02 '19 at 07:05