Контейнер внедрения зависимостей

Обзор

Контейнер внедрения зависимостей (DIC) — это мощное расширение, которое позволяет управлять зависимостями вашего приложения.

Понимание

Внедрение зависимостей (DI) — это ключевой концепт в современных PHP-фреймворках и используется для управления созданием и конфигурацией объектов. Некоторые примеры библиотек DIC: flightphp/container, Dice, Pimple, PHP-DI, и league/container.

DIC — это изысканный способ позволить вам создавать и управлять вашими классами в централизованном месте. Это полезно, когда вам нужно передавать один и тот же объект нескольким классам (например, вашим контроллерам или middleware).

Основное использование

Старый способ может выглядеть так:


require 'vendor/autoload.php';

// класс для управления пользователями из базы данных
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// в вашем файле routes.php

$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');

$UserController = new UserController($db);
Flight::route('/user/@id', [ $UserController, 'view' ]);
// другие маршруты UserController...

Flight::start();

Из приведенного выше кода видно, что мы создаем новый объект PDO и передаем его в класс UserController. Это нормально для небольшого приложения, но по мере роста вашего приложения вы обнаружите, что создаете или передаете один и тот же объект PDO в нескольких местах. Здесь DIC становится очень полезным.

Вот тот же пример с использованием DIC (используя Dice):


require 'vendor/autoload.php';

// тот же класс, что и выше. Ничего не изменилось
class UserController {

    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function view(int $id) {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);

        print_r($stmt->fetch());
    }
}

// создаем новый контейнер
$container = new \Dice\Dice;

