1. 容器的本质
服务容器本身就是一个数组,键名就是服务名,值就是服务。
服务可以是一个原始值,也可以是一个对象,可以说是任意数据。
服务名可以是自定义名,也可以是对象的类名,也可以是接口名。
$container = [
'text' => '这是一个字符串',
'customName' => new StdClass(),
'StdClass' => new StdClass(),
'Namespace\\StdClassInterface' => 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如果是一个回调函数,就放到$bindings数组,否则就用make方法实例化类。
class DeferContainer extend BaseContainer
{
protected $bindings = [];
public function bind($name, $instance)
{
if ($instance instanceof Closure) {
$this->bindings[$name] = $instance;
} else {
$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 {
$instance = new $name();
}
return $instance;
}
}
$container = new DeferContainer();
$container->bind('StdClass', function () {
echo "我被执行了\n";
return new StdClass();
});
$stdClass = $container->make('StdClass');
var_dump($stdClass);
4. 单例
从上面的代码中可以看出,每次调用make方法时,都会执行一次回调函数,并返回一个新的类实例。但是在某些情况下,我们希望这个实例是一个单例,无论make多少次,只实例化一次。这时候,我们给bind方法增加第三个参数$shared,用来标记是否是单例,默认不是单例。然后把回调函数和这个标记都存到$bindings数组里。为了方便绑定单例服务,再增加一个新的方法singleton,它直接调用bind,并且$shared参数强制为true。对于make方法,我们也要做修改。在执行$bindings里的回调函数以后,做一个判断,如果之前绑定时标记的shared是true,就把回调函数返回的结果存储到$instances里。由于我们是先从$instances里找服务,所以这样下次再make的时候就会直接返回,而不会再次执行回调函数。这样就实现了单例的绑定。
class SingletonContainer extends DeferContainer
{
public function bind($name, $instance, $shared = false)
{
if ($instance instanceof Closure) {
$this->bindings[$name] = [
'callback' => $instance,
'shared' => $shared
];
} else {
$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 {
$instance = new $name();
}
return $instance;
}
}
$container = new SingletonContainer();
$container->singleton('anonymous', function () {
return new class
{
public function __construct()
{
echo "我被实例化了\n";
}
};
});
$container->make('anonymous');
$container->make('anonymous');
$anonymous = $container->make('anonymous');
var_dump($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 {
$instance = $this->build($name);
}
return $instance;
}
public function build($class)
{
$reflector = new ReflectionClass($class);
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
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 {
$dependencies[] = $this->make($dependency->getClass()->name);
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
class Redis
{
}
class Cache
{
protected $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
}
$container = new InjectionContainer();
$container->singleton(Redis::class, function () {
return new Redis();
});
$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 {
$instance = $this->build($name, $parameters);
}
return $instance;
}
public function build($class, array $parameters = [])
{
$reflector = new ReflectionClass($class);
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
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 {
$dependencies[] = $this->make($dependency->getClass()->name);
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
class Redis
{
}
class Cache
{
protected $redis;
protected $name;
protected $default;
public function __construct(Redis $redis, $name, $default = '默认值')
{
$this->redis = $redis;
$this->name = $name;
$this->default = $default;
}
}
$container = new ParametersContainer();
$container->singleton(Redis::class, function () {
return new Redis();
});
$cache = $container->make(Cache::class, ['name' => 'test']);
var_dump($cache);
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();
$container->singleton(Redis::class, function () {
return new 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)
{
}
}
$container->addContextualBinding('ApiController','Log',new RedisLog());
$container->addContextualBinding('WebController','Log',new FileLog());
$container->when('ApiController')
->needs('Log')
->give(new RedisLog());
$context['ApiController']['Log'] = new RedisLog();
首先定义一个类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)) {
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 {
$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();
$container->when(Dog::class)
->needs('name')
->give('小狗');
$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) {
$this->bindings[$name] = [
'callback' => $instance,
'shared' => $shared
];
} else {
$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 {
$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)) {
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 {
$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);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!