• 关于laravel5.5控制器方法参数依赖注入原理深度解析及问题修复


      在laravel5.5中,可以根据控制器方法的参数类型,自动注入一个实例化对象,极大提升了编程的效率,但是相比较与Java的SpringMVC框架,功能还是有所欠缺,使用起来还是不太方便,主要体现在方法参数的注入不完全是按照参数名称进行的,如果改变了传入参数的顺序会导致类型不匹配的错误。

    一、控制器方法参数注入步骤设计

    1、在/routes/web.php中添加路由

    Route::get('/diary/show/{diary}/{page?}','DiaryDiaryController@list');

    2、编写控制器文件DiaryController.php放到/app/Http/Controllers/Diary/路径下面

    <?php
    namespace AppHttpControllersDiary;
    use AppHttpControllersController;
    class DiaryController extends Controller
    {
    public function show(AppDiary $diary,$page=11){ var_dump($diary->title,$page);
      }
    }

    3、构建模型AppDiary并安装到数据库(略)

    <?php
    namespace App;
    use IlluminateDatabaseEloquentModel;
    class Diary extends Model
    {
        protected $table='diary';
        public $timestamps = false;
    }

    4、访问控制器方法

    打开浏览器输入:“http://127.0.0.1//diary/show/4/12”

    此时输出数据表diary中id=4的title字段值和12

    二、注入参数类型说明

    说明:laravel会根据请求路由中匹配的{diary}和{page}变量和控制器方法中需要的方法参数类型,生成实例对象并注入到控制器方法中,

    针对不同的参数类型分三种情况:

    1、如果参数类型实现了UrlRoutable接口(即继承自IlluminateDatabaseEloquentModel),则在模型对象对应的表中查找id值为路由中匹配参数值的那条记录,并构建模型对象;

    2、如果参数类型为自定义类型(没有实现UrlRoutable接口),则laravel会构建一个对象后注入;

    3、如果参数类型为基础数据类型,并且名称为路由参数中定义的名称,则从路由参数中获取值;

    4、如果参数类型为基础数据类型,但名称未在路由参数中定义,如果有默认值则使用默认值,否则系统提示错误。

    三、目前注入参数存在的问题分析

    参考java的Spring MVC框架,laravel的参数类型注入还存在缺陷,主要体现在不完全是按照参数名称进行注入。

    1、如果改变控制器参数的顺序,会出现参数类型传递错误,如将DiaryController控制的show方法的参数改变顺序,则会导致错误发生:

    <?php
    namespace AppHttpControllersDiary;
    use AppHttpControllersController;
    class DiaryController extends Controller
    {
        public function show($page,AppDiary $diary){
            var_dump($diary->title,$page); 
      } 
    }

    2、由于参数类型为基础数据类型(参见二(3)),并不是按照名称来注入的参数,因此将代码改为如下,同样会运行正常

    <?php
    namespace AppHttpControllersDiary;
    use AppHttpControllersController;
    class DiaryController extends Controller
    {
        public function show(AppDiary $diary,$pag){
            var_dump($diary->title,$pag); 
      } 
    }

    四、laravel5.5控制器方法参数注入源码剖析

    1、实现了UrlRoutable接口的参数类型由路由中间件IlluminateRoutingMiddlewareSubstituteBinding实现构建

        public function handle($request, Closure $next)
        {
            $this->router->substituteBindings($route = $request->route());
            $this->router->substituteImplicitBindings($route);
    
            return $next($request);
        }

    IlluminateRoutingRouter的substituteImplicitBindings方法

        public function substituteImplicitBindings($route)
        {
            ImplicitRouteBinding::resolveForRoute($this->container, $route);
        }

    在IlluminateRoutingImplicitRouteBinding的resolveForRoute方法中实现

        public static function resolveForRoute($container, $route)
        {
            //从路由参数中获取参数值,$parameters为 ['diary':'4','page':'12']
            $parameters = $route->parameters();
            //获取控制器的函数参数列表,此处传入UrlRoutable::class,只返回实现UrlRoutable接口的参数
            $signatureParameters = $route->signatureParameters(UrlRoutable::class);
            foreach ($signatureParameters as $parameter) {
                if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
                    continue;
                }
    
                $parameterValue = $parameters[$parameterName];
                if ($parameterValue instanceof UrlRoutable) {
                    continue;
                }
                //构建模型的实例(基础自IlluminateDatabaseEloquentModel),此处为AppDiary
                $instance = $container->make($parameter->getClass()->name);
                //将参数值绑定到模型,参加IlluminateDatabaseEloquentModel的resolveRouteBinding方法
                if (! $model = $instance->resolveRouteBinding($parameterValue)) {
                    throw (new ModelNotFoundException)->setModel(get_class($instance));
                }
           //根据参数名称注入模型实例
    $route->setParameter($parameterName, $model); } }

    附加说明:

    此处调用$route对象(IlluminateRoutingRoute类型)setParameter方法,说明模型参数类型(见二(1))正是通过参数类型和参数名称同时匹配才注入模型实例

    2、其它类型的控制器参数在运行路由控制器时绑定

    关键部分在IlluminateRoutingControllerDispatcher的dispatch方法中实现:

        public function dispatch(Route $route, $controller, $method)
        {
            //解析控制器方法的参数
            $parameters = $this->resolveClassMethodDependencies(
                $route->parametersWithoutNulls(), $controller, $method
            );
            if (method_exists($controller, 'callAction')) {
                //通过IlluminateRoutingController的callAction调用控制器方法
                return $controller->callAction($method, $parameters);
            }
            //直接调用控制器方法
            return $controller->{$method}(...array_values($parameters));
        }

    调用resolveClassMethodDependencies方法

      public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
        {
            $instanceCount = 0;
            $values = array_values($parameters);
            //通过方法反射获取方法参数
            foreach ($reflector->getParameters() as $key => $parameter) {
            //如果有默认值则返回默认值,如果是自定义方法则构建实例返回
                $instance = $this->transformDependency(
                    $parameter, $parameters
                );
    
                if (! is_null($instance)) {
                    $instanceCount++;
                    //自定义类型(未实现UrlRoutable接口)的实例注入
                    $this->spliceIntoParameters($parameters, $key, $instance);
                } elseif (! isset($values[$key - $instanceCount]) &&
                          $parameter->isDefaultValueAvailable()) {
                    //未在路由参数中定义,但有默认值的参数注入
                    $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
                }
            }
            return $parameters;
        }

    问题总结说明:

    1、模型参数(见二(1))和名称在路由参数中定义的基础类型(见二(3))必须按照在路由中定义的顺序首先传入控制器方法,否则会出现类型不匹配的错误;

    2、自定义类型(见二(2))和名称未在路由参数中定义的基础类型参数(见二(4)),在控制器方法中按出现的顺序依次传入。

    五、问题修复

    打开/vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php文件,

    将resolveMethodDependencies方法修改为如下代码

        public function resolveMethodDependencies(array $parameters,ReflectionFunctionAbstract $reflector){
            $methodParameters=[];
            foreach($reflector->getParameters() as $key=>$parameter){
                $name=$parameter->getName();
                $instance=$this->transformDependency($parameter, $parameters);
                if(!is_null($instance)){
                    $methodParameters[]=$instance;
                }elseif(!isset($parameters[$name]) && $parameter->isDefaultValueAvailable()){
                    $methodParameters[]=$parameter->getDefaultValue();
                }else{
                    $methodParameters[]=isset($parameters[$name]) ? $parameters[$name] : null;
                }
            }
            return $methodParameters;
        }

    修改之后完全按照名称和类型注入控制器方法参数,代码变得更简洁,功能也更强大了!

    如果参数没有在路由中定义并且未提供默认值,那么将以null注入。

    本文为原创文章,版权归作者所有,转载请注明来源

  • 相关阅读:
    Linux进程管理及while循环(转)
    AT5661-[AGC040C]Neither AB nor BA【模型转换】
    CF573D-Bear and Cavalry【动态dp】
    关于专人整理和分析需求
    Codeforces 1005D Polycarp and Div 3
    [Luogu]P5513 [CEOI2013]Board
    IDEA Mybatis 中文数据添加到MySQL,显示乱码
    如何比较两个word文档的差异
    抗体计算设计
    抗体
  • 原文地址:https://www.cnblogs.com/springwind2006/p/7780524.html
Copyright © 2020-2023  润新知