解析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 Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
    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 Laravel\Lumen\Application(
    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' => 'App\Http\Controllers',
], 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 Laravel\Lumen;

use Illuminate\Container\Container;
use Laravel\Lumen\Routing\Router;

class Application extends Container
{
    // 加载RoutesRequests trait文件,该文件是用于处理路由。着重讲解。
    use Concerns\RoutesRequests;

    /**
     * The Router instance.
     *
     * @var \Laravel\Lumen\Routing\Router
     */
    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 Laravel\Lumen\Routing;

use Illuminate\Support\Arr;

class Router
{
    /**
     * The application instance.
     *
     * @var \Laravel\Lumen\Application
     */
    public $app;


    /**
     * Router constructor.
     *
     * @param  \Laravel\Lumen\Application  $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' => 'App\Http\Controllers',
], 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) "App\Http\Controllers\ExampleController@add"
                }
    }
}

二.Run部分

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

$app->run();

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

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 框架很多地方都是用到了反射机制,这篇文章只是分析了一小部分。多分析框架代码,还是有好处的。

posted @ 2021-03-29 23:59  Yun-Hai  阅读(516)  评论(0编辑  收藏  举报