flight项目目录 php,php微框架 flight源码阅读——3.路由Router实现及执行过程

吕高昂
2023-12-01

现在来分析路由实现及执行过程,在项目目录下创建index.php,使用文档中的路由例子(含有路由规则匹配),如下:

require 'flight/Flight.php';

Flight::route('/@name/@id:[0-9]{3}', function($name, $id){

echo "hello, $name ($id)!";

});

Flight::start();

首先引入'flight/Flight.php'框架入口文件,在执行Flight::route()时当然在Flight类中找不到该方法,于是就会调用下面的__callStatic()魔术方法,然后去执行\flight\core\Dispatcher::invokeMethod(array($app, $name), $params),其中$app就是之前框架初始化好后的Engine类实例化对象,$params就是定义路由传入的参数:匹配规则或url和一个匿名回调函数。

/**

* Handles calls to static methods.

*

* @param string $name Method name

* @param array $params Method parameters

* @return mixed Callback results

* @throws \Exception

*/

public static function __callStatic($name, $params) {

$app = Flight::app();

return \flight\core\Dispatcher::invokeMethod(array($app, $name), $params);

}

接着会调用Dispatcher类的invokeMethod()方法,$class和$method分别对应刚才的$app对象和$name参数。is_object($class)返回true,很明显count($params)值为2,因此会执行case语句中的$class->$method($params[0], $params[1]),就是去Engine对象中调用route()方法。

/**

* Invokes a method.

*

* @param mixed $func Class method

* @param array $params Class method parameters

* @return mixed Function results

*/

public static function invokeMethod($func, array &$params = array()) {

list($class, $method) = $func;

$instance = is_object($class);

switch (count($params)) {

case 0:

return ($instance) ?

$class->$method() :

$class::$method();

case 1:

return ($instance) ?

$class->$method($params[0]) :

$class::$method($params[0]);

case 2:

return ($instance) ?

$class->$method($params[0], $params[1]) :

$class::$method($params[0], $params[1]);

case 3:

return ($instance) ?

$class->$method($params[0], $params[1], $params[2]) :

$class::$method($params[0], $params[1], $params[2]);

case 4:

return ($instance) ?

$class->$method($params[0], $params[1], $params[2], $params[3]) :

$class::$method($params[0], $params[1], $params[2], $params[3]);

case 5:

return ($instance) ?

$class->$method($params[0], $params[1], $params[2], $params[3], $params[4]) :

$class::$method($params[0], $params[1], $params[2], $params[3], $params[4]);

default:

return call_user_func_array($func, $params);

}

}

当然在Engine对象中也没有route()方法,于是会触发当前对象中的__call()魔术方法。在这个方法中通过$this->dispatcher->get($name)去获取框架初始化时设置的Dispatcher对象的$events属性:$this->dispatcher->set($name, array($this, '_'.$name)),然后$events属性数组中会有一个route键名对应的值为[$this Engine对象, '_route']数组,返回的$callback=[$this Engine对象, '_route']并且is_callable($callback)==true。

/**

* Handles calls to class methods.

*

* @param string $name Method name

* @param array $params Method parameters

* @return mixed Callback results

* @throws \Exception

*/

public function __call($name, $params) {

$callback = $this->dispatcher->get($name);

if (is_callable($callback)) {

return $this->dispatcher->run($name, $params);

}

if (!$this->loader->get($name)) {

throw new \Exception("{$name} must be a mapped method.");

}

$shared = (!empty($params)) ? (bool)$params[0] : true;

return $this->loader->load($name, $shared);

}

那么,接着就该执行$this->dispatcher->run($name, $params),那就看下Dispatcher对象中的run()方法,由于框架初始化时没有对route()方法进行设置前置和后置操作,所以直接执行$this->execute($this->get($name), $params)。

/**

* Dispatches an event.

*

* @param string $name Event name

* @param array $params Callback parameters

* @return string Output of callback

* @throws \Exception

*/

public function run($name, array $params = array()) {

$output = '';

// Run pre-filters

if (!empty($this->filters[$name]['before'])) {

$this->filter($this->filters[$name]['before'], $params, $output);

}

// Run requested method

$output = $this->execute($this->get($name), $params);

// Run post-filters

if (!empty($this->filters[$name]['after'])) {

$this->filter($this->filters[$name]['after'], $params, $output);

}

return $output;

}

接着来看Dispatcher对象中的execute方法,因为is_callable($callback)==true && is_array($callback),所以又再次调用self::invokeMethod($callback, $params)。

/**

* Executes a callback function.

*

* @param callback $callback Callback function

* @param array $params Function parameters

* @return mixed Function results

* @throws \Exception

*/

public static function execute($callback, array &$params = array()) {

if (is_callable($callback)) {

return is_array($callback) ?

self::invokeMethod($callback, $params) :

self::callFunction($callback, $params);

}

else {

throw new \Exception('Invalid callback specified.');

}

}

