DIコンテナのススメ

# 概要

DIやDIコンテナについて、自分が調べたことをまとめます。

# DIとは何か

依存性の注入（Dependency injection）を略してDIと言います。
依存性とは具体的にはクラスのことです。
以下の例を見てみます。

```php
class ProductController {
  public function mainAction() {
    $model = new ProductModel();
    $product = $model->getById(1);
  }
}
class ProductModel {
  public function getById($id) {
    $db = new MySqlConnect();
    $query = $db->query('select * from product where id = ?', $id);
    $product = $query->getResult();
    // ここに様々な処理があるとする
    return $product;
  }
}
```

`ProductModel`では`MySqlConnect`をnewして利用しています。
この状態を`ProductModel`は`MySqlConnect`に依存している、と言い、以下のようなデメリットがあります。

- `ProductModel`と`MySqlConnect`は互いに密結合になるため、修正による影響を受けやすくなる。
- `MySqlConnect`をモックで置き換えられないため、`ProductModel`がテストしづらくなる。
- `ProductModel`は`MySqlConnect`が実装されていないと動かないため、分業がしづらい。

例えばデータベースシステムの移行などで、新たに`OracleConnect`を実装することを考えてみます。
上記のデメリットがあるため、`MySqlConnect`のインスタンスを`ProductController`から渡します。

```php
class ProductController {
  public function mainAction() {
    $model = new ProductModel(new MySqlConnect());
    $product = $model->getById(1);
  }
}
class ProductModel {
  private $db;
  pubic function __construct(MySqlConnect $db) {
    $this->db = $db;
  }
  public function getById($id) {
    $query = $this->db->query('select * from product where id = ?', $id);
    $product = $query->getResult();
    // ここに様々な処理があるとする
    return $product;
  }
}
```

上記の手法、デザインパターンをDIと呼びます。
コンストラクタでセットしているので、コンストラクタ注入と呼びます。

続いて`MySqlConnect`を`OracleConnect`に置き換えます。
単純に置き換えると、将来的に`PgSqlConnect`が登場した際に困りますので、`DatabaseConnect`インターフェースを実装します。

```php
interface DatabaseConnect {}
class MySqlConnect implements DatabaseConnect {}
class OracleConnect implements DatabaseConnect {}

class ProductController {
  public function mainAction() {
    // $model = new ProductModel(new MySqlConnect());
    $model = new ProductModel(new OracleConnect());
    $product = $model->getById(1);
  }
}
class ProductModel {
  private $db;
  pubic function __construct(DatabaseConnect $db) {
    $this->db = $db;
  }
  public function getById($id) {
    $query = $this->db->query('select * from product where id = ?', $id);
    $product = $query->getResult();
    // ここに様々な処理があるとする
    return $product;
  }
}
```

これで新たに`PgSqlConnect`が登場しても、`ProductModel`を修正する必要はありません。

# DIコンテナとは何か

ですが、毎回この作業を手動で実装するのは大変です。
そこでDIコンテナという仕組みを使います。

以下は`pimple`を使った例です。

```php
require_once __DIR__.'/vendor/autoload.php';
use Pimple\Container;

interface DatabaseConnect {}
class MySqlConnect implements DatabaseConnect {}
class OracleConnect implements DatabaseConnect {}

class Settings {
  private $container;
  public function __construct() {
    $container = new Container();
    $container['db'] = function ($c) {
      return new OracleConnect();
    }
    $this->container = $container;
  }
  public function get($name) {
    return $this->container[$name];
  }
}

class ProductController {
  public function mainAction() {
    $settings = new Settings();
    $model = new ProductModel($settings->get('db'));
    $product = $model->getById(1);
  }
}
class ProductModel {
  private $db;
  pubic function __construct(DatabaseConnect $db) {
    $this->db = $db;
  }
  public function getById($id) {
    $query = $this->db->query('select * from product where id = ?', $id);
    $product = $query->getResult();
    // ここに様々な処理があるとする
    return $product;
  }
}
```

これで`Settings`を変更するだけで、すべてのクラスに`OracleConnect`を適用させることができます。
DIコンテナ、インターフェースを使うことで、以下のメリットが得られます。

- クラス同士の結合度を下げ、互いの修正が影響し合わないように設計できる。
- `DatabaseConnect`インターフェースを実装したテスト用のクラスを作成することで、`ProductModel`を簡単にテストすることができる。
- `OracleConnect`が未実装であっても、`ProductModel`を開発することができるので、分業ができる。

# Symfonyではどのようになっているのか

SymfonyはDIコンテナを備えたフレームワークです。
Services.ymlという設定ファイルがDIコンテナの設定ファイルの役割を果たしています。

```yml
services:
  product_model:
    class: AppBundle\SomeDirectory\ProductModel
    arguments: ["@db"]
  db:
    # ここを変えれば別のクラスをProductModelに渡せる
    class: AppBundle\SomeDirectory\MySqlConnect
```

```php
class ProductController {
  public function mainAction() {
    $model = $this->get('product_model');
    $product = $model->getById(1);
  }
}
class ProductModel {
  private $db;
  pubic function __construct(DatabaseConnect $db) {
    $this->db = $db;
  }
  public function getById($id) {
    $query = $this->db->query('select * from product where id = ?', $id);
    $product = $query->getResult();
    // ここに様々な処理があるとする
    return $product;
  }
}
```

# まとめ

自分自身あまりよく背景を知らずにSymfonyを使っていたのですが、DIコンテナを知ることでSymfony自体の理解も進みました。
ご参考になれば幸いです。