1. 容器的本質
- 服務容器本身就是一個數(shù)組,鍵名就是服務名,值就是服務。
- 服務可以是一個原始值,也可以是一個對象,可以說是任意數(shù)據(jù)。
- 服務名可以是自定義名,也可以是對象的類名,也可以是接口名。
// 服務容器
$container = [
// 原始值
'text' => '這是一個字符串',
// 自定義服務名
'customName' => new StdClass(),
// 使用類名作為服務名
'StdClass' => new StdClass(),
// 使用接口名作為服務名
'Namespace\\StdClassInterface' => new StdClass(),
];
// ----------- ↓↓↓↓示例代碼↓↓↓↓ ----------- //
// 綁定服務到容器
$container['standard'] = new StdClass();
// 獲取服務
$standard = $container['standard'];
var_dump($standard);
2. 封裝成類
為了方便維護,我們把上面的數(shù)組封裝到類里面。
$instances還是上面的容器數(shù)組。我們增加兩個方法,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. 按需實例化
現(xiàn)在我們在綁定一個對象服務的時候,就必須要先把類實例化,如果綁定的服務沒有被用到,那么類就會白白實例化,造成性能浪費。
為了解決這個問題,我們增加一個bind函數(shù),它支持綁定一個回調(diào)函數(shù),在回調(diào)函數(shù)中實例化類。這樣一來,我們只有在使用服務時,才回調(diào)這個函數(shù),這樣就實現(xiàn)了按需實例化。
這時候,我們獲取服務時,就不只是從數(shù)組中拿到服務并返回了,還需要判斷如果是回調(diào)函數(shù),就要執(zhí)行回調(diào)函數(shù)。所以我們把get方法的名字改成make。意思就是生產(chǎn)一個服務,這個服務可以是已綁定的服務,也可以是已綁定的回調(diào)函數(shù),也可以是一個類名,如果是類名,我們就直接實例化該類并返回。
然后,我們增加一個新數(shù)組$bindings,用來存儲綁定的回調(diào)函數(shù)。然后我們把bind方法改一下,判斷下$instance如果是一個回調(diào)函數(shù),就放到$bindings數(shù)組,否則就用make方法實例化類。
class DeferContainer extend BaseContainer
{
// 已綁定的回調(diào)函數(shù)
protected $bindings = [];
// 綁定服務
public function bind($name, $instance)
{
if ($instance instanceof Closure) {
// 如果$instance是一個回調(diào)函數(shù),就綁定到bindings。
$this->bindings[$name] = $instance;
} else {
// 調(diào)用make方法,創(chuàng)建實例
$this->instances[$name] = $this->make($name);
}
}
// 獲取服務
public function make($name)
{
if (isset($this->instances[$name])) {
return $this->instances[$name];
}
if (isset($this->bindings[$name])) {
// 執(zhí)行回調(diào)函數(shù)并返回
$instance = call_user_func($this->bindings[$name]);
} else {
// 還沒有綁定到容器中,直接new.
$instance = new $name();
}
return $instance;
}
}
// ----------- ↓↓↓↓示例代碼↓↓↓↓ ----------- //
$container = new DeferContainer();
// 綁定服務
$container->bind('StdClass', function () {
echo "我被執(zhí)行了\n";
return new StdClass();
});
// 獲取服務
$stdClass = $container->make('StdClass');
var_dump($stdClass);
StdClass這個服務綁定的是一個回調(diào)函數(shù),在回調(diào)函數(shù)中才會真正的實例化類。如果沒有用到這個服務,那回調(diào)函數(shù)就不會被執(zhí)行,類也不會被實例化。
4. 單例
從上面的代碼中可以看出,每次調(diào)用make方法時,都會執(zhí)行一次回調(diào)函數(shù),并返回一個新的類實例。但是在某些情況下,我們希望這個實例是一個單例,無論make多少次,只實例化一次。
這時候,我們給bind方法增加第三個參數(shù)$shared,用來標記是否是單例,默認不是單例。然后把回調(diào)函數(shù)和這個標記都存到$bindings數(shù)組里。
為了方便綁定單例服務,再增加一個新的方法singleton,它直接調(diào)用bind,并且$shared參數(shù)強制為true。
對于make方法,我們也要做修改。在執(zhí)行$bindings里的回調(diào)函數(shù)以后,做一個判斷,如果之前綁定時標記的shared是true,就把回調(diào)函數(shù)返回的結果存儲到$instances里。由于我們是先從$instances里找服務,所以這樣下次再make的時候就會直接返回,而不會再次執(zhí)行回調(diào)函數(shù)。這樣就實現(xiàn)了單例的綁定。
class SingletonContainer extends DeferContainer
{
// 綁定服務
public function bind($name, $instance, $shared = false)
{
if ($instance instanceof Closure) {
// 如果$instance是一個回調(diào)函數(shù),就綁定到bindings。
$this->bindings[$name] = [
'callback' => $instance,
// 標記是否單例
'shared' => $shared
];
} else {
// 調(diào)用make方法,創(chuàng)建實例
$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])) {
// 執(zhí)行回調(diào)函數(shù)并返回
$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的服務,回調(diào)函數(shù)里返回了一個匿名類的實例。這個匿名類在被實例化時會輸出一段文字。無論我們make多少次anonymous,這個回調(diào)函數(shù)只會被執(zhí)行一次,匿名類也只會被實例化一次。
5. 自動注入
自動注入是Ioc容器的核心,沒有自動注入就無法做到控制反轉。
自動注入就是指,在實例化一個類時,用反射類來獲取__construct所需要的參數(shù),然后根據(jù)參數(shù)的類型,從容器中找到已綁定的服務。我們只要有了__construct方法所需的所有參數(shù),就能自動實例化該類,實現(xiàn)自動注入。
現(xiàn)在,我們增加一個build方法,它只接收一個參數(shù),就是類名。build方法會用反射類來獲取__construct方法所需要的參數(shù),然后返回實例化結果。
另外一點就是,我們之前在調(diào)用make方法時,如果傳的是一個未綁定的類,我們直接new了這個類?,F(xiàn)在我們把未綁定的類交給build方法來構建,因為它支持自動注入。
class InjectionContainer extends SingletonContainer
{
// 獲取服務
public function make($name)
{
if (isset($this->instances[$name])) {
return $this->instances[$name];
}
if (isset($this->bindings[$name])) {
// 執(zhí)行回調(diào)函數(shù)并返回
$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)) {
// 沒有構造函數(shù),直接new
return new $class();
}
$dependencies = [];
// 獲取構造函數(shù)所需的參數(shù)
foreach ($constructor->getParameters() as $dependency) {
if (is_null($dependency->getClass())) {
// 參數(shù)類型不是類時,無法從容器中獲取依賴
if ($dependency->isDefaultValueAvailable()) {
// 查找參數(shù)的默認值,如果有就使用默認值
$dependencies[] = $dependency->getDefaultValue();
} else {
// 無法提供類所依賴的參數(shù)
throw new Exception('找不到依賴參數(shù):' . $dependency->getName());
}
} else {
// 參數(shù)類型是類時,就用make方法構建該類
$dependencies[] = $this->make($dependency->getClass()->name);
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
// ----------- ↓↓↓↓示例代碼↓↓↓↓ ----------- //
class Redis
{
}
class Cache
{
protected $redis;
// 構造函數(shù)中依賴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. 自定義依賴參數(shù)
現(xiàn)在有個問題,如果類依賴的參數(shù)不是類或接口,只是一個普通變量,這時候就無法從容器中獲取依賴參數(shù)了,也就無法實例化類了。
那么接下來我們就支持一個新功能,在調(diào)用make方法時,支持傳第二個參數(shù)$parameters,這是一個數(shù)組,無法從容器中獲取的依賴,就從這個數(shù)組中找。
當然,make方法是用不到這個參數(shù)的,因為它不負責實例化類,它直接傳給build方法。在build方法尋找依賴的參數(shù)時,就先從$parameters中找。這樣就實現(xiàn)了自定義依賴參數(shù)。
需要注意的一點是,build方法是按照參數(shù)的名字來找依賴的,所以parameters中的鍵名也必須跟__construct中參數(shù)名一致。
class ParametersContainer extends InjectionContainer
{
// 獲取服務
public function make($name, array $parameters = [])
{
if (isset($this->instances[$name])) {
return $this->instances[$name];
}
if (isset($this->bindings[$name])) {
// 執(zhí)行回調(diào)函數(shù)并返回
$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)) {
// 沒有構造函數(shù),直接new
return new $class();
}
$dependencies = [];
// 獲取構造函數(shù)所需的參數(shù)
foreach ($constructor->getParameters() as $dependency) {
if (isset($parameters[$dependency->getName()])) {
// 先從自定義參數(shù)中查找
$dependencies[] = $parameters[$dependency->getName()];
continue;
}
if (is_null($dependency->getClass())) {
// 參數(shù)類型不是類或接口時,無法從容器中獲取依賴
if ($dependency->isDefaultValueAvailable()) {
// 查找默認值,如果有就使用默認值
$dependencies[] = $dependency->getDefaultValue();
} else {
// 無法提供類所依賴的參數(shù)
throw new Exception('找不到依賴參數(shù):' . $dependency->getName());
}
} else {
// 參數(shù)類型是類時,就用make方法構建該類
$dependencies[] = $this->make($dependency->getClass()->name);
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
// ----------- ↓↓↓↓示例代碼↓↓↓↓ ----------- //
class Redis
{
}
class Cache
{
protected $redis;
protected $name;
protected $default;
// 構造函數(shù)中依賴Redis服務和name參數(shù),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方法并沒有第二個參數(shù)$parameters,它是用類屬性來維護自定義參數(shù)。原理都是一樣的,只是實現(xiàn)方式不一樣。這里為了方便理解,不引入過多概念。
7. 服務別名
別名可以理解成小名、外號。服務別名就是給已綁定的服務設置一些外號,使我們通過外號也能找到該服務。
這個就比較簡單了,我們增加一個新的數(shù)組$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. 擴展綁定
有時候我們需要給已綁定的服務做一個包裝,這時候就用到擴展綁定了。我們先看一個實際的用法,理解它的作用后,才看它是如何實現(xiàn)的。
// 綁定日志服務
$container->singleton('log', new Log());
// 對已綁定的服務再次包裝
$container->extend('log', function(Log $log){
// 返回了一個新服務
return new RedisLog($log);
});
現(xiàn)在我們看它是如何實現(xiàn)的。增加一個$extenders數(shù)組,用來存放擴展器。再增加一個extend方法,用來注冊擴展器。
然后在make方法返回$instance之前,按順序依次調(diào)用之前注冊的擴展器。
class ExtendContainer extends AliasContainer
{
// 存放擴展器的數(shù)組
protected $extenders = [];
// 給服務綁定擴展器
public function extend($name, $extender)
{
if (isset($this->instances[$name])) {
// 已經(jīng)實例化的服務,直接調(diào)用擴展器
$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])) {
// 調(diào)用擴展器
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. 上下文綁定
有時侯我們可能有兩個類使用同一個接口,但希望在每個類中注入不同的實現(xiàn),例如兩個控制器,分別為它們注入不同的Log服務。
class ApiController
{
public function __construct(Log $log)
{
}
}
class WebController
{
public function __construct(Log $log)
{
}
}
最終我們要用以下方式實現(xiàn):
// 當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數(shù)組,用來存儲上下文。同時增加一個addContextualBinding方法,用來注冊上下文綁定。以ApiController為例,$context的真實模樣是:
$context['ApiController']['Log'] = new RedisLog();
然后build方法實例化類時,先從上下文中查找依賴參數(shù),就實現(xiàn)了上下文綁定。
接下來,看看鏈式操作是如何實現(xiàn)的。
首先定義一個類Context,這個類有兩個方法,needs和give。
然后在容器中,增加一個when方法,它返回一個Context對象。在Context對象的give方法中,我們已經(jīng)具備了注冊上下文所需要的所有參數(shù),所以就可以在give方法中調(diào)用addContextualBinding來注冊上下文了。
class ContextContainer extends ExtendContainer
{
// 依賴上下文
protected $context = [];
// 構建一個類,并自動注入服務
public function build($class, array $parameters = [])
{
$reflector = new ReflectionClass($class);
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
// 沒有構造函數(shù),直接new
return new $class();
}
$dependencies = [];
// 獲取構造函數(shù)所需的參數(shù)
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()])) {
// 從自定義參數(shù)中查找
$dependencies[] = $parameters[$dependency->getName()];
continue;
}
if (is_null($dependency->getClass())) {
// 參數(shù)類型不是類或接口時,無法從容器中獲取依賴
if ($dependency->isDefaultValueAvailable()) {
// 查找默認值,如果有就使用默認值
$dependencies[] = $dependency->getDefaultValue();
} else {
// 無法提供類所依賴的參數(shù)
throw new Exception('找不到依賴參數(shù):' . $dependency->getName());
}
} else {
// 參數(shù)類型是一個類時,就用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)
{
// 調(diào)用容器綁定依賴上下文
$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 = [];
// 已綁定的回調(diào)函數(shù)
protected $bindings = [];
// 服務別名
protected $aliases = [];
// 存放擴展器的數(shù)組
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是一個回調(diào)函數(shù),就綁定到bindings。
$this->bindings[$name] = [
'callback' => $instance,
// 標記是否單例
'shared' => $shared
];
} else {
// 調(diào)用make方法,創(chuàng)建實例
$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])) {
// 已經(jīng)實例化的服務,直接調(diào)用擴展器
$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])) {
// 執(zhí)行回調(diào)函數(shù)并返回
$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])) {
// 調(diào)用擴展器
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)) {
// 沒有構造函數(shù),直接new
return new $class();
}
$dependencies = [];
// 獲取構造函數(shù)所需的參數(shù)
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()])) {
// 從自定義參數(shù)中查找
$dependencies[] = $parameters[$dependency->getName()];
continue;
}
if (is_null($dependency->getClass())) {
// 參數(shù)類型不是類或接口時,無法從容器中獲取依賴
if ($dependency->isDefaultValueAvailable()) {
// 查找默認值,如果有就使用默認值
$dependencies[] = $dependency->getDefaultValue();
} else {
// 無法提供類所依賴的參數(shù)
throw new Exception('找不到依賴參數(shù):' . $dependency->getName());
}
} else {
// 參數(shù)類型是一個類時,就用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)
{
// 調(diào)用容器綁定依賴上下文
$this->container->addContextualBinding($this->when, $this->needs, $give);
}
}
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- Laravel服務容器綁定的幾種方法總結
- 詳解Laravel服務容器的綁定與解析
- laravel ajax curd 搜索登錄判斷功能的實現(xiàn)
- Laravel中Kafka的使用詳解
- laravel使用redis隊列實例講解
- Laravel的加密解密與哈希實例講解
- Laravel中10個有用的用法小結
- 詳解Laravel服務容器的優(yōu)勢