但是这次调用invokeMethod方法跟刚才有所不同,刚才的$callback是[$app, 'route'],现在的$callback是[$this Engine对象, '_route'],$params是一样的。然后invokeMethod方法中的$class为$this Engine对象,$method为'_route',is_object($class)为true。然后再执行$class->$method($params[0], $params[1]),这次在Engine对象中就可以调用到_route方法了。

接着来看Engine对象的_route()方法做了什么。$this->router()会触发当前对象的__call()魔术方法,根据刚才的分析$this->dispatcher->get($name)返回null。而$this->loader->get($name)返回true,然后就去执行$this->loader->load($name, $shared)。在Load对象的load方法中isset($this->classes[$name])为true,isset($this->instances[$name])返回false,在框架初始化时设置的$params和$backcall都为默认值,所以会执行$this->newInstance($class, $params),在newInstance方法中直接return new $class()。总结:$this->router()其实就是通过工厂模式去实例化框架初始化时所设置的'\flight\net\Router'类,依次论推$this->request()、$this->response()、$this->view()是一样的逻辑。

flight/Engine.php

/**

* Routes a URL to a callback function.

*

* @param string $pattern URL pattern to match

* @param callback $callback Callback function

* @param boolean $pass_route Pass the matching route object to the callback

*/

public function _route($pattern, $callback, $pass_route = false) {

$this->router()->map($pattern, $callback, $pass_route);

}

flight/core/Loader.php

/**

* Loads a registered class.

*

* @param string $name Method name

* @param bool $shared Shared instance

* @return object Class instance

* @throws \Exception

*/

public function load($name, $shared = true) {

$obj = null;

if (isset($this->classes[$name])) {

list($class, $params, $callback) = $this->classes[$name];

$exists = isset($this->instances[$name]);

if ($shared) {

$obj = ($exists) ?

$this->getInstance($name) :

$this->newInstance($class, $params);

if (!$exists) {

$this->instances[$name] = $obj;

}

}

else {

$obj = $this->newInstance($class, $params);

}

if ($callback && (!$shared || !$exists)) {

$ref = array(&$obj);

call_user_func_array($callback, $ref);

}

}

return $obj;

}

/**

* Gets a new instance of a class.

*

* @param string|callable $class Class name or callback function to instantiate class

* @param array $params Class initialization parameters

* @return object Class instance

* @throws \Exception

*/

public function newInstance($class, array $params = array()) {

if (is_callable($class)) {

return call_user_func_array($class, $params);

}

switch (count($params)) {

case 0:

return new $class();

case 1:

return new $class($params[0]);

case 2:

return new $class($params[0], $params[1]);

case 3:

return new $class($params[0], $params[1], $params[2]);

case 4:

return new $class($params[0], $params[1], $params[2], $params[3]);

case 5:

return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);

default:

try {

$refClass = new \ReflectionClass($class);

return $refClass->newInstanceArgs($params);

} catch (\ReflectionException $e) {

throw new \Exception("Cannot instantiate {$class}", 0, $e);

}

}

}

那$this->router()->map($pattern, $callback, $pass_route)操作的目的就是将用户定义的一个或多个route压入到Router对象的$routes属性索引数组中。至此,index.php中的Flight::route()操作就结束了,整个操作流程目的就是获取并解析用户定义的所有route,存储到Router对象的$routes属性索引数组中。接下来的Flight::start(),顾名思义,就是拿着处理好的路由请求信息去真正干活了。

flight/net/Router.php

/**

* Maps a URL pattern to a callback function.

*

* @param string $pattern URL pattern to match

* @param callback $callback Callback function

* @param boolean $pass_route Pass the matching route object to the callback

*/

public function map($pattern, $callback, $pass_route = false) {

$url = $pattern;

$methods = array('*');

//通过用户route定义的匹配规则,解析定义的methods,如'GET|POST /'

if (strpos($pattern, ' ') !== false) {

list($method, $url) = explode(' ', trim($pattern), 2);

$methods = explode('|', $method);

}

$this->routes[] = new Route($url, $callback, $methods, $pass_route);

}

Flight::start()要做的工作就是通过Request对象中获取的真实请求信息与用户所定义的路由进行匹配验证,匹配通过的然后通过Response对象返回给用户请求的结果。

根据刚才的分析,start()方法也会去调用Dispatcher类的invokeMethod方法,但$params是null,所以会执行$class->$method(),通过刚才的分析,会调用Engine对象__call()魔术方法的$this->dispatcher->run($name, $params)。在dispatcher对象的run()方法中,由于start()方法在框架初始化时设置有前置操作,所以在这里会执行所设置的前置操作,最后会执行Engine对象的_start()方法。

这里重点要分析的是从$route = $router->route($request)开始的操作。在实例化Request类获取$request对象时,会做些初始化操作,会将实际的请求信息设置在属性中,用于和用户定义的route进行匹配。

