お知らせ フロントエンド バックエンド インフラ 品質保証 セキュリティ 製品 興味・関心 その他

2023.06.27

バックエンド

DIコンテナ(PHP-DI)を導入してみた

?はじめに

こんにちは!開発チームの遠藤です?
DIコンテナライブラリであるPHP-DI導入において得られた知見を共有したいと思います!
本記事では、DIやDIコンテナ、PHP-DIの詳細な説明は割愛させて頂きます?

?導入背景

開発中のプロジェクト(PHP)では、DI(Dependency Injection)を多用しており、
インスタンス生成の責務を担ってくれるDIコンテナの導入を検討していました。

?選定基準と結果

主な考慮点は以下となります。

  • DIコンテナのみの機能を提供
  • PSR-11 Container Interfaceに準拠
  • コミットが活発

上記を踏まえた上で、PHP-DIを選定しました!
PHP-DIはマイクロフレームワークであるSlimにも導入されており、実績も◎

?DIコンテナ導入前と後のコードの比較例

先ずは、簡単なコントローラクラスとサービスクラスを作ってみます。

<?php

class CustomerController
{
  private $customerService;

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

  public function getHello(): void
  {
    $this->customerService->getHello();
  }
}
<?php

class CustomerService
{
  public function getHello(): void
  {
    echo "hello";
  }
}

続いて、上記で定義したクラスをインスタンス化して使用する処理を書いてみます。
DIコンテナを用いるパターンと用いないパターンがあります。

DIコンテナを用いないパターン

<?php

require "autoload.php";

$customerService = new CustomerService();
$customerController = new CustomerController($customerService);
$customerController->getHello();

DIコンテナを用いたパターン

<?php

require "autoload.php";

// DIコンテナをセットアップ
$builder = new \DI\ContainerBuilder();
$container = $builder->build();

$target = $container->get(CustomerController::class);
$target->getHello();

DIコンテナを用いるパターンでは、$container->get()にクラス名を渡すだけで、インスタンスを返されるようになっています!クラス名を変数で持たせれば、Factoryパターンになります!

補足として、内部ではAutowiring(インスタンス依存関係を自動解決する機能)が働いており、
newを複数回する必要がなくなっています。

DIを用いたFWであるSpring BootLaravelNestJSも内部ではAutowiringが動いているようです。

?Autowiringを用いる際の注意点

Autowiringが動かないケースもあるので注意が必要です。
例として、以下が挙げられます。

<?php

class CustomerController
{
  private $customerService;

  // CommonServiceに依存に変更
  public function __construct(CommonService $customerService)
  {
    $this->customerService = $customerService;
  }

  public function getHello(): void
  {
    $this->customerService->getHello();
  }
}
<?php

class CommonService
{
  private $id;

  // コンストラクタ引数にインスタンス以外を持つ
  public function __construct(string $id)
  {
    $this->id = $id;
  }

  public function getHello(): void
  {
    echo "hello, {$this->id}";
  }
}

CommonServiceのようなコンストラクタ引数にインスタンス以外を持つクラス依存がある場合に
上述のmain.phpを実行するとエラーが発生します。

$container->get()にはクラス名しか与えておらず、CommonServiceのコンストラクタ引数の$idは、
どこにも宣言していないからです。

PHP-DIの内部では、依存クラスのコンストラクタ引数がクラス名であれば、autoloadでクラスファイルを割り出して、引数なしでnewするようです。

Autowiringを上手く動かすには、
コンストラクタ引数はインスタンスのみを宣言することが必要となります!

但し、PHP-DIには上記エラーの回避方法も用意されています。

?Autowiring以外のインスタンス定義

<?php

require "autoload.php";

$builder = new \DI\ContainerBuilder();

// 定義を追加
$builder->addDefinitions([
  CommonService::class => function () {
    return new CommonService("hoge");
  },
]);
$container = $builder->build();

$target = $container->get(CustomerController::class);
$target->getHello();

上記のように、$builder->addDefinitions()でクラス名に紐づくインスタンス生成の定義を渡してあげれば、エラーを解消することもできます。※ 定義がないクラス名はAutowiringが適用されます

マニュアルでの定義だと記述が増えてしまい、管理の手間が増えてしまうので、
Autowiringで動くクラス設計が個人的には大切だと考えています。

?さいごに

個人的には、PHP-DIでDIコンテナを導入したことで、インスタンス管理が行いやすくなったと考えています。また、SpringBoot等のFWが内部でどのようにDIを管理しているのかも想像しやすくなりました。PHP-DIはできることも多いので、気になる方は是非、公式ドキュメントをチェックしてみてください!

遠藤

遠藤

記事一覧

マーケライズ開発チームのエンジニア
Javaに思い入れあり
最近、Reactを「完全に理解した」→「何も分からん」にレベルアップした!?