Пакет · di

Привязка интерфейсов и фабрик

Автовайринг сам разрешает конкретные классы, но не может угадать, какую реализацию должен получить интерфейс, или как построить объект, которому нужна runtime-конфигурация. ServiceProvider — место для таких привязок, собранных вместе, а не разбросанных по bootstrap.

Создайте провайдер

Унаследуйтесь от ServiceProvider и поместите привязки в register(). Контейнер передаётся внутрь.

src/AppServiceProvider.php
<?php

use Flytachi\Winter\DI\Contract\ServiceProvider;
use Flytachi\Winter\DI\Container;

class AppServiceProvider extends ServiceProvider
{
  public function register(Container $c): void
  {
      // интерфейс → реализация
      $c->singleton(CacheInterface::class, RedisCache::class);

      // фабрика-замыкание — получает контейнер
      $c->bind(MailerInterface::class, fn(Container $c) =>
          new SmtpMailer(
              host: env('MAIL_HOST', 'localhost'),
              logger: $c->make(LoggerInterface::class),
          )
      );

      // именованное скалярное значение
      $c->set('config.timeout', (int) env('APP_TIMEOUT', 30));
  }
}

Зарегистрируйте его один раз при bootstrap. Провайдеры выполняются сразу, в порядке регистрации:

php
Container::init()
  ->register(AppServiceProvider::class)
  ->register(DatabaseServiceProvider::class);

Привяжите интерфейс к реализации

Самая частая привязка: когда класс просит CacheInterface, дайте ему RedisCache.

php
$c->singleton(CacheInterface::class, RedisCache::class);

// Теперь любой автовайренный CacheInterface разрешается в общий RedisCache
class PageRenderer
{
  public function __construct(private CacheInterface $cache) {}
}

Выбирайте метод привязки по нужному времени жизни — scope зашит в вызов:

Метод Scope Для чего
singleton() один общий экземпляр stateless-сервисы, соединения, репозитории
bind() / transient() новый каждый раз объекты с состоянием, билдеры
request() один на запрос / корутину пер-запросное состояние

Полные сигнатуры — в Справочнике API; руководство по временам жизни — в Scope’ах.

Зарегистрируйте фабрику-замыкание

Когда для конструирования нужна runtime-конфигурация или своя логика, привяжите замыкание. Оно получает контейнер, чтобы разрешить другие зависимости.

php
$c->singleton(Connection::class, fn(Container $c) =>
  new PdoConnection(env('DB_DSN'), env('DB_USER'), env('DB_PASS'))
);

$c->bind(ReportBuilder::class, fn(Container $c) =>
  new ReportBuilder($c->make(Connection::class), locale: 'en_US')
);

Сохраните именованное значение

Скаляры и заранее построенные экземпляры кладутся под строковый ключ через set(), затем внедряются по имени через #[Inject('key')].

php
$c->set('config.timeout', 30);
$c->set('db.connection', $existingPdoInstance);

class ApiClient
{
  public function __construct(
      #[Inject('config.timeout')] private int $timeout,
  ) {}
}

Разбивайте провайдеры по доменам

Группируйте привязки по назначению — один провайдер на подсистему держит bootstrap читаемым.

php
class DatabaseServiceProvider extends ServiceProvider
{
  public function register(Container $c): void
  {
      $c->singleton(Connection::class, fn() =>
          new PdoConnection(env('DB_DSN'), env('DB_USER'), env('DB_PASS'))
      );
      $c->singleton(UserRepository::class);
      $c->singleton(OrderRepository::class);
  }
}

class AuthServiceProvider extends ServiceProvider
{
  public function register(Container $c): void
  {
      $c->request(AuthContext::class);
      $c->singleton(TokenValidator::class, JwtTokenValidator::class);
      $c->set('auth.secret', env('JWT_SECRET'));
  }
}

Провайдеры или атрибуты

Используйте scope-атрибут (#[Singleton] …), когда конкретный класс владеет своим временем жизни. Используйте провайдер для сопоставлений интерфейс → реализация, фабрик с runtime-конфигурацией и именованных значений — того, что конкретный класс не может объявить о себе сам.

Связанное