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