原文地址: 从零开始理解 Laravel 依赖注入
大家在使用 Laravel 的过程中,可能会感觉到在 Laravel 里很多神奇的东西会发生。依赖注入似乎是一个。但它真的很神奇吗? 下面我们来具体看下。
Laravel 中的服务容器其实就是一个依赖注入容器和应用程序的注册表。
Laravel Container
是用于管理依赖项和存储对象的强大工具,可用于各种目的; 可以存储对象并在 Facades 中使用它们。
Laravel 通常使用依赖注入。即使访问 Request
我们也可以使用注入,比如。
public function __construct(Request $request)
当尝试向类中注入对象时,Container 使用 Reflection API
检查构造函数方法并检索依赖项的内容。
首先,反射 API 从深度维度中获取能量(抽象理解就好了),因此在使用反射 API 时必须小心。使用它,但不要滥用它。当检查许多对象时,反射是昂贵的,它有可能扰乱整个宇宙(有点夸张哈)。
反射通常被定义为程序能力,主要是指检查自身并在执行时修改其逻辑。
可以从官网查看 PHP.net 具体描述。
从 PHP5 开始 PHP 带有一个完整的反射 API,增加了对类,接口,函数,方法和扩展进行逆向工程的能力。另外,反射 API 提供了检索函数,类和方法的 doc 注释的方法。 发射在 PHP 中很流行。事实上,有几种情况即使不知道它也可以使用它。一些 PHP 的内置函数间接使用了反射 API--其中一个是
call_user_func
函数。
我们可能经常使用 get_class
和 get_class_method
来理解别人的代码。
下面我们来快速看看如何使用 Reflection API
来处理函数。
可以使用 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 上查看。
<?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
}
我们必须知道类名来解决依赖关系,现在让我们停下来一分钟,弄清楚这个过程:。
以下是不使用 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 是如何管理这一进展。让我们弄清楚。
<?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 和依赖注入的工作原理有了更好的理解。
感谢阅读 ^_^
欢迎留言讨论。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.