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

一航jason 2019-06-29

现在来分析路由实现及执行过程,在项目目录下创建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源码阅读系列

相关推荐

TiDBPingCAP / 0评论 2020-07-29