从零开始理解 Laravel 依赖注入

Go qloog · 2018年05月30日 · 2739 次阅读


大家在使用 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和依赖注入的工作原理有了更好的理解。

感谢阅读 ^_^

欢迎留言讨论。