详解如何实现Laravel的服务容器的方法示例

时间:2022-07-27
本文章向大家介绍详解如何实现Laravel的服务容器的方法示例,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 容器的本质

  • 服务容器本身就是一个数组,键名就是服务名,值就是服务。
  • 服务可以是一个原始值,也可以是一个对象,可以说是任意数据。
  • 服务名可以是自定义名,也可以是对象的类名,也可以是接口名。
// 服务容器
$container = [
  // 原始值
  'text' =  '这是一个字符串',
  // 自定义服务名
  'customName' =  new StdClass(),
  // 使用类名作为服务名
  'StdClass' =  new StdClass(),
  // 使用接口名作为服务名
  'NamespaceStdClassInterface' =  new StdClass(),
];

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

// 绑定服务到容器
$container['standard'] = new StdClass();
// 获取服务
$standard = $container['standard'];
var_dump($standard);

2. 封装成类

为了方便维护,我们把上面的数组封装到类里面。

$instances还是上面的容器数组。我们增加两个方法,instance用来绑定服务,get用来从容器中获取服务。

class BaseContainer
{

  // 已绑定的服务
  protected $instances = [];

  // 绑定服务
  public function instance($name, $instance)
  {
    $this- instances[$name] = $instance;
  }

  // 获取服务
  public function get($name)
  {
    return isset($this- instances[$name]) ? $this- instances[$name] : null;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new BaseContainer();
// 绑定服务
$container- instance('StdClass', new StdClass());
// 获取服务
$stdClass = $container- get('StdClass');
var_dump($stdClass);

3. 按需实例化

现在我们在绑定一个对象服务的时候,就必须要先把类实例化,如果绑定的服务没有被用到,那么类就会白白实例化,造成性能浪费。

为了解决这个问题,我们增加一个bind函数,它支持绑定一个回调函数,在回调函数中实例化类。这样一来,我们只有在使用服务时,才回调这个函数,这样就实现了按需实例化。

这时候,我们获取服务时,就不只是从数组中拿到服务并返回了,还需要判断如果是回调函数,就要执行回调函数。所以我们把get方法的名字改成make。意思就是生产一个服务,这个服务可以是已绑定的服务,也可以是已绑定的回调函数,也可以是一个类名,如果是类名,我们就直接实例化该类并返回。

然后,我们增加一个新数组bindings,用来存储绑定的回调函数。然后我们把bind方法改一下,判断下instance如果是一个回调函数,就放到

class DeferContainer extend BaseContainer
{
  // 已绑定的回调函数
  protected $bindings = [];

  // 绑定服务
  public function bind($name, $instance)
  {
    if ($instance instanceof Closure) {
      // 如果$instance是一个回调函数,就绑定到bindings。
      $this- bindings[$name] = $instance;
    } else {
      // 调用make方法,创建实例
      $this- instances[$name] = $this- make($name);
    }
  }

  // 获取服务
  public function make($name)
  {
    if (isset($this- instances[$name])) {
      return $this- instances[$name];
    }

    if (isset($this- bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this- bindings[$name]);
    } else {
      // 还没有绑定到容器中,直接new.
      $instance = new $name();
    }

    return $instance;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new DeferContainer();
// 绑定服务
$container- bind('StdClass', function () {
  echo "我被执行了n";
  return new StdClass();
});
// 获取服务
$stdClass = $container- make('StdClass');
var_dump($stdClass);

StdClass这个服务绑定的是一个回调函数,在回调函数中才会真正的实例化类。如果没有用到这个服务,那回调函数就不会被执行,类也不会被实例化。

4. 单例

从上面的代码中可以看出,每次调用make方法时,都会执行一次回调函数,并返回一个新的类实例。但是在某些情况下,我们希望这个实例是一个单例,无论make多少次,只实例化一次。

这时候,我们给bind方法增加第三个参数shared,用来标记是否是单例,默认不是单例。然后把回调函数和这个标记都存到bindings数组里。

为了方便绑定单例服务,再增加一个新的方法singleton,它直接调用bind,并且$shared参数强制为true。

对于make方法,我们也要做修改。在执行bindings里的回调函数以后,做一个判断,如果之前绑定时标记的shared是true,就把回调函数返回的结果存储到instances里。由于我们是先从

class SingletonContainer extends DeferContainer
{
  // 绑定服务
  public function bind($name, $instance, $shared = false)
  {
    if ($instance instanceof Closure) {
      // 如果$instance是一个回调函数,就绑定到bindings。
      $this- bindings[$name] = [
        'callback' =  $instance,
        // 标记是否单例
        'shared' =  $shared
      ];
    } else {
      // 调用make方法,创建实例
      $this- instances[$name] = $this- make($name);
    }
  }

  // 绑定一个单例
  public function singleton($name, $instance)
  {
    $this- bind($name, $instance, true);
  }

  // 获取服务
  public function make($name)
  {
    if (isset($this- instances[$name])) {
      return $this- instances[$name];
    }

    if (isset($this- bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this- bindings[$name]['callback']);

      if ($this- bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this- instances[$name] = $instance;
      }
    } else {
      // 还没有绑定到容器中,直接new.
      $instance = new $name();
    }

    return $instance;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new SingletonContainer();
// 绑定服务
$container- singleton('anonymous', function () {
  return new class
  {
    public function __construct()
    {
      echo "我被实例化了n";
    }
  };
});
// 无论make多少次,只会实例化一次
$container- make('anonymous');
$container- make('anonymous');
// 获取服务
$anonymous = $container- make('anonymous');
var_dump($anonymous)

上面的代码用singleton绑定了一个名为anonymous的服务,回调函数里返回了一个匿名类的实例。这个匿名类在被实例化时会输出一段文字。无论我们make多少次anonymous,这个回调函数只会被执行一次,匿名类也只会被实例化一次。

5. 自动注入

自动注入是Ioc容器的核心,没有自动注入就无法做到控制反转。

自动注入就是指,在实例化一个类时,用反射类来获取__construct所需要的参数,然后根据参数的类型,从容器中找到已绑定的服务。我们只要有了__construct方法所需的所有参数,就能自动实例化该类,实现自动注入。

现在,我们增加一个build方法,它只接收一个参数,就是类名。build方法会用反射类来获取__construct方法所需要的参数,然后返回实例化结果。

另外一点就是,我们之前在调用make方法时,如果传的是一个未绑定的类,我们直接new了这个类。现在我们把未绑定的类交给build方法来构建,因为它支持自动注入。

class InjectionContainer extends SingletonContainer
{

  // 获取服务
  public function make($name)
  {
    if (isset($this- instances[$name])) {
      return $this- instances[$name];
    }
    if (isset($this- bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this- bindings[$name]['callback']);

      if ($this- bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this- instances[$name] = $instance;
      }
    } else {
      // 使用build方法构建此类
      $instance = $this- build($name);
    }

    return $instance;
  }

  // 构建一个类,并自动注入服务
  public function build($class)
  {

    $reflector = new ReflectionClass($class);

    $constructor = $reflector- getConstructor();

    if (is_null($constructor)) {
      // 没有构造函数,直接new
      return new $class();
    }

    $dependencies = [];

    // 获取构造函数所需的参数
    foreach ($constructor- getParameters() as $dependency) {
      if (is_null($dependency- getClass())) {
        // 参数类型不是类时,无法从容器中获取依赖
        if ($dependency- isDefaultValueAvailable()) {
          // 查找参数的默认值,如果有就使用默认值
          $dependencies[] = $dependency- getDefaultValue();
        } else {
          // 无法提供类所依赖的参数
          throw new Exception('找不到依赖参数:' . $dependency- getName());
        }
      } else {
        // 参数类型是类时,就用make方法构建该类
        $dependencies[] = $this- make($dependency- getClass()- name);
      }
    }

    return $reflector- newInstanceArgs($dependencies);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Redis
{
}

class Cache
{
  protected $redis;

  // 构造函数中依赖Redis服务
  public function __construct(Redis $redis)
  {
    $this- redis = $redis;
  }
}

$container = new InjectionContainer();
// 绑定Redis服务
$container- singleton(Redis::class, function () {
  return new Redis();
});
// 构建Cache类
$cache = $container- make(Cache::class);
var_dump($cache);

6. 自定义依赖参数

现在有个问题,如果类依赖的参数不是类或接口,只是一个普通变量,这时候就无法从容器中获取依赖参数了,也就无法实例化类了。

那么接下来我们就支持一个新功能,在调用make方法时,支持传第二个参数$parameters,这是一个数组,无法从容器中获取的依赖,就从这个数组中找。

当然,make方法是用不到这个参数的,因为它不负责实例化类,它直接传给build方法。在build方法寻找依赖的参数时,就先从$parameters中找。这样就实现了自定义依赖参数。

需要注意的一点是,build方法是按照参数的名字来找依赖的,所以parameters中的键名也必须跟__construct中参数名一致。

class ParametersContainer extends InjectionContainer
{
  // 获取服务
  public function make($name, array $parameters = [])
  {
    if (isset($this- instances[$name])) {
      return $this- instances[$name];
    }
    if (isset($this- bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this- bindings[$name]['callback']);

      if ($this- bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this- instances[$name] = $instance;
      }
    } else {
      // 使用build方法构建此类
      $instance = $this- build($name, $parameters);
    }

    return $instance;
  }

  // 构建一个类,并自动注入服务
  public function build($class, array $parameters = [])
  {
    $reflector = new ReflectionClass($class);

    $constructor = $reflector- getConstructor();

    if (is_null($constructor)) {
      // 没有构造函数,直接new
      return new $class();
    }

    $dependencies = [];

    // 获取构造函数所需的参数
    foreach ($constructor- getParameters() as $dependency) {

      if (isset($parameters[$dependency- getName()])) {
        // 先从自定义参数中查找
        $dependencies[] = $parameters[$dependency- getName()];
        continue;
      }

      if (is_null($dependency- getClass())) {
        // 参数类型不是类或接口时,无法从容器中获取依赖
        if ($dependency- isDefaultValueAvailable()) {
          // 查找默认值,如果有就使用默认值
          $dependencies[] = $dependency- getDefaultValue();
        } else {
          // 无法提供类所依赖的参数
          throw new Exception('找不到依赖参数:' . $dependency- getName());
        }
      } else {
        // 参数类型是类时,就用make方法构建该类
        $dependencies[] = $this- make($dependency- getClass()- name);
      }
    }

    return $reflector- newInstanceArgs($dependencies);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Redis
{
}

class Cache
{
  protected $redis;

  protected $name;

  protected $default;

  // 构造函数中依赖Redis服务和name参数,name的类型不是类,无法从容器中查找
  public function __construct(Redis $redis, $name, $default = '默认值')
  {
    $this- redis = $redis;
    $this- name = $name;
    $this- default = $default;
  }
}

$container = new ParametersContainer();
// 绑定Redis服务
$container- singleton(Redis::class, function () {
  return new Redis();
});
// 构建Cache类
$cache = $container- make(Cache::class, ['name' =  'test']);
var_dump($cache);

提示:实际上,Laravel容器的build方法并没有第二个参数$parameters,它是用类属性来维护自定义参数。原理都是一样的,只是实现方式不一样。这里为了方便理解,不引入过多概念。

7. 服务别名

别名可以理解成小名、外号。服务别名就是给已绑定的服务设置一些外号,使我们通过外号也能找到该服务。

这个就比较简单了,我们增加一个新的数组$aliases,用来存储别名。再增加一个方法alias,用来让外部注册别名。

唯一需要我们修改的地方,就是在make时,要先从$aliases中找到真实的服务名。

class AliasContainer extends ParametersContainer
{
  // 服务别名
  protected $aliases = [];

  // 给服务绑定一个别名
  public function alias($alias, $name)
  {
    $this- aliases[$alias] = $name;
  }

  // 获取服务
  public function make($name, array $parameters = [])
  {
    // 先用别名查找真实服务名
    $name = isset($this- aliases[$name]) ? $this- aliases[$name] : $name;

    return parent::make($name, $parameters);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new AliasContainer();

// 绑定服务
$container- instance('text', '这是一个字符串');
// 给服务注册别名
$container- alias('string', 'text');
$container- alias('content', 'text');

var_dump($container- make('string'));
var_dump($container- make('content'));

8. 扩展绑定

有时候我们需要给已绑定的服务做一个包装,这时候就用到扩展绑定了。我们先看一个实际的用法,理解它的作用后,才看它是如何实现的。

// 绑定日志服务
$container- singleton('log', new Log());

// 对已绑定的服务再次包装
$container- extend('log', function(Log $log){
  // 返回了一个新服务
  return new RedisLog($log);
});

现在我们看它是如何实现的。增加一个$extenders数组,用来存放扩展器。再增加一个extend方法,用来注册扩展器。

然后在make方法返回$instance之前,按顺序依次调用之前注册的扩展器。

class ExtendContainer extends AliasContainer
{
  // 存放扩展器的数组
  protected $extenders = [];

  // 给服务绑定扩展器
  public function extend($name, $extender)
  {
    if (isset($this- instances[$name])) {
      // 已经实例化的服务,直接调用扩展器
      $this- instances[$name] = $extender($this- instances[$name]);
    } else {
      $this- extenders[$name][] = $extender;
    }
  }

  // 获取服务
  public function make($name, array $parameters = [])
  {
    $instance = parent::make($name, $parameters);

    if (isset($this- extenders[$name])) {
      // 调用扩展器
      foreach ($this- extenders[$name] as $extender) {
        $instance = $extender($instance);
      }
    }

    return $instance;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Redis
{
  public $name;

  public function __construct($name = 'default')
  {
    $this- name = $name;
  }

  public function setName($name)
  {
    $this- name = $name;
  }
}

$container = new ExtendContainer();

// 绑定Redis服务
$container- singleton(Redis::class, function () {
  return new Redis();
});

// 给Redis服务绑定一个扩展器
$container- extend(Redis::class, function (Redis $redis) {
  $redis- setName('扩展器');
  return $redis;
});
$redis = $container- make(Redis::class);
var_dump($redis- name);

9. 上下文绑定

有时侯我们可能有两个类使用同一个接口,但希望在每个类中注入不同的实现,例如两个控制器,分别为它们注入不同的Log服务。

class ApiController
{
  public function __construct(Log $log)
  {
  }
}

class WebController
{
  public function __construct(Log $log)
  {
  }
}

最终我们要用以下方式实现:

// 当ApiController依赖Log时,给它一个RedisLog
$container- addContextualBinding('ApiController','Log',new RedisLog());

// 当WebController依赖Log时,给它一个FileLog
$container- addContextualBinding('WebController','Log',new FileLog());

为了更直观更方便更语义化的使用,我们把这个过程改成链式操作:

$container- when('ApiController')
    - needs('Log')
    - give(new RedisLog());

我们增加一个context数组,用来存储上下文。同时增加一个addContextualBinding方法,用来注册上下文绑定。以ApiController为例,context的真实模样是:

$context['ApiController']['Log'] = new RedisLog();

然后build方法实例化类时,先从上下文中查找依赖参数,就实现了上下文绑定。

接下来,看看链式操作是如何实现的。

首先定义一个类Context,这个类有两个方法,needs和give。

然后在容器中,增加一个when方法,它返回一个Context对象。在Context对象的give方法中,我们已经具备了注册上下文所需要的所有参数,所以就可以在give方法中调用addContextualBinding来注册上下文了。

class ContextContainer extends ExtendContainer
{
// 依赖上下文
protected $context = [];
// 构建一个类,并自动注入服务
public function build($class, array $parameters = [])
{
$reflector = new ReflectionClass($class);
$constructor = $reflector- getConstructor();
if (is_null($constructor)) {
// 没有构造函数,直接new
return new $class();
}
$dependencies = [];
// 获取构造函数所需的参数
foreach ($constructor- getParameters() as $dependency) {
if (isset($this- context[$class]) && isset($this- context[$class][$dependency- getName()])) {
// 先从上下文中查找
$dependencies[] = $this- context[$class][$dependency- getName()];
continue;
}
if (isset($parameters[$dependency- getName()])) {
// 从自定义参数中查找
$dependencies[] = $parameters[$dependency- getName()];
continue;
}
if (is_null($dependency- getClass())) {
// 参数类型不是类或接口时,无法从容器中获取依赖
if ($dependency- isDefaultValueAvailable()) {
// 查找默认值,如果有就使用默认值
$dependencies[] = $dependency- getDefaultValue();
} else {
// 无法提供类所依赖的参数
throw new Exception('找不到依赖参数:' . $dependency- getName());
}
} else {
// 参数类型是一个类时,就用make方法构建该类
$dependencies[] = $this- make($dependency- getClass()- name);
}
}
return $reflector- newInstanceArgs($dependencies);
}
// 绑定上下文
public function addContextualBinding($when, $needs, $give)
{
$this- context[$when][$needs] = $give;
}
// 支持链式方式绑定上下文
public function when($when)
{
return new Context($when, $this);
}
}
class Context
{
protected $when;
protected $needs;
protected $container;
public function __construct($when, ContextContainer $container)
{
$this- when = $when;
$this- container = $container;
}
public function needs($needs)
{
$this- needs = $needs;
return $this;
}
public function give($give)
{
// 调用容器绑定依赖上下文
$this- container- addContextualBinding($this- when, $this- needs, $give);
}
}
// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //
class Dog
{
public $name;
public function __construct($name)
{
$this- name = $name;
}
}
class Cat
{
public $name;
public function __construct($name)
{
$this- name = $name;
}
}
$container = new ContextContainer();
// 给Dog类设置上下文绑定
$container- when(Dog::class)
- needs('name')
- give('小狗');
// 给Cat类设置上下文绑定
$container- when(Cat::class)
- needs('name')
- give('小猫');
$dog = $container- make(Dog::class);
$cat = $container- make(Cat::class);
var_dump('Dog:' . $dog- name);
var_dump('Cat:' . $cat- name);

10. 完整代码

class Container
{
// 已绑定的服务
protected $instances = [];
// 已绑定的回调函数
protected $bindings = [];
// 服务别名
protected $aliases = [];
// 存放扩展器的数组
protected $extenders = [];
// 依赖上下文
protected $context = [];
// 绑定服务实例
public function instance($name, $instance)
{
$this- instances[$name] = $instance;
}
// 绑定服务
public function bind($name, $instance, $shared = false)
{
if ($instance instanceof Closure) {
// 如果$instance是一个回调函数,就绑定到bindings。
$this- bindings[$name] = [
'callback' =  $instance,
// 标记是否单例
'shared' =  $shared
];
} else {
// 调用make方法,创建实例
$this- instances[$name] = $this- make($name);
}
}
// 绑定一个单例
public function singleton($name, $instance)
{
$this- bind($name, $instance, true);
}
// 给服务绑定一个别名
public function alias($alias, $name)
{
$this- aliases[$alias] = $name;
}
// 给服务绑定扩展器
public function extend($name, $extender)
{
if (isset($this- instances[$name])) {
// 已经实例化的服务,直接调用扩展器
$this- instances[$name] = $extender($this- instances[$name]);
} else {
$this- extenders[$name][] = $extender;
}
}
// 获取服务
public function make($name, array $parameters = [])
{
// 先用别名查找真实服务名
$name = isset($this- aliases[$name]) ? $this- aliases[$name] : $name;
if (isset($this- instances[$name])) {
return $this- instances[$name];
}
if (isset($this- bindings[$name])) {
// 执行回调函数并返回
$instance = call_user_func($this- bindings[$name]['callback']);
if ($this- bindings[$name]['shared']) {
// 标记为单例时,存储到服务中
$this- instances[$name] = $instance;
}
} else {
// 使用build方法构建此类
$instance = $this- build($name, $parameters);
}
if (isset($this- extenders[$name])) {
// 调用扩展器
foreach ($this- extenders[$name] as $extender) {
$instance = $extender($instance);
}
}
return $instance;
}
// 构建一个类,并自动注入服务
public function build($class, array $parameters = [])
{
$reflector = new ReflectionClass($class);
$constructor = $reflector- getConstructor();
if (is_null($constructor)) {
// 没有构造函数,直接new
return new $class();
}
$dependencies = [];
// 获取构造函数所需的参数
foreach ($constructor- getParameters() as $dependency) {
if (isset($this- context[$class]) && isset($this- context[$class][$dependency- getName()])) {
// 先从上下文中查找
$dependencies[] = $this- context[$class][$dependency- getName()];
continue;
}
if (isset($parameters[$dependency- getName()])) {
// 从自定义参数中查找
$dependencies[] = $parameters[$dependency- getName()];
continue;
}
if (is_null($dependency- getClass())) {
// 参数类型不是类或接口时,无法从容器中获取依赖
if ($dependency- isDefaultValueAvailable()) {
// 查找默认值,如果有就使用默认值
$dependencies[] = $dependency- getDefaultValue();
} else {
// 无法提供类所依赖的参数
throw new Exception('找不到依赖参数:' . $dependency- getName());
}
} else {
// 参数类型是一个类时,就用make方法构建该类
$dependencies[] = $this- make($dependency- getClass()- name);
}
}
return $reflector- newInstanceArgs($dependencies);
}
// 绑定上下文
public function addContextualBinding($when, $needs, $give)
{
$this- context[$when][$needs] = $give;
}
// 支持链式方式绑定上下文
public function when($when)
{
return new Context($when, $this);
}
}
class Context
{
protected $when;
protected $needs;
protected $container;
public function __construct($when, Container $container)
{
$this- when = $when;
$this- container = $container;
}
public function needs($needs)
{
$this- needs = $needs;
return $this;
}
public function give($give)
{
// 调用容器绑定依赖上下文
$this- container- addContextualBinding($this- when, $this- needs, $give);
}
}

以上就是本文的全部内容,希望对大家的学习有所帮助。