Как грамотно реализовать одно соединение с базой данных на все приложение, с помощью Dependency Injection Container?

Здравствуйте, изучаю паттерны проектирования. В данном случае Dependency Injection Container. Нужен совет от опытных коллег.

Есть базовая модель
abstract class Model
{
    protected $connection;

    public function __construct()
    {
        $this->connection = (new PDOMysqlConnection)->getConnection();
    }
}


Модель
namespace models;

class User extends Model
{
    public function getAll(): array
    {
        return $this->connection->executeQuery('SELECT * FROM users')->fetchAllAssociative();
    }
}


Контроллер
namespace controllers;

use models\User as UserModel;

class User extends Controller
{
    private $model;

    public function index()
    {
        $this->model = new UserModel;
    }
}


При каждом создании нового объекта модели, будет создаваться новое соединение с базой данных. Обычно соединение делал с помощью Singleton. Подскажите пожалуйста, есть ли возможность решить это с помощью DIC?

Хранить зависимости в статическом свойстве-массиве?

Код контейнера
class Container implements ContainerInterface
{
    private array $instances = [];

    public function set(string $id, callable $resolver): void
    {
        $this->instances[$id] = $resolver;
    }

    public function has(string $id): bool
    {
        return isset($this->instances[$id]);
    }

    public function get(string $id)
    {
        if (!$this->has($id)) {
            throw new NotFoundException("Dependency '$id' not found in the container.");
        }

        return $this->instances[$id]($this);
    }

    public function bind(string $id)
    {
        return $this->buildDependencies($id);
    }

    private function buildDependencies(string $className)
    {
        $reflector = new \ReflectionClass($className);

        if (empty($constructor = $reflector->getConstructor())
            || empty($parameters = $constructor->getParameters())) {
            return new $className;
        }

        $dependencies = [];

        foreach ($parameters as $parameter) {
            $dependency = $parameter->getType()?->getName();
            $dependencies[] = $this->get($dependency);
        }

        return $reflector->newInstanceArgs($dependencies);
    }
}
  • Вопрос задан
  • 378 просмотров
Пригласить эксперта
Ответы на вопрос 2
@Vitsliputsli
Создать зависимость можно только 2 способами, получив объект изнутри или проведя инъекцию снаружи. Если изнутри, то так или иначе понадобится синглтон, если снаружи, то ктото снаружи должен контролировать передачу одного и того же объекта во все нуждающиеся объекты.

1. Изнутри. Очевидно просто инстанцировать объект - это очень плохой вариант. Более менее нормальный вариант, это сервис локатор, с методом возвращающим нужный объект работающим как синглтон (т.е. не нужен прям класс синглтона, только метод который проверяет существует ли объект). Выглядит это не очень, синглтон и сервис локатор не считают хорошим решением. Из минусов статичное обращение к сервис локатору создаст проблемы для подмены объекта. Для разных подключений вы можете создать отдельные методы сервис локатора или разрулить в одном методе динамический выбор. Реальная проблема - тестирование, вы не сможете мокировать объект работы с БД. Но, можно это сделать ухищрениями, сделав ленивое подключение, и метод подмены объекта БД, хотя придется этот метод тащить в каждый класс. Как вариант, делать инъекцию объекта сервис локатора. Но остается другой большой минус, сервис локатор "гвоздями прибит" к большинству классов и вам придется его везде таскать, т.е. о красивых компонентах без странной зависимости от сервис локатора можно забыть.
2. Снаружи. Оптимально, и считается трендом - инъекция нужного объекта в конструктор. Будет ли это делать DIC или чтото иное не имеет значения. Не нужно таскать какието дополнительные сервис локаторы. Но данное решение имеет тот же минус, что и п.1, вы не сможете подменить объект на другой при мокировании:
public function __construct(PDO $db)
внедряемый объект обязан быть классом PDO, если хочется чтобы мок просто подсовывал одни и те же значения нужно менять на интерфейс, тогда моку достаточно выполнять требования интерфейса. Но, если ваш DIC использует автоматическое определение требуемых объектов по структуре конструктора, то использовать интерфейс не получится.
Ответ написан
Комментировать
Henryh
@Henryh
Веб-программист
Без DI контейнера ты создаёшь какую-то конкретную реализацию подключения к БД и каждый раз к ней обращаешься, без возможности изменить или создать новую. То есть экземпляр объекта класса PDO создаёшь внутри.
При Dependency Injection, в сухом остатке, тебе не нужно привязываться к реализации, достаточно создать объект класса pdo снаружи и передать его.
DI-контейнер позволяет автоматизировать создание объектов и внедрение зависимостей. Это означает, что нам нужно иметь класс, который хранит все варианты созданных подключений к БД.
То есть, в любой момент мы можем положить в контейнер нужное подключение, а потом просто обращаться к уже готовому подключению. Но в отличие от Singleton, таких подключений может быть несколько и в коде можно выбирать разные, там где это нужно.

В твоём случае это будет что-то типа такого:

$container = new Container();

$container->set('primary_db', function () {
    return new PDO(
        'mysql:host=localhost;dbname= primary_db',
        'db_user',
        'password'
    );
});

$db = $container->get('primary_db');


А в той экземпляр Model нужно передавать в конструктор уже существующее подключение. Например, так:

class User {
    private $db;

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

$db = $container->get('primary_db');

$user = new User($db);

или через биндинг.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы