编程

创建自己的 PHP 服务容器 - 最小容器

740 2023-08-14 18:54:00

本文将介绍 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 的服务容器。

 

PHP