// добавляем правило, чтобы сказать контейнеру, как создать объект PDO
// не забудьте переприсвоить его самому себе, как ниже!
$container = $container->addRule('PDO', [
    // shared означает, что один и тот же объект будет возвращен каждый раз
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// Это регистрирует обработчик контейнера, чтобы Flight знал, как его использовать.
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// теперь мы можем использовать контейнер для создания нашего UserController
Flight::route('/user/@id', [ UserController::class, 'view' ]);

Flight::start();

Я уверен, вы можете подумать, что в примере добавилось много лишнего кода. Магия происходит, когда у вас есть другой контроллер, которому нужен объект PDO.


// Если все ваши контроллеры имеют конструктор, который требует объект PDO
// каждый из маршрутов ниже автоматически получит его внедренным!!!
Flight::route('/company/@id', [ CompanyController::class, 'view' ]);
Flight::route('/organization/@id', [ OrganizationController::class, 'view' ]);
Flight::route('/category/@id', [ CategoryController::class, 'view' ]);
Flight::route('/settings', [ SettingsController::class, 'view' ]);

Дополнительным преимуществом использования DIC является то, что модульное тестирование становится гораздо проще. Вы можете создать мок-объект и передать его в ваш класс. Это огромное преимущество при написании тестов для вашего приложения!

Создание централизованного обработчика DIC

Вы можете создать централизованный обработчик DIC в вашем файле services, расширив ваше приложение через extending. Вот пример:

// services.php

// создаем новый контейнер
$container = new \Dice\Dice;
// не забудьте переприсвоить его самому себе, как ниже!
$container = $container->addRule('PDO', [
    // shared означает, что один и тот же объект будет возвращен каждый раз
    'shared' => true,
    'constructParams' => ['mysql:host=localhost;dbname=test', 'user', 'pass' ]
]);

// теперь мы можем создать маппируемый метод для создания любого объекта. 
Flight::map('make', function($class, $params = []) use ($container) {
    return $container->create($class, $params);
});

// Это регистрирует обработчик контейнера, чтобы Flight знал, как использовать его для контроллеров/middleware
Flight::registerContainerHandler(function($class, $params) {
    Flight::make($class, $params);
});

// предположим, у нас есть следующий пример класса, который принимает объект PDO в конструкторе
class EmailCron {
    protected PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function send() {
        // код для отправки email
    }
}

// И наконец, вы можете создавать объекты с использованием внедрения зависимостей
$emailCron = Flight::make(EmailCron::class);
$emailCron->send();

flightphp/container

Flight имеет плагин, который предоставляет простой контейнер, соответствующий PSR-11, который вы можете использовать для обработки внедрения зависимостей. Вот быстрый пример, как его использовать:


// index.php например
require 'vendor/autoload.php';

use flight\Container;

$container = new Container;

$container->set(PDO::class, fn(): PDO => new PDO('sqlite::memory:'));

Flight::registerContainerHandler([$container, 'get']);

class TestController {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function index() {
    var_dump($this->pdo);
    // выведет это правильно!
  }
}

Flight::route('GET /', [TestController::class, 'index']);

Flight::start();

Расширенное использование flightphp/container

Вы также можете разрешать зависимости рекурсивно. Вот пример:

<?php

require 'vendor/autoload.php';

use flight\Container;

class User {}

interface UserRepository {
  function find(int $id): ?User;
}

class PdoUserRepository implements UserRepository {
  private PDO $pdo;

  function __construct(PDO $pdo) {
    $this->pdo = $pdo;
  }

  function find(int $id): ?User {
    // Реализация ...
    return null;
  }
}

$container = new Container;

$container->set(PDO::class, static fn(): PDO => new PDO('sqlite::memory:'));
$container->set(UserRepository::class, PdoUserRepository::class);

$userRepository = $container->get(UserRepository::class);
var_dump($userRepository);

/*
object(PdoUserRepository)#4 (1) {
  ["pdo":"PdoUserRepository":private]=>
  object(PDO)#3 (0) {
  }
}
 */

DICE

Вы также можете создать свой собственный обработчик DIC. Это полезно, если у вас есть кастомный контейнер, который вы хотите использовать и который не соответствует PSR-11 (Dice). См. раздел basic usage о том, как это сделать.

Кроме того, есть некоторые полезные значения по умолчанию, которые облегчат вам жизнь при использовании Flight.

Экземпляр Engine

Если вы используете экземпляр Engine в ваших контроллерах/middleware, вот как вы бы его настроили:


// Где-то в вашем bootstrap-файле
$engine = Flight::app();

$container = new \Dice\Dice;
$container = $container->addRule('*', [
    'substitutions' => [
        // Здесь вы передаете экземпляр
        Engine::class => $engine
    ]
]);

$engine->registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

// Теперь вы можете использовать экземпляр Engine в ваших контроллерах/middleware

class MyController {
    public function __construct(Engine $app) {
        $this->app = $app;
    }

    public function index() {
        $this->app->render('index');
    }
}

Добавление других классов

Если у вас есть другие классы, которые вы хотите добавить в контейнер, с Dice это просто, поскольку они будут автоматически разрешены контейнером. Вот пример:


$container = new \Dice\Dice;
// Если вам не нужно внедрять зависимости в ваши классы,
// вам не нужно ничего определять!
Flight::registerContainerHandler(function($class, $params) use ($container) {
    return $container->create($class, $params);
});

class MyCustomClass {
    public function parseThing() {
        return 'thing';
    }
}

class UserController {

    protected MyCustomClass $MyCustomClass;

    public function __construct(MyCustomClass $MyCustomClass) {
        $this->MyCustomClass = $MyCustomClass;
    }

    public function index() {
        echo $this->MyCustomClass->parseThing();
    }
}

Flight::route('/user', 'UserController->index');

PSR-11

Flight также может использовать любой контейнер, соответствующий PSR-11. Это означает, что вы можете использовать любой контейнер, реализующий интерфейс PSR-11. Вот пример с использованием PSR-11 контейнера League:


require 'vendor/autoload.php';

// тот же класс UserController, что и выше

$container = new \League\Container\Container();
$container->add(UserController::class)->addArgument(PdoWrapper::class);
$container->add(PdoWrapper::class)
    ->addArgument('mysql:host=localhost;dbname=test')
    ->addArgument('user')
    ->addArgument('pass');
Flight::registerContainerHandler($container);

Flight::route('/user', [ 'UserController', 'view' ]);

Flight::start();

Это может быть немного более многословным, чем предыдущий пример с Dice, но все равно выполняет задачу с теми же преимуществами!

См. также

  • Extending Flight - Узнайте, как вы можете добавить внедрение зависимостей в свои собственные классы, расширив фреймворк.
  • Configuration - Узнайте, как настроить Flight для вашего приложения.
  • Routing - Узнайте, как определять маршруты для вашего приложения и как работает внедрение зависимостей с контроллерами.
  • Middleware - Узнайте, как создавать middleware для вашего приложения и как работает внедрение зависимостей с middleware.

Устранение неисправностей

  • Если у вас проблемы с контейнером, убедитесь, что вы передаете правильные имена классов в контейнер.

Журнал изменений

  • v3.7.0 - Добавлена возможность регистрации обработчика DIC в Flight.