MENU

# 03: 路由中间件之 SubstituteBindings

January 14, 2020 • Laravel 源码

SubstituteBindings 中间件

SubstituteBindings 作用:显式和隐式地根据请求参数绑定对应数据模型。

调用 SubstituteBindings

Laravel 默认定义了 webapi 两个路由组,而这两个组由组分别指定了 webapi 中间件组:

// 路径:app/Providers/RouteServiceProvider.php
protected function mapWebRoutes()
{
    Route::middleware('web') // web 中间件组
        ->namespace($this->namespace)
        ->group(base_path('routes/web.php'));
}

protected function mapApiRoutes()
{
Route::prefix('api')
    ->middleware('api') // api 中间件组
        ->namespace($this->namespace)
        ->group(base_path('routes/api.php'));
}

查看这两个中间件组,可以发现它们都在最后使用了 SubstituteBindings 中间件

// 路径:app/Http/Kernel.php
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class, // <--- 在这里 
    ],

    'api' => [
        'throttle:60,1',
        \Illuminate\Routing\Middleware\SubstituteBindings::class, // <--- 在这里 
    ],
];

查看 SubstituteBindingshandle 方法,可以看到这里主要调用了 2 个方法:

  • substituteBindings:处理显式绑定
  • substituteImplicitBindings:处理隐式绑定
// 路径:vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php
public function handle($request, Closure $next)
{
    $this->router->substituteBindings($route = $request->route());

    $this->router->substituteImplicitBindings($route);

    return $next($request);
}

显式模型绑定原理

查看 substituteBindings 方法源码,大概思路就是遍历路由参数,根据参数名称查看是否有对应的绑定关系,如果有则调用绑定的闭包获取相应的模型对象,接着就用该对象替换参数值,而后面注入到控制器方法时参数值自然就编程了相应的模型对象了:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/Router.php
public function substituteBindings($route)
{
    foreach ($route->parameters() as $key => $value) { // (1)遍历路由参数
        if (isset($this->binders[$key])) { // (2)查看参数是否有对应的绑定
            $route->setParameter($key, $this->performBinding($key, $value, $route)); // (3)处理绑定的参数
        }
    }
    return $route;
}

可以看到调用了好几个函数和变量,这些都是什么呢?我们一个一个分析:

  • $route->parameters():获取路由参数,返回的是一组关联数组,如:[['post' => '1'], ...]
  • $this->binders:获取所有显式绑定的 [[参数名 => 闭包], ...] 一组数据,最终的参数值就是通过这些闭包获取的
  • $this->performBinding($key, $value, $route):执行绑定的闭包,$value 是参数值,最终返回所需的模型对象
  • $route->setParameter():重新设置该参数的参数值,就是把参数值替换为刚刚获取的模型对象

现在整个过程就十分清晰了,再看回 performBinding() 函数,看它是如何获取模型对象的?实际上这个函数十分简单,就是调用参数名绑定的闭包,然后把参数值和当前路由传了进去:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/Router.php
protected function performBinding($key, $value, $route)
{
    return call_user_func($this->binders[$key], $value, $route);
}

那么这些绑定的闭包又是如何注入到路由处理器中的呢?这就要看回显示绑定是如何声明的了:

// 路径:app/Providers/RouteServiceProvider.php
public function boot()
{
    parent::boot();

    //(1)指定模型
    Route::model('user', App\User::class);
    // (2)传递闭包
    Route::bind('user', function ($value) {
        return App\User::where('name', $value)->first() ?? abort(404);
    });
}

声明显示绑定有两种方法,一种是通过 Route::model() 方法指定模型,另一种是通过 Route::bind() 方法传递闭包,先查看 Route::model() 方法,不难发现其本质也是通过 Route::bind() 方法声明绑定的:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/Router.php
public function model($key, $class, Closure $callback = null)
{
    $this->bind($key, RouteBinding::forModel($this->container, $class, $callback));
}

既然这样我们还是直奔 bind() 方法吧,回头再看 model() 方法。OK,就是这里把参数绑定关系写到 binders 数组中的:

路径:vendor/laravel/framework/src/Illuminate/Routing/Router.php
public function bind($key, $binder)
{
    $this->binders[str_replace('-', '_', $key)] = RouteBinding::forCallback(
        $this->container, $binder
    );
}

照理来说传入的 $binder 已经是一个闭包了,RouteBinding::forCallback() 又对它进行了什么处理呢?

// 路径:vendor/laravel/framework/src/Illuminate/Routing/RouteBinding.php
public static function forCallback($container, $binder)
{
    if (is_string($binder)) {
        return static::createClassBinding($container, $binder);
    }
    return $binder;
}

