V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
qloog
V2EX  ›  PHP

分享 - 如何从零开始理解 Laravel 依赖注入

  •  
  •   qloog · 2018-05-30 14:50:12 +08:00 · 2329 次点击
    这是一个创建于 2129 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址: 从零开始理解 Laravel 依赖注入

    大家在使用 Laravel 的过程中,可能会感觉到在 Laravel 里很多神奇的东西会发生。依赖注入似乎是一个。但它真的很神奇吗? 下面我们来具体看下。

    Laravel 容器(Container)

    Laravel 中的服务容器其实就是一个依赖注入容器和应用程序的注册表。

    Laravel Container 是用于管理依赖项和存储对象的强大工具,可用于各种目的; 可以存储对象并在 Facades 中使用它们。

    Laravel 通常使用依赖注入。即使访问 Request 我们也可以使用注入,比如。

    public function __construct(Request $request)
    

    当尝试向类中注入对象时,Container 使用 Reflection API 检查构造函数方法并检索依赖项的内容。

    Reflection Api 是什么

    首先,反射 API 从深度维度中获取能量(抽象理解就好了),因此在使用反射 API 时必须小心。使用它,但不要滥用它。当检查许多对象时,反射是昂贵的,它有可能扰乱整个宇宙(有点夸张哈)。

    反射通常被定义为程序能力,主要是指检查自身并在执行时修改其逻辑。

    可以从官网查看 PHP.net 具体描述。

    从 PHP5 开始 PHP 带有一个完整的反射 API,增加了对类,接口,函数,方法和扩展进行逆向工程的能力。另外,反射 API 提供了检索函数,类和方法的 doc 注释的方法。 发射在 PHP 中很流行。事实上,有几种情况即使不知道它也可以使用它。一些 PHP 的内置函数间接使用了反射 API--其中一个是call_user_func 函数。

    我们可能经常使用 get_classget_class_method 来理解别人的代码。

    下面我们来快速看看如何使用 Reflection API 来处理函数。

    ReflectionFunction

    可以使用 ReflectionFunction 获取函数的信息。

    <?php
    
    function functionWithParameters($param1){
    
    }
    
    
    $reflectionFunction = new ReflectionFunction('functionWithParameters');
    
    $name = $reflectionFunction->getName(); // functionWithParameters
    
    $parameters = $reflectionFunction->getParameters();
    
    /*
    Array
        (
            [0] => ReflectionParameter Object
                (
                    [name] => param1
                )
        )
     */
    

    ReflectionClass

    容器大多数情况和类一起工作,所以我们需要学习如何使用 ReflectionClass。在 ReflectionClass 中 公开了一组用于获取对象信息的方法。

    我们将使用它来获得依赖关系。但首先我们需要首先看看 构造函数

    这实际上很简单,你想知道的一切都可以在 ReflectionClass 上查看。

    <?php
    
    class OurDependencyClass{}
    
    class OurTestClass
    {
    
      public function __construct(OurDependencyClass $anotherClass)
      {
      }
    }
    
    $reflectedClass = new ReflectionClass(new OurTestClass());
    
    // or
    
    $reflectedClass = new ReflectionClass('OurTestClass');
    

    你可以将实例或类名称提供给 ReflectionClass。它可以解析给定的参数。

    我们可以通过调用 getConstructor 方法来检查构造函数。它返回一个 ReflectionMethod,它包含我们需要的几乎所有东西。

    <?php
    
    $reflection = new ReflectionClass('OurTestClass');
    
    $constructor = $reflection->getConstructor();
    

    我们需要检查参数,前面已经解释过 ReflectionFuction

    警告: 如果类没有构造函数方法,则 $constructor 将被赋值为 null。所以你也应该检查一下。

    <?php
    
    // now we can retrieve out parameters
    
    $parameters = $constructor->getParameters();
    
    /*
    
    array(1) {
      [0]=>
      object(ReflectionParameter)#3 (1) {
        ["name"]=>
        string(10) "otherClass"
      }
    }
    
     output will be like this
    
    */
    

    它返回一个 ReflectionParameter 数组,并检索有关函数或方法参数的信息。

    现在,我们将检查所有这些参数并确定我们需要解决的问题。

    <?php
    
    
    foreach ($parameters as $parameter)
    {
      $class = $parameter->getClass();
    
      if(null === $class){
           // this parameter doesn't have a class name
           // we can't resolve it so we will skip for now
      }
    
      $name = $class->name; // we'll use it later
    }
    

    我们必须知道类名来解决依赖关系,现在让我们停下来一分钟,弄清楚这个过程:。

    经典的 PHP 代码

    以下是不使用 Container 的代码大致工作的方式:

    • Application 依赖类 Foo, 所以我们需要这么做:
    • 使用 Application 前创建 Foo
    • Application 中调用 Foo
    • Foo 依赖 Bar (比如一个 service), 所以:
    • 在使用 Foo 前,先创建 Bar
    • Foo 中调用 Bar
    • Bar 依赖 Bim (比如可能是一个 service, 也可能是 repository, …), 所以:
    • 在使用 Bar 前先要创建 Bim
    • Bar does something

    ** 感觉如何?**

    使用依赖注入

    以下是使用 Container 的代码大致工作的方式:

    • Application 依赖 Foo, Foo 依赖 Bar, Bar 依赖 Bim, 所以:
    • Application 直接发现的是 Bim, 所以直接创建 Bim
    • 创建 Bim 时发现需要 Bar, 所以 Application 创建 Bar 并返回给 Bim
    • 创建 Bar 时发现需要 Foo, 所以 Application 创建 Foo 并返回给 Bar
    • Application 调用 Foo
    • Foo 调用 Bar
    • Bar 调用 Bim
    • Bim does something

    这是 控制反转 的模式。被调用者和被调用者之间的依赖性控制是相反的。

    下面我们在代码中模拟这种情况。

    让我们再看看我们的代码。我们挖掘了构造函数的参数,现在我们知道我们需要解决什么。所需要的是一个递归函数,直到无法解决为止。让我们把所有这些放在一起。

    <?php
    
    class Container
    {
      /**
       *
       *  @param mixed $class
       * 
       */
      public function make($class)
      {
        // pass $class into ReflectionClass
        // note that: ReflectionClass may throw an Exception if user puts
        // a class name that doesn't exist.
        $reflection = new ReflectionClass($class);
    
        $constructor = $reflection->getConstructor();
    
        // we'll store the parameters that we resolved
        $resolvedParameters = [];
    
        foreach ($constructor->getParameters() as $parameter){
    
            $parameterClass = $parameter->getClass();
    
            if(null === $parameterClass){
               // this parameter is probably is a primitive variable
               // we can't resolve it so we will skip for now
            }
    
            $parameterName = $parameter->getName();
            $className = $parameterClass->name;
    
            // this function is becoming resursive now.
            // it'll continue 'till  nothing left.
    
            $resolvedParameters[$parameterName] = $this->make($className);
    
            // we need to know which value belongs to which parameter
            // so we'll store as an associative array.
        }
      }
    }
    
    

    不要试图运行这个!它肯定会失败的。

    我们也需要解决原始变量,也就是参数。所以我们只需在我们的 make 方法中添加一个可选参数。

    <?php
    
     /* change the method definition as follows;
     public function make($class, $params = [])
     */
    
    $parameterName = $parameter->getName();
    
    if(null === $parameterClass){
       // if our primitive parameter given by user we'll use it
       // if not, we'll just throw an Exception
       if(isset($params[$parameterName])){
           // this is just a very simple example
           // in real world you have to check whether this parameter passed by
           // reference or not
           $resolvedParameters[$parameterName]= $params[$parameterName];
        }else{
            throw new Exception(
                   sprintf('Container could not solve %s parameter', $parameterName)
                 );
        }
    }
    

    警告: 我们只考虑变量是否存在。但在现实世界中,你必须考虑更多的情况。如;它是一个可选参数吗?它有默认值吗?

    我们将创建一个新的实例来返回它。

    <?php
    
    // this will create and return a new instance of given class.
    return $reflection->newInstanceArgs($resolvedParameters);
    

    就是这么简单!但是我们的工作还没有完成。我们来看看代码现在的样子。

    <?php
    
    
    class Container
    {
    
        /**
         *
         * @param mixed $class
         * @param array $params
         *
         */
        public function make($class, array $params = [])
        {
    
    
            // pass $class into ReflectionClass
            // note that: ReflectionClass may throw an Exception if user puts
            // a class name that doesn't exist.
            $reflection = new ReflectionClass($class);
    
            //  if object does not have an constructor method, $constructor will be assigned null.
            // you better have check this too
            $constructor = $reflection->getConstructor();
    
            // we'll store the parameters that we resolved
            $resolvedParameters = [];
    
    
            foreach ($constructor->getParameters() as $parameter) {
    
                $parameterClass = $parameter->getClass();
                $className = $parameterClass->name;
    
                $parameterName = $parameter->getName();
    
    
                if (null === $parameterClass) {
    
                    // if our primitive parameter given by user we'll use it
                    // if not, we'll just throw an Exception
                    if (isset($params[$parameterName])) {
                        // this is just a very simple example
                        // in real world you have to check whether this parameter passed by
                        // reference or not
                        $resolvedParameters[$parameterName] = $params[$parameterName];
                    } else {
                        throw new Exception(
                            sprintf('Container could not solve %s parameter', $parameterName)
                        );
                    }
    
                } else {
    
    
                    // this function is becoming recursive now.
                    // it'll continue 'till  nothing left.
    
                    $resolvedParameters[$parameterName] = $this->make($className);
    
                    // we need to know which value belongs to which parameter
                    // so we'll store as an associative array.
    
                }
            }
    
            return $reflection->newInstanceArgs($resolvedParameters);
        }
    }
    

    到目前为止,我们已经学习了什么是 Reflection API 以及我们如何使用它来反射函数,参数和类。

    回到 Laravel

    让我们回到 Laravel。看看 Laravel 是如何管理这一进展。让我们弄清楚。

    <?php
    
    // When you call App::make or app()->make it refers to Container::make and it's just a duplication of  Container::resolve
    
    class Container implements ArrayAccess, ContainerContract
    {
        /**
         * Resolve the given type from the container.
         *
         * @param  string  $abstract
         * @param  array  $parameters
         * @return mixed
         */
        protected function resolve($abstract, $parameters = [])
        {
    
            $this->with[] = $parameters;
    
            $concrete = $this->getConcrete($abstract);
    
            // We're ready to instantiate an instance of the concrete type registered for
            // the binding. This will instantiate the types, as well as resolve any of
            // its "nested" dependencies recursively until all have gotten resolved.
            if ($this->isBuildable($concrete, $abstract)) {
                $object = $this->build($concrete);
            } else {
                $object = $this->make($concrete);
            }
    
            return $object;
    
        }
     
    }
    

    原始功能更长,更复杂。我减少了大部分的复杂性。

    Laravel 检查对象并确定它是否可以轻松实例化,或者是否需要首先解决“嵌套”依赖关系。

    就像我们做的一样?

    <?php
    
    
        /**
         * Instantiate a concrete instance of the given type.
         *
         * @param  string  $concrete
         * @return mixed
         *
         * @throws \Illuminate\Contracts\Container\BindingResolutionException
         */
        public function build($concrete)
        {
    
            $reflector = new ReflectionClass($concrete);
    
            // If the type is not instantiable, the developer is attempting to resolve
            // an abstract type such as an Interface of Abstract Class and there is
            // no binding registered for the abstractions so we need to bail out.
            if (! $reflector->isInstantiable()) {
                return $this->notInstantiable($concrete);
            }
    
            $this->buildStack[] = $concrete;
    
            $constructor = $reflector->getConstructor();
    
            // If there are no constructors, that means there are no dependencies then
            // we can just resolve the instances of the objects right away, without
            // resolving any other types or dependencies out of these containers.
            if (is_null($constructor)) {
                array_pop($this->buildStack);
    
                return new $concrete;
            }
    
            $dependencies = $constructor->getParameters();
    
            // Once we have all the constructor's parameters we can create each of the
            // dependency instances and then use the reflection instances to make a
            // new instance of this class, injecting the created dependencies in.
            $instances = $this->resolveDependencies(
                $dependencies
            );
    
            array_pop($this->buildStack);
    
            return $reflector->newInstanceArgs($instances);
        }
    

    继续研究 Laravel 如何解决依赖关系。所以我们再深入一点。

    <?php
    
    
        /**
         * Resolve all of the dependencies from the ReflectionParameters.
         *
         * @param  array  $dependencies
         * @return array
         */
        protected function resolveDependencies(array $dependencies)
        {
            $results = [];
    
            foreach ($dependencies as $dependency) {
                // If this dependency has a override for this particular build we will use
                // that instead as the value. Otherwise, we will continue with this run
                // of resolutions and let reflection attempt to determine the result.
                if ($this->hasParameterOverride($dependency)) {
                    $results[] = $this->getParameterOverride($dependency);
    
                    continue;
                }
    
                // If the class is null, it means the dependency is a string or some other
                // primitive type which we can not resolve since it is not a class and
                // we will just bomb out with an error since we have no-where to go.
                $results[] = is_null($class = $dependency->getClass())
                                ? $this->resolvePrimitive($dependency)
                                : $this->resolveClass($dependency);
            }
    
            return $results;
        }
    
    

    好的。如果你不明白我会解释;

    Laravel 检查依赖关系是一个原始类还是一个类,并且基于此进行处理。

    <?php
    
        /**
         * Resolve a class based dependency from the container.
         *
         * @param  \ReflectionParameter  $parameter
         * @return mixed
         *
         * @throws \Illuminate\Contracts\Container\BindingResolutionException
         */
        protected function resolveClass(ReflectionParameter $parameter)
        {
            try {
                return $this->make($parameter->getClass()->name);
            }
    
            // If we can not resolve the class instance, we will check to see if the value
            // is optional, and if it is we will return the optional parameter value as
            // the value of the dependency, similarly to how we do this with scalars.
            catch (BindingResolutionException $e) {
                if ($parameter->isOptional()) {
                    return $parameter->getDefaultValue();
                }
    
                throw $e;
            }
        }
    

    就这样吧。看完上面的过程你应该对 Container 和依赖注入的工作原理有了更好的理解。

    感谢阅读 ^_^

    欢迎留言讨论。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3190 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 13:05 · PVG 21:05 · LAX 06:05 · JFK 09:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.