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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章