protected static function createClassBinding($container, $binding)
{
    return function ($value, $route) use ($container, $binding) {
        // If the binding has an @ sign, we will assume it's being used to delimit
        // the class name from the bind method name. This allows for bindings
        // to run multiple bind methods in a single class for convenience.
        [$class, $method] = Str::parseCallback($binding, 'bind');

        $callable = [$container->make($class), $method];

        return $callable($value, $route);
    };
}

原来 bind() 方法实际上还支持类似路由中 class@func 的传参方法,这部分就是将 class@func 转成闭包的过程。闭包的逻辑大概是先分割出类名和方法名,接着通过 Laravel 容器的 make 方法创建出该类的实例,最后调用实例中的方法,喵啊!最后还是把视角返回 model() 函数上,model() 本质上也是调用 bind(),因此它需要通过 RouteBinding::forModel() 方法将传入的模型类转化成一个闭包:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/RouteBinding.php
public static function forModel($container, $class, $callback = null)
{
    return function ($value) use ($container, $class, $callback) {
        if (is_null($value)) {
            return;
        }

        //(1)获取模型实例
        $instance = $container->make($class);

        //(2)获取指定的模型对象
        if ($model = $instance->resolveRouteBinding($value)) {
            return $model;
        }

        if ($callback instanceof Closure) {
            return $callback($value);
        }

        throw (new ModelNotFoundException)->setModel($class);
    };
}

这一部分也十分简单,就是查询数据库是否存在指定的记录,找到就把相应的对象返回回去,找不到就抛出 ModelNotFoundException 异常,值得注意的是我们还可以在声明显示绑定时加上第三个参数 $callback,自定义查询失败的回调。值得注意的还有 resolveRouteBinding() 方法,看到这里就明白为什么 getRouteKeyName() 方法可以指定查询的字段名了,默认是 id

// vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
public function resolveRouteBinding($value)
{
    return $this->where($this->getRouteKeyName(), $value)->first();
}

OK,整个显式绑定的解析就到这里,简单地说就是我们先声明了哪些路由参数需要预处理、如何预处理的过程。另外有一点是,在判断路由参数是否存在显示绑定时会遍历整个 $binders 数组,这里会有一点性能的问题。

隐式模型绑定原理

首先需要清楚的是,隐式绑定默认有一种约定,就是路由参数名与模型名称是匹配的,如:参数 user -> 模型 User、参数 user_profile -> 模型 UserProfile。隐式绑定由 substituteImplicitBindings() 方法处理,跳转过来发现实际调用的是 ImplicitRouteBinding::resolveForRoute() 方法:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/Router.php
public function substituteImplicitBindings($route)
{
    ImplicitRouteBinding::resolveForRoute($this->container, $route);
}

简单描述下处理过程,详细看注释部分:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php
public static function resolveForRoute($container, $route)
{
    //(1)获取路由参数
    $parameters = $route->parameters();

    // (3)循环路由回调方法的参数中,类型属于 `UrlRoutable::class` 的形参
    foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
        // (3-1)判断形参在路由参数中是否有匹配的参数
        if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
            continue;
        }

        // (3-2)获取路由参数值
        $parameterValue = $parameters[$parameterName];

        //  (3-3)如果路由参数值是 UrlRoutable 的实例,则跳出循环,这里实际上是为了避免重复处理,因为有可能在显示绑定中已经处理过了
        if ($parameterValue instanceof UrlRoutable) {
            continue;
        }

        // (3-4)获取模型的实例
        $instance = $container->make($parameter->getClass()->name);

        // (3-5)获取对应查询的模型实例
        if (! $model = $instance->resolveRouteBinding($parameterValue)) {
            throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
        }

        // (3-6)替换路由参数值为实际需要的模型实例
        $route->setParameter($parameterName, $model);
    }
}

继续深入,先看 $route->signatureParameters(UrlRoutable::class) 如何获得模型类型的参数。可以看到实际上调用的是 RouteSignatureParameters::fromAction() 方法,这里的 $this->action 是一个关联数组,包含了当前路由的具体处理函数的信息,具体可以 dd($this->action) 查看。

// 路径:vendor/laravel/framework/src/Illuminate/Routing/Route.php
public function signatureParameters($subClass = null)
{
    return RouteSignatureParameters::fromAction($this->action, $subClass);
}

继续追踪可以看到,实际是通过「反射」获取参数的:

// 路径:vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php
public static function fromAction(array $action, $subClass = null)
{
    // (1)获取处理函数的所有的参数
    $parameters = is_string($action['uses'])
                    ? static::fromClassMethodString($action['uses'])
                    : (new ReflectionFunction($action['uses']))->getParameters();

    // 过滤出类型属于 $subClass 的参数
    return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) {
        return $p->getClass() && $p->getClass()->isSubclassOf($subClass);
    });
}

OK,整个隐式绑定的解析就到这里,相对来说要比显式绑定简单了一些。

Last Modified: January 20, 2020