Kaspi/di-container — это легковесный контейнер внедрения зависимостей для PHP >= 8.0 с автоматическим связыванием.
composer require kaspi/di-container
// определение контейнера с настройкой "zero configuration for dependency inject"
// когда ненужно объявлять зависимость если класс существуют
// и может быть запрошен по "PSR-4 auto loading"
$container = (new \Kaspi\DiContainer\DiContainerFactory())->make();
// определение класса
namespace App\Controllers\Post;
use App\Services\Mail;
use App\Models\Post;
class Post {
public function __construct(private Mail $mail, private Post $post){}
public function send(): bool {
$this->mail->subject('Publication success')->body('Post <'.$post->title.'> was published.');
}
}
// получить класс Post с внедренными сервисами Mail, Post и выполнить метод "send"
$post = $container->get(App\Controllers\Post::class);
$post->send();
Фактически DiContainer
выполнит следующие действия:
$post = new App\Controllers\Post(
new App\Services\Mail(),
new App\Models\Post()
);
Примеры использования пакета kaspi/di-container в репозитории 🦄
Для конфигурирования параметров используется класс:
Kaspi\DiContainer\DiContainerConfig::class
который имплементирует интерфейс Kaspi\DiContainer\Interfaces\DiContainerConfigInterface
$diConfig = new \Kaspi\DiContainer\DiContainerConfig(
// Использовать автоматическое разрешение аргументов
// сервисов-классов или методов-классов или функций.
useAutowire: true,
// Ненужно объявлять каждую зависимость.
// Если класс или функция или интерфейс существуют -
// то он может быть запрошен по "PSR-4 autoloading".
useZeroConfigurationDefinition: true,
// Использовать Php-атрибуты для объявления зависимостей.
useAttribute: true,
// Сервис (объект) будет создаваться заново при разрешении зависимости
// если знание true, то объект будет создан как Singleton.
isSingletonServiceDefault: false,
// Строка (символ) определяющий шаблон как ссылку другой контейнер
referenceContainerSymbol: '@',
);
// передать настройки в контейнер
$container = new \Kaspi\DiContainer\DiContainer(config: $diConfig);
Или использовать фабрику с настроенными по умолчанию параметрами:
$container = (new \Kaspi\DiContainer\DiContainerFactory())->make(definitions: []);
Получение существующего класса и разрешение встроенных типов параметров в конструкторе:
// Определения для DiContainer
use Kaspi\DiContainer\{DiContainer, DiContainerConfig};
use Kaspi\DiContainer\Interfaces\DiContainerInterface;
$definitions = [
\PDO::class => [
// ⚠ Ключ "arguments" является зарезервированным значением
// и служит для передачи в конструктор класса.
// Таким объявлением в конструкторе класса \PDO
// аргумент с именем $dsn получит значение
// DiContainerInterface::ARGUMENTS = 'arguments'
DiContainerInterface::ARGUMENTS => [
'dsn' => 'sqlite:/opt/databases/mydb.sq3',
],
// Сервис будет создан как Singleton - в течении
// жизненного цикла контейнера.
DiContainerInterface::SINGLETON => true,
];
];
$config = new DiContainerConfig();
$container = new DiContainer(definitions: $definitions, config: $config);
// Объявление класса
namespace App;
class MyClass {
public function __construct(public \PDO $pdo) {}
}
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyClass;
/** @var MyClass $myClass */
$myClass = $container->get(MyClass::class);
$myClass->pdo->query('...')
Разрешение типов аргументов в конструкторе по имени:
// Объявление класса
namespace App;
class MyUsers {
public function __construct(public array $listOfUsers) {}
}
// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;
// При разрешении аргументов конструктора можно в качестве id контейнера
// использовать имя аргумента в конструкторе
$container = (new DiContainerFactory())->make(
[
'listOfUsers' => [
'John',
'Arnold',
];
]
);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyUsers;
/** @var MyUsers::class $users */
$users = $container->get(MyUsers::class);
print implode(',', $users->users); // John, Arnold
Для внедрения зависимостей в аргуемнты испольузется синтаксис @container-id
-
где строка начинающаяся с символа @
будет означать ссылку на другое определение
в контейнере, а часть container-id
определение в контейнере.
Разрешение простых (builtin) типов аргументов в объявлении:
// Объявление класса
namespace App;
class MyUsers {
public function __construct(public array $users) {}
}
class MyEmployers {
public function __construct(public array $employers) {}
}
// Определения для DiContainer
use App\{MyUsers, MyEmployers};
use Kaspi\DiContainer\DiContainerFactory;
use Kaspi\DiContainer\Interfaces\DiContainerInterface;
// В объявлении arguments->users = "@data"
// будет искать в контейнере определение "data".
$definitions = [
'data' => ['user1', 'user2'],
// ... more definitions
App\MyUsers::class => [
DiContainerInterface::ARGUMENTS => [
// внедрение зависимости аргумента по ссылке на контейнер-id
'users' => '@data',
],
],
App\MyEmployers::class => [
DiContainerInterface::ARGUMENTS => [
// внедрение зависимости аргумента по ссылке на контейнер-id
'employers' => '@data',
],
],
];
$container = (new DiContainerFactory())->make($definitions);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\{MyUsers, MyEmployers};
/** @var MyUsers::class $users */
$users = $container->get(MyUsers::class);
print implode(',', $users->users); // user1, user2
/** @var MyEmployers::class $employers */
$employers = $container->get(MyEmployers::class);
print implode(',', $employers->employers); // user1, user2
Получение через функцию обратного вызова (\Closure
):
// Объявление класса
namespace App;
use Psr\Log\LoggerInterface;
class MyLogger {
public function __construct(protected LoggerInterface $logger) {}
public function logger(): LoggerInterface {
return $this->logger;
}
}
// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Monolog\{Logger, Handler\StreamHandler, Level};
$definitions = [
'logger_file' => '/path/to/your.log',
'logger_name' => 'app-logger',
LoggerInterface::class =>, static function (ContainerInterface $c) {
return (new Logger($c->get('logger_name')))
->pushHandler(new StreamHandler($c->get('logger_file')));
}
];
$container = (new DiContainerFactory())->make($definitions);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyLogger;
/** @var MyClass $myClass */
$myClass = $container->get(MyLogger::class);
$myClass->logger()->debug('...');
Получение через объявления в контейнере:
// Объявление классов
namespace App;
interface ClassInterface {}
class ClassFirst implements ClassInterface {
public function __construct(public string $file) {}
}
// Определения для DiContainer
use App\ClassFirst;
use App\ClassInterface;
use Kaspi\DiContainer\DiContainerFactory;
use Kaspi\DiContainer\Interfaces\DiContainerInterface;
$definition = [
ClassInterface::class => [
ClassFirst::class,
DiContainerInterface::ARGUMENTS => [
'file' => '/var/log/app.log',
]
],
];
$container = (new DiContainerFactory()->make($definition);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\ClassInterface;
/** @var ClassFirst $myClass */
$myClass = $container->get(ClassInterface::class);
print $myClass->file; // /var/log/app.log
Отдельное определение для класса и приявязка интерфейса к реализациия для примера выше:
// Определения для DiContainer - отдельно класс и реализации.
use App\ClassFirst;
use App\ClassInterface;
use Kaspi\DiContainer\DiContainerFactory;
use Kaspi\DiContainer\Interfaces\DiContainerInterface;
$definition = [
ClassFirst::class => [
DiContainerInterface::ARGUMENTS => [
'file' => '/var/log/app.log',
],
],
ClassInterface::class => ClassFirst::class,
];
$container = (new DiContainerFactory()->make($definition);
Класс фабрика должен реализовывать интерфейс Kaspi\DiContainer\Interfaces\DiFactoryInterface
.
// Объявления классов
namespace App;
use Kaspi\DiContainer\Interfaces\DiFactoryInterface;
use Psr\Container\ContainerInterface;
class MyClass {
public function __construct(private Db $db) {}
// ...
}
// ....
class FactoryMyClass implements DiFactoryInterface {
public function __invoke(ContainerInterface $container): MyClass {
return new MyClass(new Db(...));
}
}
// определения для контейнера
use Kaspi\DiContainer\DiContainerFactory;
$definitions = [
App\MyClass::class => App\FactoryMyClass::class
];
$container = (new DiContainerFactory())->make($definitions);
// Получение данных из контейнера с автоматическим связыванием зависимостей
$container->get(App\MyClass::class); // instance of App\MyClass
Kaspi\DiContainer\diDefinition(?string $containerKey = null, mixed $definition = null, ?array $arguments = null, ?bool $isSingleton = null): array
Пример использования хэлпера для конфигурирования:
// объявления классов
namespace App;
interface SumInterface {}
class Sum {
public function __construct(public int $init) {}
}
// Определения контейнера
use Kaspi\DiContainer\diDefinition;
$definition = [
App\SumInterface::class => diDefinition(definition: App\Sum::class, arguments: ['init' => 50]),
App\Sum::class => diDefinition(arguments: ['init' => 10], isSingleton: true),
];
$c = (new DiContainerFactory())->make($definition);
// ... вызова определения
print $c->get(App\SumInterface::class)->init; // 50
print $c->get(App\Sum::class)->init; // 10
альтернативное объявление определений:
use \Kaspi\DiContainer\diDefinition;
$definition1 = diDefinition(
containerKey: App\SumInterface::class,
definition: App\Sum::class,
arguments: ['init' => 50]
);
$definition2 = diDefinition(
containerKey: App\Sum::class,
arguments: ['init' => 10],
isSingleton: true
);
$c = (new DiContainerFactory())->make($definition1 + $definition2);
В конфигурации контейнера по умолчанию параметр useAttribute
включён.
Доступные атрибуты:
Kaspi\DiContainer\Interfaces\DiFactoryInterface
#[\Kaspi\DiContainer\Attributes\Inject(
id: '', // определение зависимости
arguments: [], // аргументы конструктора для зависимости
isSingleton: false, // сервис создаётся как Singleton
)]
Получение существующего класса и разрешение простых типов параметров в конструкторе:
// Объявление класса
namespace App;
use Kaspi\DiContainer\Attributes\Inject;
class MyClass {
public function __construct(
#[Inject(arguments: ['dsn' => '@pdo_dsn'])]
public \PDO $pdo
) {}
}
// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;
$definitions = ['pdo_dsn' => 'sqlite:/opt/databases/mydb.sq3'];
$container = (new DiContainerFactory())->make($definitions);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyClass;
/** @var MyClass $myClass */
$myClass = $container->get(MyClass::class);
$myClass->pdo->query('...')
Использование Inject атрибута на простых (встроенных) типах для получения данных из контейнера:
// Объявление класса
namespace App;
use Kaspi\DiContainer\Attributes\Inject;
class MyUsers {
public function __construct(
// ссылка на контейнер с определением
#[Inject('@users_data')]
public array $users
) {}
}
class MyEmployers {
public function __construct(
// ссылка на контейнер с определением
#[Inject('@users_data')]
public array $employers
) {}
}
// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;
$definitions = [
'users_data' => ['user1', 'user2'],
];
$container = (new DiContainerFactory())->make($definitions);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\{MyUsers, MyEmployers};
/** @var MyUsers::class $users */
$users = $container->get(MyUsers::class);
print implode(',', $users->users); // user1, user2
/** @var MyEmployers::class $employers */
$employers = $container->get(MyEmployers::class);
print implode(',', $employers->employers); // user1, user2
Внедрение типизированных аргументов через атрибут Inject:
// Объявление класса
namespace App;
use Kaspi\DiContainer\Attributes\Inject;
class MyUsers {
public function __construct(public array $users) {}
}
class MyCompany {
public function __construct(
#[Inject(arguments: ['users' => '@users_bosses'])]
public MyUsers $bosses,
#[Inject(arguments: ['users' => '@users_staffs'])]
public MyUsers $staffs,
) {}
}
// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;
$definitions = [
'users_bosses' => ['user1', 'user2'],
'users_staffs' => ['user3', 'user3'],
];
$container = (new DiContainerFactory())->make($definitions);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyCompany;
/** @var MyCompany::class $company */
$company = $container->get(MyCompany::class);
print implode(',', $company->bosses->users); // user1, user2
print implode(',', $company->staffs->users); // user3, user4
#[\Kaspi\DiContainer\Attributes\Service(
id: '', // Класс реализующий интерфейс
arguments: [], // аргументы конструктора для зависимости
isSingleton: false, // сервис создаётся как Singleton
)]
// Объявление классов
namespace App;
use Kaspi\DiContainer\Attributes\Inject;
use Kaspi\DiContainer\Attributes\Service;
#[Service(CustomLogger::class)]
interface CustomLoggerInterface {
public function loggerFile(): string;
}
class CustomLogger implements CustomLoggerInterface {
public function __construct(
#[Inject('@logger_file')]
protected string $file,
) {}
public function loggerFile(): string {
return $this->file;
}
}
// ...
class MyLogger {
public function __construct(
#[Inject]
public CustomLoggerInterface $customLogger
) {}
}
// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;
$container = (new DiContainerFactory())->make(
definitions: ['logger_file' => '/var/log/app.log']
);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyLogger;
/** @var MyLogger $myClass */
$myClass = $container->get(MyLogger::class);
print $myClass->customLogger->loggerFile(); // /var/log/app.log
#[\Kaspi\DiContainer\Attributes\Service(
id: '', // Класс реализующий интерфейс Kaspi\DiContainer\Interfaces\DiFactoryInterface
arguments: [], // аргументы конструктора для зависимости
isSingleton: false, // сервис создаётся как Singleton
)]
// Определение класса
namespace App;
#[Factory(App\Factory\FactorySuperClass::class)]
class SuperClass
{
public function __construct(public string $name, public int $age) {}
}
// определение фабрики
namespace App\Factory;
use Kaspi\DiContainer\Interfaces\DiFactoryInterface;
use Psr\Container\ContainerInterface;
class FactorySuperClass implements DiFactoryInterface
{
public function __invoke(ContainerInterface $container): App\SuperClass
{
return new App\SuperClass('Piter', 22);
}
}
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\SuperClass;
/** @var SuperClass $myClass */
$myClass = $container->get(SuperClass::class);
print $myClass->name; // Piter
print $myClass->age; // 22
Так же можно использовать атрибут Factory для аргументов конструктора или методов класса:
// определение класса
namespace App;
use Kaspi\DiContainer\Attributes\DiFactory;
class ClassWithFactoryArgument
{
public function __construct(
#[DiFactory(FactoryClassWithFactoryArgument::class)]
public \ArrayIterator $arrayObject
) {}
}
// Фабрика класса
namespace App;
use Kaspi\DiContainer\Interfaces\DiFactoryInterface;
use Psr\Container\ContainerInterface;
class FactoryClassWithFactoryArgument implements DiFactoryInterface
{
public function __invoke(ContainerInterface $container): \ArrayIterator
{
return new \ArrayIterator(
$container->has('names') ? $container->get('names') : []
);
}
}
// Определение для контейнера
use Kaspi\DiContainer\DiContainerFactory;
$container = (new DiContainerFactory())->make(
definitions: [
'names' => ['Ivan', 'Piter', 'Vasiliy']
]
);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\ClassWithFactoryArgument;
/** @var ClassWithFactoryArgument $myClass */
$myClass = $container->get(ClassWithFactoryArgument::class);
$myClass->arrayObject->getArrayCopy(); // массив ['Ivan', 'Piter', 'Vasiliy']
Контейнер предоставляет метод call()
, который может вызывать любой PHP callable тип:
\Closure
)App\MyClass::someStaticMethod
[$classInstance, 'someMethod']
$classInstance
Так же доступны вызовы с параметрами:
__invoke
метод
$container->call(App\MyClassWithInvokeMethod::class);
$container->call([App\MyClass::class, 'someMethod']);
$container->call(App\MyClass::class.'::someMethod');
$container->call('App\MyClass::someMethod');
Дополнительно call
может:
аргументы метода:
call(array|callable|string $definition, array $arguments = [])
Абстрактный пример с контроллером:
// определение класса
namespace App\Controllers;
use App\Service\ServiceOne;
class Post {
public function __construct(private ServiceOne $serviceOne) {}
public function store(string $name) {
$this->serviceOne->save($name);
return 'The name '.$name.' saved!';
}
}
// определение контейнера
namespace App;
use Kaspi\DiContainer\DiContainerFactory;
$container = (new DiContainerFactory())->make();
// вызов контроллера с автоматическим разрешением зависимостей и передачей аргументов
print $container->call(
['App\Controllers\Post', 'store'],
[$_POST] // $_POST содержит ['name' => 'Ivan']
);
результат
The name Ivan saved!
Абстрактный пример с autowiring
и подстановкой дополнительных параметров при вызове функции:
use Kaspi\DiContainer\DiContainerFactory;
// определение контейнера
$container = (new DiContainerFactory())->make();
// ... more code ...
// определение callback с типизированным параметром
$helperOne = static function(App\Service\ServiceOne $service, string $name) {
$service->save($name);
return 'The name '.$name.' saved!';
};
// ... more code ...
// вызов callback с autowiring
print $container->call($helperOne, ['name' => 'Vasiliy']); // The name Vasiliy saved!
Прогнать тесты без подсчёта покрытия кода
composer test
Запуск тестов с проверкой покрытия кода тестами
./vendor/bin/phpunit
Для статического анализа используем пакет Phan.
Запуск без PHP расширения PHP AST
./vendor/bin/phan --allow-polyfill-parser
Для приведения кода к стандартам используем php-cs-fixer который объявлен в dev зависимости composer-а
composer fixer
Указать образ с версией PHP можно в файле .env
в ключе PHP_IMAGE
.
По умолчанию контейнер собирается с образом php:8.0-cli-alpine
.
Собрать контейнер
docker-compose build
Установить зависимости php composer-а:
docker-compose run --rm php composer install
Прогнать тесты с отчетом о покрытии кода
docker-compose run --rm php vendor/bin/phpunit
⛑ pезультаты будут в папке .coverage-html
Статический анализ кода Phan (static analyzer for PHP)
docker-compose run --rm php vendor/bin/phan
Можно работать в shell оболочке в docker контейнере:
docker-compose run --rm php sh