現在來分析路由實現及執行過程,在項目目錄下創建index.php,使用文檔中的路由例子(含有路由規則匹配),如下:
<?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[1].'>'.$matches[3].')';
}
return '(?P<'.$matches[1].'>[^/\?]+)';
},
$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;
}