PHP DIでDIを学ぶ

目次

  1. 背景
  2. DIとは
    1. 依存性とは
    2. 依存性注入
    3. 余談 IoC(Inversion of Control: 制御の反転)
  3. DIコンテナとは
    1. DIコンテナの基本機能
      1. DIコンテナphp-diの紹介
      2. 機能1.宣言を一か所にまとめる
      3. 機能2.Constructor Injectionのサポート
      4. 機能3.Autowired
      5. 機能4.Interfaceの実装
  4. アンチパターンなど
    1. Dependency Fetch アンチパターン
      1. make()メソッド
  5. まとめ
  6. 関連記事

背景

こんにちは。 かりんとうマニア(@karintozuki)です。
この記事ではDI(Dependency Injection: 依存性注入)とは何なのか、を
php-diというDIコンテナを使って説明したいと思います。

DIとは

DIはDependency Injectionの頭文字をとったもので、
日本語にすると、依存性注入と訳せます。

依存性とは

何かのクラスAがあったとします。

クラスAが他のクラスBを使うとき、クラスBはクラスAの依存性です。
クラスAはクラスBに依存しているということもできます。

これを普通にコードにしてみると以下の感じになると思います。

1
2
3
4
5
6
7
8
9
10
<?php
// 依存性注入を使わない例
class ClassA {
private ClassB $b;

function __construct(){
$this->b = new ClassB();
}
}

依存性注入

先ほどの例では、クラスBをクラスAの中でnewしていました。

依存性注入では、依存性であるクラスBを外部から「注入」します。

以下のコードを見てください。

1
2
3
4
5
6
7
8
9
10
<?php
// 依存性注入を使う
class ClassA {
private ClassB $b;

function __construct(ClassB $b){
$this->b = $b;
}
}

コードからnewが消えましたね。

これをするメリットは依存性を一か所にまとめることができることです。

依存性注入を使わずにクラスAを使う場合
1.アプリケーションがクラスAをnewする
2.クラスAがクラスBをnewする

依存性注入を使ってクラスAを使う場合
1.アプリケーションがクラスAに必要なクラスBをnewする
2.アプリケーションがクラスBを渡してクラスAをnewする

依存性注入を使う場合、主語がアプリケーションになっていることが分かるでしょうか。
個別のクラスは依存しているクラスの詳細を知る必要がなく、
アプリケーション一か所にクラスの宣言をまとめることができます。

これとInterfaceを合わせて使うことでクラスAのコードを変更することなく、
クラスBをクラスCに変えるなんてことができるようになります。
(DIとInterfaceについては詳しく後述します。)

余談 IoC(Inversion of Control: 制御の反転)

DIの文脈でIoCという言葉も良く出てきます。
これは先ほどの例でいうと、依存性クラスBを制御(new)するのが、
クラスA(アプリケーションから呼び出される側)からアプリケーション(Aを呼び出す側)に
反転しているという意味です。

Dependency Inversionということもあるみたいです。
いわゆるSOLID原則のDはDependency Inversionですね。

DIコンテナとは

まずはじめに、DIとDIコンテナの違いを説明します。
DI
DIはきれいなコードを書くためのテクニックです。

DIコンテナ
DIコンテナはDIを簡単に書くためのライブラリでDIを実装する際に便利な機能を有します。

DIはシンプルにテクニックの名前なので、
DIコンテナを使わなくてもDIを使ったコードは書けます。

DIコンテナの基本機能

最後にDIコンテナはどんな機能を提供するのか、
それにはどんなメリットがあるのか、具体例を紹介します。

DIコンテナphp-diの紹介

この記事ではphp-diというDIコンテナを使っていきます。
他にもDIコンテナはあるのですが、このライブラリが一番使われているようだったので、
php-diをチョイスしました。
何となく使えてしまうというか、直感的に使えるのでおすすめです。

また、LaravelのようなWebフレームワークを使っている方は
元からDIコンテナがフレームワークに組み込まれています。

呼び出し方や設定が違うだけで、根本の考え方は同じなので、
この記事を参考に自分が使っているフレームワークでは
どのような実装になるのか調べてみてください。

機能1.宣言を一か所にまとめる

先ほど、DIを使うと主語がアプリケーションになる、という説明をしました。
DIコンテナを使う場合、このアプリケーションに当たるのがDIコンテナです。
そのため、DIコンテナは

  • すべてのクラスをどのようにnewするのか、
  • どのクラスがなんのクラスに依存しているのか

を知っている必要があります。

この「どのようにnewするのか」をphp-diでは配列として宣言します。

実際のコードで確認してみましょう。
以下のようなプロジェクト構成を想定してください。

1
2
3
4
project root
├bootstrap.php
└app.php

bootstrap.phpでコンテナを設定して、app.phpでそれを使うような構成です。

注意
本記事のコードを実際に動かすには
php-diをcomposerなどでダウンロードするなどいくらか設定が必要です。
細かい設定などはphp-diの公式ドキュメントをご覧ください。

以下の例ではMonologを設定しています。

bootstrap.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

<?php

use DI\ContainerBuilder;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Psr\Log\LoggerInterface;
use Psr\Container\ContainerInterface;

// autoloadの読み込み
require __DIR__.'/../vendor/autoload.php';

// 設定配列の宣言
$def = [
// ここでLoggerをnewする
Logger::class => function(){
$logger = new Logger('DI-App');
$logger->pushHandler(new StreamHandler(__DIR__.'/app.log'));
return $logger;
},
// LoggerInterfaceにLoggerを設定
LoggerInterface::class => function(ContainerInterface $c){
return $c->get(Logger::class);
}
];

// 設定をもとにコンテナをビルドする
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions($def);
$container = $containerBuilder->build();

return $container;

ちょっと長いですが、$defという配列に依存性を宣言しています。

この部分を見てみます。

bootstrap.php
1
2
3
4
5
6
7
// ここでLoggerをnewする
Logger::class => function(){
$logger = new Logger('DI-App');
$logger->pushHandler(new StreamHandler(__DIR__.'/app.log'));
return $logger;
},

ここではシンプルにnewするだけでなく、Monologの初期設定もしています。

DIコンテナを使わない場合、こういった初期設定を
ログを使うすべてのクラスで行わないといけません。

これくらいの設定ならまだいいですが、
設定が10数行に及ぶことはよくあるかと思います。

そういった手間がなくせるのが良いところですね。

それではこのコンテナを使うコードを見てみましょう。

app.php
1
2
3
4
5
6
7
8
9
10
11
<?php

use Monolog\Logger;

$container = require __DIR__.'/bootstrap.php';

/** @var Logger $logger */
$logger = $container->get(Logger::class);

$logger->info('ログ');

コンテナからクラスを取り出す際はnewを使う代わりにget()メソッドを使用します。

app.php
1
2
3
/** @var Logger $logger */
$logger = $container->get(Logger::class);

ちなみに/** @var Logger $logger */を記述することで
IDEなどがクラスを認識してくれます。

newの代わりにコンテナからget()しただけだと、
変数がなんのクラスが不明のままになってしまいます。

機能2.Constructor Injectionのサポート

それでは先ほど使用したロガーを使うクラスを作ってみます。
Hello.phpというクラスを作ります。

1
2
3
4
5
project root
├bootstrap.php
├app.php
└Hello.php

Hello.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

<?php

namespace App;

use Monolog\Logger;

class Hello {
private Logger $logger;
function __construct(Logger $logger) { // Constructor Injection
$this->logger = $logger;
}

function sayHello(){
$this->logger->info("Hello");
}
}

app.php
1
2
3
4
5
6
7
8
9
10
11
<?php

use App\Hello;

$container = require __DIR__.'/bootstrap.php';

/** @var Hello $hello */
$hello = $container->get(Hello::class);

$hello->sayHello();

ここではコンストラクタインジェクションというテクニックを使用しています。
Hello.phpクラスのコンストラクタを見てみます。

Hello.php
1
2
3
4
function __construct(Logger $logger) { // Constructor Injection
$this->logger = $logger;
}

コンストラクタインジェクションは
依存性をコンストラクタの引数として記述し、そのクラスを作成する際に依存性を注入するテクニックです。
こうすることでHelloクラスの中でLoggerクラスをnewすることがなくなります。

DIコンテナはHelloクラスを作るときにLoggerクラスを自動的に作ってくれます。

app.phpのこの部分を見てください。

app.php
1
2
3
/** @var Hello $hello */
$hello = $container->get(Hello::class);

もし、コンストラクタインジェクションをphp-diが自動でしてくれなければ、
以下のようにいちいち依存性を自分で注入してあげないといけませんね。

app.php
1
2
3
4
5
6
$logger = new Logger('DI-App');
// $loggerを初期設定
...

$hello = new Hello($logger);

DIコンテナはHelloに必要な依存性をConstructorの引数から
自動的に判断して、LoggerクラスをHelloクラスに渡してくれます。

便利ですね。

機能3.Autowired

先ほどの例ですが、実はbootstrap.phpの$defを一切変更せずに動きます。

本来であればHelloクラスをどのようにnewするかは$defに記述しなければいけません。
しかし、php-diはそれも自動でやってくれます。

初期設定を必要としないHelloのようなクラスは、
Hello::classをコンテナに渡すことで勝手に作ってくれます。

これは便利ですね。

逆にLoggerのようにシンプルにnewするだけでなく、
設定などをしたいクラスは$defに記述する必要があります。

機能4.Interfaceの実装

先ほどの例でLoggerをMonologではなく別のライブラリに変えるような
場面を想定してみます。

普通であればLoggerを使っているクラスすべてを書き換えなければならず、
面倒でバグを生みやすい作業になるのですが、
Interfaceを使うことで解決できます。

MonologはPSR-3 LoggerInterfaceというPHP標準のロガーインターフェースに準拠しています。

bootstrap.phpの$defをもう一度見てみてください。

bootstrap.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 設定配列の宣言
$def = [
// ここでLoggerをnewする
Logger::class => function(){
$logger = new Logger('DI-App');
$logger->pushHandler(new StreamHandler(__DIR__.'/app.log'));
return $logger;
},
// LoggerInterfaceにLoggerを設定
LoggerInterface::class => function(ContainerInterface $c){
return $c->get(Logger::class);
}
];

この部分でPHPの標準インターフェースであるLoggerInterfaceにMonologを設定しています。

bootstrap.php
1
2
3
4
5
// LoggerInterfaceにLoggerを設定
LoggerInterface::class => function(ContainerInterface $c){
return $c->get(Logger::class);
}

こうすることで、クラス内でLoggerInterfaceを依存性として宣言した場合、
Monologを自動的にインターフェースの実装として使ってくれます。

それではHelloクラスをMonologに依存しない形に変えてみます。

Hello.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App;

use Psr\Log\LoggerInterface;

class Hello {
private LoggerInterface $logger;
function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}

function sayHello(){
$this->logger->info("Hello");
}
}

こうすることでLoggerをほかのPSR-3準拠のLoggerに変更する際に、
Helloクラスを変更する必要がなくなりました。

LoggerやMailerなど標準Interfaceが用意されているものや、
テスト用にMockを使いたい場合などに役立ちますね。

アンチパターンなど

最後にアンチパターンやTipsを紹介します。

Dependency Fetch アンチパターン

基本的に$container->get()を呼び出すのは最小限にするようにしましょう。

すべてのクラスが$container変数を持っているようなコードはDependency Injectionではなく
Dependency Fetchというアンチパターンになってしまいます。

以下のような感じです。

Hello.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App;

use Monolog\Logger;

class HelloAnti {

private Logger $logger;

function __construct() {
$container = require __DIR__.'/bootstrap.php'; //直接コンテナを触っている
$this->logger = $container->get(Logger::class);
}

function sayHello(){
$this->logger->info("Hello Anti");
}
}

コンテナから直接、依存性を取り出すのではなく、
コンストラクタインジェクションや@injectアノテーションを使うことが推奨されています。

make()メソッド

php-diを使うときはコンストラクタに依存性を記述しますが、
普通コンストラクタって引数を記述するところですよね。

普通の引数と依存性のための引数を使うクラスの場合、get()ではなく、
make()メソッドを使用します。

例えば、Helloクラスに言語という引数を足してみます。
以下のようなコードです。

HelloMake.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App;

use Monolog\Logger;

class HelloMake {
private Logger $logger;
private string $lang;

function __construct(Logger $logger, string $lang) { //引数に言語をとる
$this->logger = $logger;
$this->lang = $lang;
}

function sayHello(){
$hello = "hello";
if($this->lang === 'jp')
$hello = "こんにちは";

$this->logger->info($hello);
}
}

このようなクラスを呼ぶ場合はmake()メソッドを使用します。

app.php
1
2
3
4
5
6
7
8
9
10
11
<?php

use App\HelloMake;

$container = require __DIR__.'/bootstrap.php';

/** @var HelloMake $hello */
$hello = $container->make(HelloMake::class, ['lang' => 'jp']); //make メソッド

$hello->sayHello();

また、このようなクラスを依存性に持つ場合は、
コンストラクタにDI\FactoryInterfaceをとり、そこからmake()を呼び出すことが推奨されています。
これは先ほどのアンチパターン($containerをクラスが直接持つ)にならないようにするためですね。

まとめ

この記事ではphp-diを使ってDIやDIコンテナの使用例を紹介してきました。
DIを知らなかった人やフレームワークに入っているものを何となく使っていた人の
理解が深まると幸いです。

それじゃ今日はこの辺で。

関連記事

こちらの記事もおすすめです。

Spring Boot + Azure App Engine +Cosmos DBでAPIを無料で爆速開発する - その2