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

Здравствуйте! Я изучаю паттерны проектирования, и сейчас мне интересен Dependency Injection Container. Мне нужна консультация от более опытных специалистов. 

Имеется базовая модель:

```php
abstract class Model
{
    protected $connection;

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

А также модель:

```php
namespace models;

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

И контроллер:

```php
namespace controllers;

use models\User as UserModel;

class User extends Controller
{
    private $model;

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

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

Следует ли хранить зависимости в статическом свойстве массива?

Вот код моего контейнера:

```php
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");
        }
    }
}
```

Заранее благодарю за советы и рекомендации!
  • 6 октября 2024 г. 17:14
Ответы на вопрос 2
Здравствуйте! Отличный вопрос, и вам абсолютно правы, что использование Dependency Injection (DI) Container поможет решить вашу проблему и позволить вам иметь одно подключение к базе данных на всё приложение, вместо создания нового подключения при каждом создании модели.

### Основная идея

Чтобы использовать DI Container для управления длиной жизни соединений с базой данных, вам нужно:

1. Создать соединение с базой данных один раз и затем передавать это соединение в ваши модели через конструктор.
2. Использовать метод `set` в контейнере, чтобы хранить объект подключения и возвращать его при запросах.

### Изменения в коде

1. Измените класс модели так, чтобы он принимал подключение через конструктор:

```php
abstract class Model
{
    protected $connection;

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

2. Измените класс `User` так, чтобы он передавал подключение:

```php
namespace models;

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

3. В вашем контроллере передавайте подключение из контейнера:

```php
namespace controllers;

use models\User as UserModel;

class User extends Controller
{
    private $model;

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

    public function index()
    {
        // Используйте $this->model для работы с базой
    }
}
```

4. Теперь организуйте контейнер, чтобы он создавал это единственное подключение и передавал его:

```php
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];
    }
}
```

5. Настройте контейнер и используйте его:

```php
$container = new Container();

// Создаем подключение, например
$databaseConnection = (new PDOMysqlConnection())->getConnection();

// Регистрируем соединение в контейнере
$container->set('db', function () use ($databaseConnection) {
    return $databaseConnection;
});

// Можно создавать контроллеры
$userController = new controllers\User($container->get('db'));
```

### Заключение

Теперь класс `User` будет получать одно соединение с базой данных, что поможет оптимизировать ваши запросы и уменьшить нагрузку на базу данных. Такой подход также легко модифицировать: если вам когда-нибудь понадобится подключение к другой базе данных или сделать его временным, вы сможете сделать это, не меняя внешний код.

Использование статических свойств, как вы упомянули, не рекомендуется, так как это не позволяет легко тестировать и управлять зависимостями. Использование DI Container — более современный и гибкий подход, который позволяет улучшить ваш код и сделать его более тестируемым.
Без 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);

или через биндинг.
Похожие вопросы