PHP-DIとはなんなのか

採用はこちら

PHP-DIとは、PHPで依存性注入、Dependency Injection / DIを実現しやすくするためのDIコンテナライブラリです。

簡単に言うと、PHP-DIは「クラスが必要とするオブジェクトを自動的に組み立て、必要な場所に渡してくれる仕組み」です。

PHPアプリケーションが大きくなると、Controller、Service、Repository、Mailer、Logger、Database Connectionなど、多くのクラスが互いに依存するようになります。

これらを毎回手動で new して組み立てていると、コードが複雑になり、保守しづらくなります。

PHP-DIを使うと、そうした依存関係の組み立てをコンテナに任せることができます。

目次

DIとPHP-DIの違い

DIは設計パターン

DIとは、Dependency Injectionの略で、日本語では「依存性注入」と呼ばれます。

これはライブラリ名ではなく、設計パターンの一種です。

DIの基本的な考え方は、クラスが必要とする部品を自分で作らず、外から受け取ることです。

たとえば、次のようなコードがあるとします。

class UserService
{
    private Mailer $mailer;

    public function __construct()
    {
        $this->mailer = new Mailer();
    }

    public function register(string $email): void
    {
        $this->mailer->send($email, '登録完了');
    }
}

このコードでは、UserService の中で new Mailer() を実行しています。

一見問題なさそうに見えますが、この設計では UserServiceMailer の作り方まで知ってしまっています。

その結果、テスト時に別のメール送信クラスへ差し替えにくくなったり、メール送信の実装を変更したときに UserService にも影響が出やすくなったりします。

DIを使うと、次のように書けます。

class UserService
{
    public function __construct(
        private Mailer $mailer
    ) {}

    public function register(string $email): void
    {
        $this->mailer->send($email, '登録完了');
    }
}

この場合、UserServiceMailer を自分で作りません。外から受け取るだけです。

$mailer = new Mailer();
$userService = new UserService($mailer);

これが依存性注入です。

PHP-DIはDIを実現しやすくするコンテナ

PHP-DIは、DIという設計をPHPで扱いやすくするためのライブラリです。

DIそのものは、PHP-DIがなくても実現できます。

しかし、アプリケーションの規模が大きくなると、依存オブジェクトを手動で組み立てるコードが増えていきます。

たとえば、次のようなコードです。

$pdo = new PDO($dsn, $user, $password);
$userRepository = new UserRepository($pdo);
$mailer = new Mailer($config);
$userService = new UserService($userRepository, $mailer);
$userController = new UserController($userService);

小さなアプリケーションなら問題ありません。

しかし、クラスが増えるほど、どこで何を作るのかが複雑になります。

PHP-DIを使うと、このようなオブジェクトの組み立てをコンテナに任せられます。

$userController = $container->get(UserController::class);

この1行で、UserController に必要な UserService、さらにその先に必要な UserRepositoryMailer などを、定義や型情報に基づいて解決してくれます。

PHP-DIを使う目的

newの散らばりを減らす

PHP-DIを使う大きな目的の1つは、アプリケーション内に散らばった new を減らすことです。

もちろん、new 自体が悪いわけではありません。

問題は、ビジネスロジックやControllerの中で、依存オブジェクトの生成処理まで抱え込んでしまうことです。

たとえば、次のようなControllerは多くの責務を持ちすぎています。

class ContactController
{
    private ContactService $service;

    public function __construct()
    {
        $pdo = new PDO(
            'mysql:host=localhost;dbname=app',
            'root',
            'password'
        );

        $repository = new ContactRepository($pdo);
        $mailer = new SmtpMailer('smtp.example.com', 587);
        $this->service = new ContactService($repository, $mailer);
    }

    public function submit(array $data): void
    {
        $this->service->submit($data);
    }
}

このコードでは、ContactController が次のようなことまで知っています。

  • DB接続の作り方
  • Repositoryの作り方
  • Mailerの作り方
  • Serviceの作り方

本来、Controllerの役割はリクエストを受け取り、適切な処理へ渡すことです。

依存オブジェクトの作成は、Controllerの責務ではありません。

PHP-DIを使うと、Controllerは次のようにシンプルになります。

class ContactController
{
    public function __construct(
        private ContactService $service
    ) {}

    public function submit(array $data): void
    {
        $this->service->submit($data);
    }
}

依存オブジェクトの組み立てはPHP-DIに任せ、クラス本体は本来の責務に集中できます。

テストしやすくする

PHP-DIを使うと、依存オブジェクトを差し替えやすくなります。

たとえば、メール送信を行うクラスがあるとします。

interface MailerInterface
{
    public function send(string $to, string $subject, string $body): void;
}

本番環境ではSMTPを使う実装を利用します。

class SmtpMailer implements MailerInterface
{
    public function send(string $to, string $subject, string $body): void
    {
        // SMTPでメール送信
    }
}

一方、テスト環境では実際にメールを送信したくありません。

その場合、テスト用の実装を用意できます。

class FakeMailer implements MailerInterface
{
    public array $sent = [];

    public function send(string $to, string $subject, string $body): void
    {
        $this->sent[] = compact('to', 'subject', 'body');
    }
}

Service側は、具体的な SmtpMailer ではなく、MailerInterface に依存します。

class RegistrationService
{
    public function __construct(
        private MailerInterface $mailer
    ) {}
}

PHP-DIの定義を環境ごとに切り替えれば、本番では SmtpMailer、テストでは FakeMailer を使えます。

return [
    MailerInterface::class => DI\get(SmtpMailer::class),
];

このように、PHP-DIを使うことで、テストしやすく、実装を差し替えやすい設計にできます。

クラスの責務を明確にする

PHP-DIを使うと、クラスの責務が明確になります。

クラスは「何をするか」に集中し、依存オブジェクトの作り方はコンテナに任せます。

たとえば、UserService はユーザーに関する処理を行うクラスです。

DB接続の作り方やメール送信クラスの生成方法まで知る必要はありません。

class UserService
{
    public function __construct(
        private UserRepository $repository,
        private MailerInterface $mailer
    ) {}
}

このように依存関係をコンストラクタに明示すると、そのクラスが何に依存しているのかがコード上でわかりやすくなります。

PHP-DIの基本的な使い方

Composerでインストールする

PHP-DIはComposerでインストールできます。

composer require php-di/php-di

インストール後、PHP-DIのコンテナを作成して利用します。

use DI\Container;

$container = new Container();

コンテナからクラスを取得する

PHP-DIでは、コンテナからクラスを取得できます。

$userService = $container->get(UserService::class);

このとき、UserService のコンストラクタに依存クラスが定義されていれば、PHP-DIは可能な範囲で自動的に依存関係を解決します。

class UserService
{
    public function __construct(
        private UserRepository $repository,
        private Mailer $mailer
    ) {}
}

UserRepositoryMailer が通常のクラスで、さらにその依存関係も解決可能であれば、PHP-DIは自動でインスタンスを作成して注入します。

Autowiringとは

型宣言から依存関係を自動解決する仕組み

PHP-DIの重要な機能の1つがAutowiringです。

Autowiringとは、コンストラクタの型宣言を見て、必要な依存オブジェクトを自動で解決する仕組みです。

たとえば、次のようなクラスがあります。

class OrderService
{
    public function __construct(
        private OrderRepository $repository,
        private PaymentGateway $paymentGateway
    ) {}
}

PHP-DIは、このコンストラクタを見て、OrderService には OrderRepositoryPaymentGateway が必要だと判断します。

そして、それらのクラスも作成可能であれば、自動的に生成して OrderService に渡します。

Autowiringで解決できないもの

Autowiringは便利ですが、万能ではありません。

特に、次のような値は自動解決できません。

class PaymentGateway
{
    public function __construct(
        private string $apiKey
    ) {}
}

string $apiKey という型情報だけでは、どの文字列を渡せばよいのか判断できません。

同じように、次のような値も自動では判断できません。

  • DB接続文字列
  • DBユーザー名
  • DBパスワード
  • APIキー
  • 環境ごとの設定値
  • intbool などのスカラー値
  • インターフェースに対する具体的な実装

このような場合は、PHP-DIに明示的な定義を渡します。

return [
    PaymentGateway::class => DI\create(PaymentGateway::class)
        ->constructor('your-api-key'),
];

PHP-DIの定義とは

定義は「作り方のルール」

PHP-DIにおける定義とは、「このクラスや値はこう作ってください」というルールです。

たとえば、PDO はDB接続情報が必要なため、PHP-DIが自動では作れません。

そのため、次のように定義します。

return [
    PDO::class => function () {
        return new PDO(
            'mysql:host=localhost;dbname=my_app;charset=utf8mb4',
            'root',
            'password'
        );
    },
];

この定義があれば、PHP-DIは PDO::class が必要になったときに、この関数を使って PDO インスタンスを作成できます。

ContainerBuilderを使った定義の読み込み

実務では、定義を専用ファイルに分けることが多いです。

たとえば、config/di.php に定義を書きます。

use function DI\get;

return [
    PDO::class => function () {
        return new PDO(
            $_ENV['DB_DSN'],
            $_ENV['DB_USER'],
            $_ENV['DB_PASSWORD']
        );
    },

    MailerInterface::class => get(SmtpMailer::class),
];

そして、アプリケーションの起動時に読み込みます。

use DI\ContainerBuilder;

$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/config/di.php');

$container = $builder->build();

これで、PHP-DIは config/di.php に書かれたルールに従って依存関係を解決します。

インターフェースとPHP-DI

インターフェースは自動解決できない

PHP-DIは、通常のクラスであればAutowiringによって自動解決できます。

しかし、インターフェースはそのままではインスタンス化できません。

たとえば、次のようなServiceがあるとします。

class RegistrationService
{
    public function __construct(
        private MailerInterface $mailer
    ) {}
}

この場合、PHP-DIは MailerInterface が必要だということはわかります。

しかし、MailerInterface の実装として SmtpMailer を使うのか、SendGridMailer を使うのか、FakeMailer を使うのかは判断できません。

そのため、明示的に定義します。

return [
    MailerInterface::class => DI\get(SmtpMailer::class),
];

これにより、MailerInterface が必要な場所には SmtpMailer が注入されます。

実装を差し替えやすくなる

インターフェースを使うと、実装の差し替えが簡単になります。

本番環境ではSMTPを使う場合は次のようにします。

return [
    MailerInterface::class => DI\get(SmtpMailer::class),
];

テスト環境ではFake実装に差し替えます。

return [
    MailerInterface::class => DI\get(FakeMailer::class),
];

このように、Service側のコードを変更せずに、利用する実装だけを切り替えられます。

これはPHP-DIを使う大きなメリットの1つです。

PHP-DIで使われる主な機能

Autowiring

Autowiringは、コンストラクタの型宣言から依存関係を自動的に解決する機能です。

単純なクラス依存であれば、明示的な定義を書かなくてもPHP-DIが解決してくれます。

ただし、スカラー値やインターフェースなどは明示的な定義が必要です。

PHP definitions

PHP definitionsは、PHP配列を使って依存関係の定義を書く方法です。

return [
    LoggerInterface::class => DI\get(FileLogger::class),
    CacheInterface::class => DI\get(RedisCache::class),
];

実務では、このPHP definitionsを使って設定を書くことが多いです。

Attributes

PHP-DIでは、PHP 8のAttributesを使った定義も可能です。

Attributesを使うと、クラスやコンストラクタの近くに注入設定を書けます。

ただし、環境ごとに変わる設定や、インターフェースと実装の対応関係などは、定義ファイルにまとめた方が見通しがよい場合もあります。

PSR-11対応

PHP-DIはPSR-11に対応しています。

PSR-11は、PHPのコンテナインターフェースに関する標準仕様です。

基本的には、次のようなAPIを提供します。

$container->get($id);
$container->has($id);

PSR-11に対応していることで、SlimなどのPSR-11対応フレームワークやライブラリと組み合わせやすくなります。

PHP-DIのオブジェクトライフサイクル

定義は要求されたときに解決される

PHP-DIでは、定義を書いたからといって、すぐにすべてのオブジェクトが生成されるわけではありません。

定義はあくまで「作り方のルール」です。

実際にオブジェクトが必要になったとき、つまり get() で取得されたときや、別のクラスの依存として必要になったときに解決されます。

$userService = $container->get(UserService::class);

この時点で、UserService とその依存オブジェクトが解決されます。

同じインスタンスが再利用される点に注意する

PHP-DIでは、一度解決された定義の結果は、基本的にコンテナの存続期間中保持されます。

たとえば、次のように同じクラスを2回取得した場合、同じインスタンスが返る前提で考える必要があります。

$a = $container->get(UserService::class);
$b = $container->get(UserService::class);

毎回新しいインスタンスが必要な場合は、make() やFactoryを使う設計を検討します。

この点は、PHP-DIを実務で使ううえで重要です。

PHP-DIを使うときの注意点

コンテナをあちこちで直接呼ばない

PHP-DIを使い始めると、どこでも次のように書きたくなることがあります。

$service = $container->get(UserService::class);

しかし、アプリケーションの内部でコンテナを直接呼び出しすぎるのは避けた方がよいです。

たとえば、次のようなコードは望ましくありません。

class UserController
{
    public function __construct(
        private ContainerInterface $container
    ) {}

    public function index(): void
    {
        $service = $this->container->get(UserService::class);
        $service->doSomething();
    }
}

このような書き方をすると、クラスの依存関係が見えにくくなります。

望ましいのは、必要な依存をコンストラクタで明示的に受け取る形です。

class UserController
{
    public function __construct(
        private UserService $service
    ) {}

    public function index(): void
    {
        $this->service->doSomething();
    }
}

コンテナは、アプリケーションの入口やフレームワークとの接続部分で使うのが基本です。

各クラスの中では、コンテナではなく必要な依存オブジェクトそのものを受け取るようにします。

Autowiringに任せすぎない

Autowiringは便利ですが、すべてを自動解決に任せると、アプリケーションとしての意図が見えにくくなることがあります。

特に、次のようなものは明示的に定義した方がよいです。

return [
    LoggerInterface::class => DI\get(MonologLogger::class),
    CacheInterface::class => DI\get(RedisCache::class),
    MailerInterface::class => DI\get(SmtpMailer::class),
];

単純なクラス依存はAutowiringに任せ、アプリケーションとして選択が必要な部分は定義に書く、という使い分けが重要です。

コンストラクタの引数を増やしすぎない

DIを使うと、依存オブジェクトをコンストラクタに並べる設計になります。

しかし、コンストラクタ引数が多すぎる場合、そのクラスが多くの責務を持ちすぎている可能性があります。

class UserService
{
    public function __construct(
        UserRepository $users,
        OrderRepository $orders,
        Mailer $mailer,
        Logger $logger,
        Cache $cache,
        PaymentClient $payment,
        AnalyticsClient $analytics,
        CsvExporter $exporter
    ) {}
}

このような場合は、PHP-DIの設定を見直すよりも、クラス設計そのものを見直すべきです。

責務を分割し、より小さなServiceに分けることで、コードの見通しがよくなります。

グローバルな箱として使わない

PHP-DIのコンテナは便利ですが、「何でも入っているグローバルな箱」として使うべきではありません。

必要になったらどこからでもコンテナから取り出す、という使い方をすると、依存関係が隠れてしまいます。

良い使い方は、必要な依存をコンストラクタで明示することです。

class ArticleService
{
    public function __construct(
        private ArticleRepository $articles,
        private LoggerInterface $logger
    ) {}
}

この形なら、ArticleService が何に依存しているのかがすぐにわかります。

PHP-DIが向いているケース

素のPHPアプリケーション

PHP-DIは、フレームワークを使わない素のPHPアプリケーションで特に役立ちます。

ファイルが増え、ServiceやRepositoryが増えてくると、依存関係の管理が複雑になります。

PHP-DIを使うことで、オブジェクト生成のルールを整理しやすくなります。

Slimなどの軽量フレームワーク

PHP-DIは、Slimのような軽量フレームワークとも相性がよいです。

SlimではPSR-11対応コンテナを利用できるため、PHP-DIを組み合わせることで、ControllerやActionクラスに依存オブジェクトを注入しやすくなります。

レガシーPHPのリファクタリング

既存のPHPアプリケーションで、クラスの中に new が大量に散らばっている場合、PHP-DIはリファクタリングの助けになります。

いきなり全体を作り替えるのではなく、ServiceやRepositoryなどの単位から少しずつDI化していくことで、テストしやすく保守しやすい構造へ移行できます。

独自フレームワークやCLIツール

独自フレームワークやCLIツールでも、PHP-DIは有効です。

Webアプリケーションに限らず、バッチ処理、コマンドラインツール、クローラー、社内ツールなどでも、依存関係の管理が必要になることがあります。

そのような場面でも、PHP-DIを使うことで構成を整理しやすくなります。

PHP-DIが不要なケース

小さなスクリプト

非常に小さなスクリプトであれば、PHP-DIを導入する必要はありません。

数個の関数やクラスだけで完結する処理なら、手動でインスタンスを作成した方がシンプルです。

LaravelやSymfonyを使っている場合

LaravelにはLaravelのサービスコンテナがあり、SymfonyにはSymfonyのDIコンテナがあります。

そのため、LaravelやSymfonyのアプリ本体では、それぞれの標準コンテナを使うのが自然です。

もちろん、フレームワーク外の独自ツールや共通ライブラリでPHP-DIを使う可能性はあります。

ただし、通常のアプリケーション開発では、既存フレームワークの仕組みに乗る方が保守しやすいです。

チームがDIに慣れていない場合

DIやDIコンテナに慣れていないチームでは、導入直後にコードがかえって複雑に見えることがあります。

PHP-DIを導入する場合は、コンテナを直接呼び出しすぎないこと、インターフェースと実装の対応を明示すること、Autowiringに任せすぎないことなど、基本的なルールをチームで共有しておくとよいです。

実務での典型的な構成

ディレクトリ構成の例

PHP-DIを使う場合、次のような構成が考えられます。

app/
  Controller/
    UserController.php
  Service/
    UserService.php
  Repository/
    UserRepository.php
  Contract/
    UserRepositoryInterface.php

config/
  di.php

public/
  index.php

public/index.phpの例

アプリケーションの入口でコンテナを作成します。

use DI\ContainerBuilder;

require __DIR__ . '/../vendor/autoload.php';

$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../config/di.php');

$container = $builder->build();

$controller = $container->get(App\Controller\UserController::class);
$controller->index();

config/di.phpの例

依存関係の定義は、設定ファイルにまとめます。

use function DI\get;

return [
    PDO::class => function () {
        return new PDO(
            $_ENV['DB_DSN'],
            $_ENV['DB_USER'],
            $_ENV['DB_PASSWORD']
        );
    },

    App\Contract\UserRepositoryInterface::class =>
        get(App\Repository\PdoUserRepository::class),
];

Controllerの例

Controllerは、必要なServiceをコンストラクタで受け取ります。

namespace App\Controller;

use App\Service\UserService;

class UserController
{
    public function __construct(
        private UserService $userService
    ) {}

    public function index(): void
    {
        $users = $this->userService->listUsers();

        // レスポンス生成
    }
}

Serviceの例

Serviceは、Repositoryのインターフェースに依存します。

namespace App\Service;

use App\Contract\UserRepositoryInterface;

class UserService
{
    public function __construct(
        private UserRepositoryInterface $users
    ) {}

    public function listUsers(): array
    {
        return $this->users->findAll();
    }
}

このようにすると、ControllerやServiceはコンテナを直接知る必要がありません。

依存関係はコンストラクタで明示され、具体的な実装の選択は config/di.php に集約されます。

PHP-DIを理解するための重要ポイント

PHP-DIはnewを完全になくすものではない

PHP-DIは、new を完全に消すための魔法ではありません。

正確には、オブジェクト生成の責務をアプリケーションの入口や設定ファイルに集約するための仕組みです。

ビジネスロジックの中で依存オブジェクトを直接作るのではなく、コンテナに組み立てを任せます。

作れるものはAutowiringに任せる

単純なクラス依存であれば、Autowiringに任せるとコードがシンプルになります。

class OrderService
{
    public function __construct(
        private OrderRepository $repository
    ) {}
}

このような依存関係は、PHP-DIが自動で解決しやすいです。

選ぶべきものは定義に書く

一方で、アプリケーションとして選択が必要な部分は、明示的に定義します。

return [
    MailerInterface::class => DI\get(SmtpMailer::class),
    CacheInterface::class => DI\get(RedisCache::class),
];

「作れるものは自動で作らせる。選ぶべきものは明示する。」

この考え方が、PHP-DIを実務で使ううえで重要です。

まとめ

PHP-DIは、PHP向けのDIコンテナです。

DIとは、クラスが必要な依存オブジェクトを自分で作らず、外から受け取る設計パターンです。

PHP-DIは、そのDIを実用的に扱うために、依存関係の解決やオブジェクト生成を支援してくれます。

PHP-DIを使うことで、new の散らばりを減らし、クラスの責務を明確にし、テストしやすく、実装を差し替えやすいコードにできます。

ただし、PHP-DIは万能ではありません。Autowiringで解決できるのは、主に型宣言から判断できる依存関係です。

インターフェースの実装選択、DB接続情報、APIキー、環境ごとの設定値、stringint などのスカラー値は、明示的に定義する必要があります。

PHP-DIは、素のPHP、Slim、独自フレームワーク、CLIツール、レガシーPHPのリファクタリングなどで特に有効です。

一方で、LaravelやSymfonyのように標準のDIコンテナを持つフレームワークでは、通常はそのフレームワークの仕組みを使う方が自然です。

PHP-DIを正しく使うポイントは、コンテナをアプリケーション内部で直接呼びすぎないことです。

各クラスは必要な依存をコンストラクタで受け取り、コンテナはアプリケーションの入口や設定部分で使うようにすると、保守しやすい設計になります。

以上、PHP-DIとはなんなのかについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次