php微框架 flight源码阅读——3.路由Router实现及执行过程
现在来分析路由实现及执行过程,在项目目录下创建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; }
php微框架 flight源码阅读系列