Создайте сервис контейнер на PHP — минимальный контейнер

Источник: «Build Your Own Service Container in PHP - Minimal Container»
В этой мини статье вы узнаете, как создать сервис контейнер для внедрения зависимостей в PHP. Я начну с самого простого контейнера, совместимого с PSR-11, а затем добавлю различные функции, пока у нас не получиться мощный контейнер общего назначения.

Что такое сервис контейнер

Сервис контейнер — это 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(). Этот метод должен принимать 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 содержит пару комментариев DocBlock с аннотациями @throws. Чтобы класс Container был действительно совместим с PSR-11, нужно убедиться, что выбрасывается Exception, реализующее интерфейс NotFoundExceptionInterface, при попытке вызвать get() с несуществующим $id.

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, заявляя, что Exception, создаваемые непосредственно контейнером, должны его реализовать. 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.

Если вы хотите посмотреть весь код, то можете найти его на GitHub ryangjchandler/container.

Дополнительные материалы

Предыдущая Статья

7 причин не использовать генераторы статических сайтов

Следующая Статья

10 лучших SEO рекомендаций для веб-разработчиков