PHPのモジュールシステムを整理してみた ~require_onceからcomposerオートローダーまで~


何かとPHPを使うことが増えたので、PHPのモジュールシステムについて整理してみました。ここで言うモジュールシステムとは、他ファイルからクラス、関数、定数などを取り込める機能のことです。

TL;DR

ぶっちゃけcomposerオートローダーがを使えばいいと思います。
極論ですがcomposerがないPHPはレガシーPHPです。

PHPモジュールシステム解説

この記事で使うコードは、全てgithubに公開されており、実際に動かすことができます。サンプルコードを動かすためには、ローカル環境にPHP(7系統)、composerがインストールされている必要があります。

サンプルコード
https://github.com/kaidouji85/php-module

1 require_onceをシンプルに使う

対応するサンプルコード
https://github.com/kaidouji85/php-module/tree/master/01-require

main.phpの中で、greeting.phpに定義されたmessage関数を呼び出しています。これだけシンプルなら問題ありませんが、ファイルが増えてくると名前が衝突する危険があります。それがどういう意味なのかは、次のセクションで説明します。

2 名前衝突でエラーになる

対応するサンプルコード
https://github.com/kaidouji85/php-module/tree/master/02-name-conflict

main.phpでgreeting.php、secret.phpを読み込んでいますが、両方のファイルで同じ名前の関数が定義されています。この場合、message関数が2個定義されたことになりますが、PHPでは関数、クラス、定数などを二重定義するとエラーになります。したがって、このコードは動きません。仮にPHPで二重定義がOKだったとしても、secret-word.php、greeting.phpのどちらのmessageを呼び出したのか区別することが出来ません。

開発規模が大きくなると、違うファイルで同じ名前のものが沢山出てくると思います。全モジュールで被らない名前を考えるのは、規模が大きくなるほど現実的でないです。この問題を解決するために、名前空間という機能が用意されています。

3 名前空間で二重定義を回避する

対応するサンプルコード
https://github.com/kaidouji85/php-module/tree/master/03-name-space

main.php
<?php
require_once("greeting.php");
require_once("secret-word.php");

print(\SecretWord\message());
secret-word.php
<?php
namespace SecretWord;

function message() {
    return "secret word!!";
}

名前空間とは、greeting.php、secret-word.phpを仮想的なフォルダのようにものに入れて名前が被るのを防ぐ機能です。main.phpを見ると\SecretWord\message()としていますが、これは名前空間SecretWordに含まれるmessage関数を呼び出すという意味です。名前空間SecretWordにはmessageという関数は1つしか含まれていないので、二重定義エラーにはなりません。しかし、名前空間内に同じ名前が存在した場合には、二重定義エラーになります。

名前空間はParent\child\grandchildというように、階層構造を持つことができます。ただ、名前空間の階層が深くなると、その分コードがが冗長になります。これを解決するために、PHPにはuseという文法があります。

4 useで名前空間配下のものをシンプルに呼び出す

対応するサンプルコード
https://github.com/kaidouji85/php-module/tree/master/04-use

main.php
<?php
require_once("greeting.php");
require_once("secret-word.php");

use function \SecretWord\message;

print(message());

main.phpを見るとmessage()と名前空間なしで関数を呼び出しています。ただ、名前空間指定なしで、どのようにしてgreeting.php、secret.phpの区別をしているのでしょうか。答えはmain.php 5行目のuse function \SecretWord\message;です。useであらかじめ利用するものを名前空間込みで書いておけば、以降は名前空間なしで呼ぶことができます。

これで全ての問題が解決したように思えますが、そんなことはありません。開発規模が大きくなると名前空間の階層が深くなることは先に述べた通りですが、名前空間の管理方法を決めておかないと意図しない二重定義が発生する恐れがあります。また、useを使えばコードはスッキリしますが、毎回require_once、useを書かなければいけません。この問題を解決するために、PHPではcomposerオートロードという仕組みを導入しました。

5 composerオートロード

対応するサンプルコード
https://github.com/kaidouji85/php-module/tree/master/05-composer

composer.json
{
    "name": "phpModule",
    "authors": [
        {
            "name": "y.takeuchi",
            "email": "[email protected]"
        }
    ],
    "autoload": {
        "psr-4": {
            "App\\" : "app/"
        }
    },
    "require": {}
}
main.php
<?php
require_once("vendor/autoload.php");
use App\SecretWord;

print(SecretWord::message());
app/SecretWord.php
<?php
namespace App;

class SecretWord
{
    public static function message() {
        return "secret word!!";
    }
}

autoload.php

構成が一気に変わりました。最初に目につくのはvendor/autoload.php(以下autoload.php)でしょうか。autload.phpはプログラマーが書くのではなく、以下コマンドで自動生成するものです。

composer dump-autoload
# vendor/autoload.phpが自動生成される
# ほかにも必要なファイルが自動生成される

autload.phpはプロジェクトで使うphpファイルを全てrequire_onceするためのものです。プログラムを起動したら一番初めに実行されるファイルいわゆるエントリポイント内で、require_once("vendor/autoload.php");と書きます。するとautoload.phpが必要なphpファイルを全て読み込んでくれるので、エントリポイント以外でrequire_onceを書く必要がなくなります。エントリポイント以外ではuseを使っていきます。autoload.phpはcomposerで自動生成するべきものなので、git管理に含めてはいけません。

PSR-4

autoload.phpが必要なPHPファイルの読み込みをやってくれるのは分かりましたが、新しいPHPファイルを追加した場合はどうなるのでしょうか。再度composer dump-autoloadを実行すればよい気もしますが、それだとファイル構成に変化があるたびにコマンド再実行をしなければなりません。はっきり言って、面倒くさいです。この問題を解決するために、composerはファイルを配置する場所、名前空間、クラス名が一定のルールに沿っていれば、ファイル構成の変更を自動的に反映してくれる仕組みを備えています。このルールはPSR-4と呼ばれています。

PSR-4
https://www.php-fig.org/psr/psr-4/

細かいところは省きますが、PSR-4では名前空間とファイルパスの一部が完全一致しています。また、名前空間とソースコードのルートディレクトリの関連付けがcomposer.jsonに定義されています。さらにファイル名から.phpを除いたものクラス名も完全一致しています。これらの規約を満たしていれば、autoload.phpに自動的に取り込まれます。余談ですが、PSR-4に準拠していればVS CodeなどのIDEでのコード補完が抜群に良くなりました。

6 オートロードで多層階層をスマートに

対応するサンプルコード
https://github.com/kaidouji85/php-module/tree/master/06-multi-layer

最後に呼び出し階層が複数に渡る例を紹介します。ファイル構成が複雑なので、クラス図を用意しました。Greetingをインタフェース化しているのにSecretWordでそれを呼び出していないのが微妙ですが、そこはご容赦ください。全体構成は複雑ですが、単体ソースコードのレベルではモジュール読込がとても分かりやすくなっています。エントリポイントはrequire_once("vendor/autoload.php");が書いてありますが、それ以外のモジュール読込はすべてuse XXXXXXXXXで統一されています。一般的にエントリポイントでは数行で済ませることが多いので、実質的にモジュール読込はuse xxxxxxxxxだけで出来るようになりました。

結論

よほどの理由がない限り、composer オートローダを使うのがいいと思います。以下がその理由です。

  • モジュール読込がuse XXXXX形式に統一できる
  • 基本はPSR-4に準拠しているので名前空間の事故が起こりにくい
  • 副次効果としてIDE補完が効きやすくなる