• 解析Laravel框架—路由处理


    介绍

    Laravel框架,也用了几年。我很好奇,该框架是如何解析路由(routes/web.php)文件。为什么,如下代码,就可以执行ExampleController控制器中的add方法。于是乎,我就去一层层查看框架代码,揭下它神秘的面纱。

    注:因为框架文件代码量过大,本文所贴代码,皆已删减。

    $router->get('/', 'ExampleController@add');
    

    准备

    • Lumen(精简版的Laravel框架),选择的版本是Lumen8.2.3。
    • 好用的ide工具,选择的是PhpStorm。
    • 本地PHP版本,选择的是7.2.30。

    正文

    查看入口文件index.php,该文件位于public文件夹下。我们先分析第一行,require部分。

    <?php
    
    $app = require __DIR__.'/../bootstrap/app.php';
    
    $app->run();
    
    

    一.Require部分

    require主要就是加载bootstrap文件夹下的app.php文件。

    2.我把app.php文件中的代码贴到了下面。为了便于读者浏览,我在代码中写入了注释。

    <?php
    
    
    // 加载vendor文件夹中的扩展包。
    require_once __DIR__.'/../vendor/autoload.php';
    
    // 加载env变量
    (new LaravelLumenBootstrapLoadEnvironmentVariables(
        dirname(__DIR__)
    ))->bootstrap();
    
    // 设置默认的时区
    date_default_timezone_set(env('APP_TIMEZONE', 'UTC'));
    
    /*
    |--------------------------------------------------------------------------
    | Create The Application
    |--------------------------------------------------------------------------
    |
    | Here we will load the environment and create the application instance
    | that serves as the central piece of this framework. We'll use this
    | application as an "IoC" container and router for this framework.
    | 
    | 在这里,我们将加载环境变量(env文件)并创建应用程序实例,作为这个框架的中心。 
    | 
    */
    
    
    // Application代码
    $app = new LaravelLumenApplication(
        dirname(__DIR__)
    );
    
    
    /*
    |--------------------------------------------------------------------------
    | Load The Application Routes
    |--------------------------------------------------------------------------
    |
    | Next we will include the routes file so that they can all be added to
    | the application. This will provide all of the URLs the application
    | can respond to, as well as the controllers that may handle them.
    |
    | 我们将加载路由文件。这包括所有的请求地址,看看我们如何更好的处理它们。
    */
    
    // 在本文件的上半部分,我们已创建一个app实例。这里就是将实例与路由文件绑定到一起。
    $app->router->group([
        'namespace' => 'AppHttpControllers',
    ], function ($router) {
        require __DIR__.'/../routes/web.php';
    });
    
    return $app;
    
    

    3.我们可以看到,$app实例,是由 new Application产生。然后 $app调用router属性的group方法,由此将路由文件,加载到应用中。查看下Application类。

    通过Application类文件,我们可以看到构造函数中调用了bootstrapRouter方法。此方法中,又包含了对router属性的赋值。

    文件位置:vendor/laravel/lumen-framework/src/Application.php

    <?php
    
    namespace LaravelLumen;
    
    use IlluminateContainerContainer;
    use LaravelLumenRoutingRouter;
    
    class Application extends Container
    {
        // 加载RoutesRequests trait文件,该文件是用于处理路由。着重讲解。
        use ConcernsRoutesRequests;
    
        /**
         * The Router instance.
         *
         * @var LaravelLumenRoutingRouter
         */
        public $router;
        
        /**
         * Create a new Lumen application instance.
         * 创建app实例,如下构造函数。
         *  
         * @param  string|null  $basePath
         * @return void
         */
        public function __construct($basePath = null)
        {
            // 路由部分。
            $this->bootstrapRouter();
        }
    
        /**
         * Bootstrap the router instance.
         * 为router属性赋值。
         *
         * @return void
         */
        public function bootstrapRouter()
        {
            $this->router = new Router($this);
        }
    }
    
    

    4.接下来看看,Router类中的group方法,又起到了什么作用。
    Router类文件位置:vendor/laravel/lumen-framework/src/Routing/Router.php

    <?php
    
    namespace LaravelLumenRouting;
    
    use IlluminateSupportArr;
    
    class Router
    {
        /**
         * The application instance.
         *
         * @var LaravelLumenApplication
         */
        public $app;
    
    
        /**
         * Router constructor.
         *
         * @param  LaravelLumenApplication  $app
         */
        public function __construct($app)
        {
            $this->app = $app;
        }
    
        /**
         * Register a set of routes with a set of shared attributes.
         *
         * @param  array  $attributes
         * @param  Closure  $callback
         * @return void
         */
        public function group(array $attributes, Closure $callback)
        {
            if (isset($attributes['middleware']) && is_string($attributes['middleware'])) {
                $attributes['middleware'] = explode('|', $attributes['middleware']);
            }
    
            $this->updateGroupStack($attributes);
    
            call_user_func($callback, $this);
    
            array_pop($this->groupStack);
        }
    
        
        /**
         * Update the group stack with the given attributes.
         * 整合传入的参数。存储到groupStack数组。
         *
         * @param  array  $attributes
         * @return void
         */
        protected function updateGroupStack(array $attributes)
        {
            if (! empty($this->groupStack)) {
                $attributes = $this->mergeWithLastGroup($attributes);
            }
    
            $this->groupStack[] = $attributes;
        }
    
        /**
         * Add a route to the collection.
         *
         * @param  array|string  $method
         * @param  string  $uri
         * @param  mixed  $action
         * @return void
         */
        public function addRoute($method, $uri, $action)
        {
            $action = $this->parseAction($action);
    
            $attributes = null;
    
            if ($this->hasGroupStack()) {
                $attributes = $this->mergeWithLastGroup([]);
            }
    
            if (isset($attributes) && is_array($attributes)) {
                if (isset($attributes['prefix'])) {
                    $uri = trim($attributes['prefix'], '/').'/'.trim($uri, '/');
                }
    
                if (isset($attributes['suffix'])) {
                    $uri = trim($uri, '/').rtrim($attributes['suffix'], '/');
                }
    
                $action = $this->mergeGroupAttributes($action, $attributes);
            }
    
            $uri = '/'.trim($uri, '/');
    
            if (isset($action['as'])) {
                $this->namedRoutes[$action['as']] = $uri;
            }
    
            if (is_array($method)) {
                foreach ($method as $verb) {
                    $this->routes[$verb.$uri] = ['method' => $verb, 'uri' => $uri, 'action' => $action];
                }
            } else {
                $this->routes[$method.$uri] = ['method' => $method, 'uri' => $uri, 'action' => $action];
            }
        }
    
        /**
         * Register a route with the application.
         *
         * @param  string  $uri
         * @param  mixed  $action
         * @return $this
         */
        public function get($uri, $action)
        {
            $this->addRoute('GET', $uri, $action);
    
            return $this;
        }
    }
    
    

    5.我把group方法,单独拿出来讲解。这个方法,有两个入参,一个是数组,另一个是回调方法。

    public function group(array $attributes, Closure $callback)
    {
        call_user_func($callback, $this);
    }
    

    在app.php文件中,咱们填入的参数如下:

    app.php文件。group方法的调用参数。
    
    [
        'namespace' => 'AppHttpControllers',
    ], function ($router) {
        require __DIR__.'/../routes/web.php';
        // 实际加载后的代码,如下所示:
        // $router->get('/', 'ExampleController@add');
    }
    

    6.使用call_user_func执行回调方法,当前路由为get请求,于是调用Router类中的get方法。

    get方法中,又进行addRoute操作,addRoute就是将路由存入routes属性中。加载操作,到这里,就结束了。下一步,看看如何执行。

    public function get($uri, $action)
    {
        $this->addRoute('GET', $uri, $action);
    
        return $this;
    }
    
    public function addRoute($method, $uri, $action)
    {
        $uri = '/'.trim($uri, '/');
    
        if (isset($action['as'])) {
            $this->namedRoutes[$action['as']] = $uri;
        }
    
        if (is_array($method)) {
            foreach ($method as $verb) {
                $this->routes[$verb.$uri] = ['method' => $verb, 'uri' => $uri, 'action' => $action];
            }
        } else {
            $this->routes[$method.$uri] = ['method' => $method, 'uri' => $uri, 'action' => $action];
        }
    }
    
    
    // 存入后的格式
    $this->routes[$method.$uri] = 
    array(1) {
        ["GET/"]=>array(3) {
                    ["method"]=> string(3) "GET"
                    ["uri"]=> string(1) "/"
                    ["action"]=> array(1) {
                                ["uses"]=> string(42) "AppHttpControllersExampleController@add"
                    }
        }
    }
    
    

    二.Run部分

    1.接下来,分析run部分。

    $app->run();
    

    $app变量是Application实例,于是去Application.php,查找run方法。最后在该文件引入的trait 类, ConcernsRoutesRequests 文件中找到。

    RoutesRequests文件位置:vendor/laravel/lumen-framework/src/Concerns/RoutesRequest.php

    public function run($request = null)
    {
        // 请求处理
        $response = $this->dispatch($request);
    
        // 返回值
        if ($response instanceof SymfonyResponse) {
            $response->send();
        } else {
            echo (string) $response;
        }
    
        if (count($this->middleware) > 0) {
            $this->callTerminableMiddleware($response);
        }
    }
    

    2.请求处理部分是在dispatch这个方法中。核心代码
    以下,就是我删减,合并后的代码。这样看起来,就清晰明了。

    首先是通过routes,验证该路由是否存在。存在,就进行下一步的处理,拆分controller与method名称。使用make实例化controller, 去验证method是否存在。

    $instance 是make实例化,核心就是make。 make里面使用了反射类。

    创建反射,之后从给出的参数创建一个新的类实例。执行[$instance, 'add'] () 就可以调用ExampleController中的add方法。这样就达到了,执行控制器方法的效果。

    $reflector = new ReflectionClass($concrete);

    $reflector->newInstanceArgs($instances);

        public function dispatch($request = null)
        {
            // 丢到一个通道去执行操作
            return $this->sendThroughPipeline($this->middleware, function ($request) use ($method, $pathInfo) {
                $this->instance(Request::class, $request);
    
                // 判断路由是否存在,存在继续执行
                if (isset($this->router->getRoutes()[$method.$pathInfo])) {
                      return $this->prepareResponse($this->callControllerAction($routeInfo));
                }
            });
            
        }
        
        // 处理路由,将路由中的controller与method 拆开
        protected function callControllerAction($routeInfo)
        {
            $uses = $routeInfo[1]['uses'];
    
            if (is_string($uses) && ! Str::contains($uses, '@')) {
                $uses .= '@__invoke';
            }
    
            [$controller, $method] = explode('@', $uses);
    
            // $instance赋值
            if (! method_exists($instance = $this->make($controller), $method)) {
                throw new NotFoundHttpException;
            }
    
            return $this->callControllerCallable($instance, $method, $routeInfo);
            
        }
        
        
        protected function callControllerCallable($instance ,$method, $routeInfo)
        {
            return $this->prepareResponse(
                $this->call([$instance, $method], $routeInfo[2])
            );
        }
        
        public function call($callback, array $parameters = [], $defaultMethod = null)
        {
            return $callback();
        }
    

    3.如下,就是针对路由,产生的反射类及类实例。

    $router->get('/', 'ExampleController@add');
    
    $reflection = new ReflectionClass('ExampleController');
    $instance = $reflection->newInstanceArgs();
    [$instance, 'add']()
    

    总结

    Laravel 框架很多地方都是用到了反射机制,这篇文章只是分析了一小部分。多分析框架代码,还是有好处的。

    分享是一种美德
  • 相关阅读:
    关于string的对象引用
    浅谈Java与C#
    TSQL: 用26个字母表示10000条不重复的编号
    解决Firefox访问12306"连接不受信任"的问题
    计算机速成课 第十七集 集成电路&摩尔定律
    纯CSS实现复杂表头和多列同时固定
    请记住:以管理员身份运行Visual Studio Ultimate Beta 11的安装包
    Mybatis核心配置文件
    Mybatis代理开发
    Spring注解开发
  • 原文地址:https://www.cnblogs.com/jingying/p/14595060.html
Copyright © 2020-2023  润新知