kaspi/di-container

Dependency injection container with autowired

v1.1.0 2024-01-08 14:44 UTC

This package is auto-updated.

Last update: 2024-04-10 09:57:43 UTC


README

Kaspi/di-container — это легковесный контейнер внедрения зависимостей для PHP >= 8.0 с автоматическим связыванием зависимостей.

Установка

composer require kaspi/di-container

Миграция с версии 1.0.x к версии 1.1.x

Новая сигнатура интерфейса DiContainerFactoryInterface для метод make:

// Для версий 1.0.x
$container = DiContainerFactory::make($definitions);
// Для версий 1.1.х и выше
$container = (new DiContainerFactory())->make($definitions);

Примеры использования

DiContainer со стандартным конфигурированием

Через определения зависимостей вручную в DiContainer.

Получение существующего класса и разрешение встроенных типов параметров в конструкторе:

// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;

$container = (new DiContainerFactory())->make(
    [
        \PDO::class => [
            // ⚠ Ключ "arguments" является зарезервированным значением
            // и служит для передачи значений в конструктор класса.
            // Таким объявлением в конструкторе класса \PDO
            // аргумент с именем $dsn получит значение
            'arguments' => [
                'dsn' => 'sqlite:/opt/databases/mydb.sq3',
            ],
        ];
    ]
);
// Объявление класса
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 $users) {}
}

class MyEmployers {
    public function __construct(public array $employers) {}
}
// Определения для DiContainer
use App\{MyUsers, MyEmployers};
use Kaspi\DiContainer\DiContainerFactory;

// В объявлении arguments->users = "data"
// будет искать в контейнере ключ "data".

$definitions = [
    'data' => ['user1', 'user2'],
    App\MyUsers::class => [
        'arguments' => [
            'users' => 'data',
        ],
    ],
    App\MyEmployers::class => [
        'arguments' => [
            '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

Разрешение встроенных (простых) типов аргументов в объявлении со ссылкой на другой id контейнера:

// Определения для DiContainer
use Kaspi\DiContainer\DiContainerFactory;

// В конструкторе DiContainer - параметр "linkContainerSymbol"
// определяет значение-ссылку для авто связывания аргументов -
// по умолчанию символ "@"

$container = (new DiContainerFactory())->make(
    [
        // основной id в контейнере
        'sqlite-home' => 'sqlite:/opt/databases/mydb.sq3',
        //.....
        // Id в контейнере содержащий ссылку на id контейнера = "sqlite-home"
        'sqlite-test' => '@sqlite-home',
        \PDO::class => [
            'arguments' => [
                'dsn' => 'sqlite-test',
            ],
        ];
    ]
);
// Объявление класса
namespace App;

class MyClass {
    public function __construct(public \PDO $pdo) {}
}

// ....

/** @var MyClass $myClass */
$myClass = $container->get(MyClass::class);
// в конструктор MyClass будет вызван с определением
// new MyClass(
//      pdo: new \PDO(dsn: 'sqlite:/opt/databases/mydb.sq3') 
// );

Разрешение типов аргументов в конструкторе по имени аргумента:

// Объявление класса
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

Получение класса по интерфейсу

// Объявление класса
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};

$container = (new DiContainerFactory())->make([
    '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')));
    }
])
// Получение данных из контейнера с автоматическим связыванием зависимостей
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;

$container = (new DiContainerFactory())->make();
// ⚠ параметр "arguments" метода "set" установить аргументы для конструктора.
$container->set(ClassFirst::class, arguments: ['file' => '/var/log/app.log']);
$container->set(ClassInterface::class, ClassFirst::class);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\ClassInterface;

/** @var ClassFirst $myClass */
$myClass = $container->get(ClassInterface::class);
print $myClass->file; // /var/log/app.log

DiContainer c PHP атрибутами

Конфигурирование DiContainer c PHP атрибутами для определений.

Получение существующего класса и разрешение простых типов параметров в конструкторе:

// Объявление класса
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;

$container = (new DiContainerFactory())->make(
    ['pdo_dsn' => 'sqlite:/opt/databases/mydb.sq3']
);
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\MyClass;

/** @var MyClass $myClass */
$myClass = $container->get(MyClass::class);
$myClass->pdo->query('...')

Использование Inject атрибута на простых (встроенных) типах для
получения данных из контейнера, где ключ "users_data" определен в контейнере:

// Объявление класса
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

Получение по интерфейсу:

// Объявление классов
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([
    '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

Access array delimiter notation

Доступ к "контейнер-id" с вложенными определениям.

По-умолчанию символ разделитель .

Произвольный символ разделитель можно определить

  • Kaspi\DiContainer\DiContainer::__construct аргумент $delimiterAccessArrayNotationSymbol
  • Kaspi\DiContainer\DiContainerFactory::make аргумент $delimiterAccessArrayNotationSymbol
Access-array-delimiter-notation определение на базе ручного конфигурирования
// Определения для DiContainer
$definitions = [
    'app' => [
        'admin' => [
            'email' =>'admin@mail.com',
        ],
        'logger' => App\Logger::class,
        'logger_file' => '/var/app.log',
    ],
    App\Logger::class => [
        'arguments' => [
            'file' => 'app.logger_file'
        ],
    ],
    App\SendEmail::class => [
        'arguments' => [
            'from' => 'app.admin.email',
            'logger' => 'app.logger',
        ],
    ],
];

$container = DiContainerFactory::make($definitions);
// Объявление классов
namespace App;

interface LoggerInterface {}

class Logger implements LoggerInterface {
    public function __construct(
        public string $file
    ) {}
}

class SendEmail {
    public function __construct(
        public string $from,
        public LoggerInterface $logger,
    ) {}
}
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\SendEmail;

/** @var SendEmail $myClass */
$sendEmail = $container->get(SendEmail::class);
print $sendEmail->from; // admin@mail.com
print $sendEmail->logger->file; // /var/app.log
Access-array-delimiter-notation - определения на основе PHP атрибутов.
// Определения для DiContainer
$definitions = [
    'app' => [
        'admin' => [
            'email' =>'admin@mail.com',
        ],
        'logger' => App\Logger::class,
        'logger_file' => '/var/app.log',
    ],
];

$container = DiContainerFactory::make($definitions);
// Объявление классов
namespace App;

use Kaspi\DiContainer\Attributes\Inject;

interface LoggerInterface {}

class Logger implements LoggerInterface {
    public function __construct(
        #[Inject('app.logger_file')]
        public string $file
    ) {}
}

class SendEmail {
    public function __construct(
        #[Inject('app.admin.email')]
        public string $from,
        #[Inject('app.logger')]
        public LoggerInterface $logger,
    ) {}
}
// Получение данных из контейнера с автоматическим связыванием зависимостей
use App\SendEmail;

/** @var SendEmail $myClass */
$sendEmail = $container->get(SendEmail::class);
print $sendEmail->from; // admin@mail.com
print $sendEmail->logger->file; // /var/app.log

Тесты

Прогнать тесты без подсчета покрытия кода

composer test

Запуск тестов с проверкой покрытия кода тестами

./vendor/bin/phpunit

Статический анализ кода

Для статического анализа используем пакет Phan.

Запуск без PHP расширения PHP AST

./vendor/bin/phan --allow-polyfill-parser

Code style

Для приведения кода к стандартам используем php-cs-fixer который объявлен в dev зависимости composer-а

composer fixer

Использование Docker образа с PHP 8.0, 8.1, 8.2, 8.3

Указать образ с версией 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