/**

* Starts the framework.

* @throws \Exception

*/

public function _start() {

$dispatched = false;

$self = $this;

$request = $this->request(); //获取Request对象

$response = $this->response(); //获取Response对象

$router = $this->router(); //获取Router对象

// Allow filters to run 设置start()方法执行的后置操作

$this->after('start', function() use ($self) {

$self->stop();

});

// Flush any existing output

if (ob_get_length() > 0) {

$response->write(ob_get_clean());

}

// Enable output buffering

ob_start();

// Route the request

while ($route = $router->route($request)) {

$params = array_values($route->params);

// Add route info to the parameter list

if ($route->pass) {

$params[] = $route;

}

// Call route handler

$continue = $this->dispatcher->execute(

$route->callback,

$params

);

$dispatched = true;

if (!$continue) break;

$router->next();

$dispatched = false;

}

if (!$dispatched) {

$this->notFound();

}

}

flight/net/Request.php

/**

* Constructor.

*

* @param array $config Request configuration

*/

public function __construct($config = array()) {

// Default properties

if (empty($config)) {

$config = array(

'url' => str_replace('@', '%40', self::getVar('REQUEST_URI', '/')),

'base' => str_replace(array('\\',' '), array('/','%20'), dirname(self::getVar('SCRIPT_NAME'))),

'method' => self::getMethod(),

'referrer' => self::getVar('HTTP_REFERER'),

'ip' => self::getVar('REMOTE_ADDR'),

'ajax' => self::getVar('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest',

'scheme' => self::getVar('SERVER_PROTOCOL', 'HTTP/1.1'),

'user_agent' => self::getVar('HTTP_USER_AGENT'),

'type' => self::getVar('CONTENT_TYPE'),

'length' => self::getVar('CONTENT_LENGTH', 0),

'query' => new Collection($_GET),

'data' => new Collection($_POST),

'cookies' => new Collection($_COOKIE),

'files' => new Collection($_FILES),

'secure' => self::getVar('HTTPS', 'off') != 'off',

'accept' => self::getVar('HTTP_ACCEPT'),

'proxy_ip' => self::getProxyIpAddress()

);

}

$this->init($config);

}

现在来看$router->route($request) 操作都做了什么。$route = $this->current()可以获取到刚才$this->router->map()保存的用户定义的第一个route,如果为false,就会直接返回404。否则,通过$route->matchMethod($request->method) && $route->matchUrl($request->url, $this->case_sensitive)来匹配验证用户定义的routes和实际请求的信息(请求方法和请求url)。

flight/net/Router.php

/**

* Routes the current request.

*

* @param Request $request Request object

* @return Route|bool Matching route or false if no match

*/

public function route(Request $request) {

while ($route = $this->current()) {

if ($route !== false && $route->matchMethod($request->method) && $route->matchUrl($request->url, $this->case_sensitive)) {

return $route;

}

$this->next();

}

return false;

}

flight/net/Route.php

/**

* Checks if a URL matches the route pattern. Also parses named parameters in the URL.

*

* @param string $url Requested URL

* @param boolean $case_sensitive Case sensitive matching

* @return boolean Match status

*/

public function matchUrl($url, $case_sensitive = false) {

// Wildcard or exact match

if ($this->pattern === '*' || $this->pattern === $url) {

return true;

}

$ids = array();

$last_char = substr($this->pattern, -1);

// Get splat

if ($last_char === '*') {

$n = 0;

$len = strlen($url);

$count = substr_count($this->pattern, '/');

for ($i = 0; $i < $len; $i++) {

if ($url[$i] == '/') $n++;

if ($n == $count) break;

}

$this->splat = (string)substr($url, $i+1); // /blog/* *匹配的部分

}

// Build the regex for matching

$regex = str_replace(array(')','/*'), array(')?','(/?|/.*?)'), $this->pattern);

//对路由匹配实现正则匹配 "/@name/@id:[0-9]{3}"

$regex = preg_replace_callback(

'#@([\w]+)(:([^/\(\)]*))?#',

function($matches) use (&$ids) {

$ids[$matches[1]] = null;

if (isset($matches[3])) {

return '(?P'.$matches[3].')';

}

return '(?P[^/\?]+)';

},

$regex

);

// Fix trailing slash

if ($last_char === '/') {

$regex .= '?';

}

// Allow trailing slash

else {

$regex .= '/?';

}

// Attempt to match route and named parameters

if (preg_match('#^'.$regex.'(?:\?.*)?$#'.(($case_sensitive) ? '' : 'i'), $url, $matches)) {

foreach ($ids as $k => $v) {

$this->params[$k] = (array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null;

}

$this->regex = $regex;

return true;

}

return false;

}

/**

* Checks if an HTTP method matches the route methods.

*

* @param string $method HTTP method

* @return bool Match status

*/

public function matchMethod($method) {

return count(array_intersect(array($method, '*'), $this->methods)) > 0;

}

 类似资料: