创建自己的 PHP 服务容器 - 最小容器
本文将介绍 PHP 中是如何创建自己服务容器,用于依赖注入。我将从最简单的 PSR-11 容器,并逐步添加各种特性直至我们有一个强大、通用的容器。
"服务容器(service container)" 是什么?
服务容器是一个 PHP 对象,用于负责其他对象的实例化。我们告诉容器如何初始化对象,然后在项目需要它的实例时,再去请求。
PSR-11 是什么?
PSR-11 是一个指定服务容器通用接口的文档。它同时定义了两个服务容器必须抛出的 Exception 接口。
该接口只定义了两个方法:
interface ContainerInterface
{
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
*
* @throws NotFoundExceptionInterface No entry was found for **this** identifier.
* @throws ContainerExceptionInterface Error while retrieving the entry.
*
* @return mixed Entry.
*/
public function get(string $id);
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
*
* @param string $id Identifier of the entry to look for.
*
* @return bool
*/
public function has(string $id): bool;
}
接口的故意设计得很简单。只要求容器有这两个方法,它并没有规定服务要如何绑定到容器。
创建符合 PSR-11 得最小容器
首先安装 psr/container 包,它提供了 ContainerInterface 接口。然后创建实现该接口的 Container 类。
composer require psr/container
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
public function get(string $id): mixed
{
// ?
}
public function has(string $id): bool
{
// ?
}
}
我们希望 ”绑定bind" 一些服务到容器,bind() 是一个合适的方法名。该方法接收一个 string $id 以及一个(暂时的) object $service。
我们可以将绑定存在一个数组中。
class Container implements ContainerInterface
{
protected array $bindings = [];
public function bind(string $id, object $service): void
{
$this->bindings[$id] = $service;
}
}
现在 get() 和 has() 方法可以从 $bindings 读取:
class Container implements ContainerInterface
{
// ...
public function get(string $id): mixed
{
return $this->bindings[$id];
}
public function has(string $id): bool
{
return isset($this->bindings[$id]);
}
}
ContainerInterface 包含一些带有 @throws 注解的文档注释块。对于 Container 类,要真正符合 PSR-11,我们需要确保使用不存在的 $id 调用 get() 时,抛出实现 NotFoundExceptionInterface 接口的 Exception。
use Psr\Container\NotFoundExceptionInterface;
use Exception;
class ServiceNotFoundException extends Exception implements NotFoundExceptionInterface
{
// ...
}
class Container implements ContainerInterface
{
// ...
public function get(string $id): mixed
{
if (! $this->has($id)) {
throw new ServiceNotFoundException($id);
}
return $this->bindings[$id];
}
}
PSR-11 同时也定义了一个 ContainerExceptionInterface 接口,声明容器直接抛出的异常应该实现它。NotFoundExceptionInterface 接口已经对该接口进行了扩展,所以不需要再多做其他事。
我们可以编写一些 Pest 测试该 Container 类。
it('can be constructed', function () {
expect(new Container)->toBeInstanceOf(Container::class);
});
it('can bind and retrieve services', function () {
$container = new Container;
$container->bind('container', $container);
expect($container->has('container'))->toBeTrue();
expect($container->get('container'))->toBeInstanceOf(Container::class)->toBe($container);
});
it('throws a ServiceNotFoundException when trying to retrieve a non-existent service', function () {
$container = new Container;
expect(fn () => $container->get('foo'))
->toThrow(ServiceNotFoundException::class);
});
就这样,我们有了一个符合 PSR-11 的服务容器。