How does MVC routing work?

2019-01-16 11:51发布

问题:

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.

回答1:

A Router class (or Dispatcher as some would call it) examines the URL of an HTTP request and attempts to match its constituent components to a concrete Controller and a method (a.k.a action or command) defined in that controller. It also passes arguments to the desired Controller's method, if any are present in the URL.

Standard URL: Query String Format

In the Apache HTTP Server, without using mod_rewrite, the URL in an HTTP request would probably be in query string format:

http://localhost/index.php?route=news/economics/param1/param2

Rewritten URL: Desired Format

A URL, after rewriting by a web server, tends to look like this:

http://localhost/news/economics/param1/param2

Without URL rewriting, you will need a class that resolves the route param in the query sting from the standard URL. That is the first thing a Routing class might do. In this case, it resolves the param to:

  • controller = NewsController
  • method = economics()
  • parameters: [param1, param2]

If all goes well, something similar to this will occur:

$controller = new NewsController();
$controller->economics([param1, param2])

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).

Now, the class that you are showing resolves a requested 'route' to the right controller/action. So, for example the URL below:

http://localhost/index.php?route=news/economics/param1/param2

... is an English URL. Hence the word news in the query string. Suppose you wanted the URL to work in Dutch.

http://localhost/index.php?route=nieuws/economie/param1/param2

That would mean the concrete Controller would be called nieuwsController.php, but it does not exist. That is where your example comes in to play: the RouteBuilder class.

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 = array(
        array(
            'url' => 'nieuws/economie/.*?', // regular expression.
            'controller' => 'news',
            'action' => 'economie'
        ),
        array(
            '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


回答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;
  }