PHP のオートローダーでハマった話

こんにちは。沖縄移住組 IT エンジニアの勇大(@yudy1152)です。

最近は Laravel を触っています。

クラスを新しく追加したけれども、実行時に not found となり、どうもうまくオートロードしてくれなくてハマってしまったので、調査内容や解決方法を書き留めておこうと思います。

※ファイル構成やクラス名などはこの記事を書くにあたって簡易的にしたものです。

目次

not found and could not be autoloaded.

クラスやインターフェースを追加してテストを実行した時に、以下のようなエラーメッセージが出てしまいました。

Fatal error: Interface 'App\Test\TestInterfaceA' not found in /var/www/app/Test/TestA.php on line 5

静的解析ツール PHPStan を導入していたので実行してみたところ、以下のようなメッセージが出ました。(一部抜粋)

> phpstan analyse
Cannot load Xdebug - it was already loaded
Note: Using configuration file /var/www/phpstan.neon.
 59/59 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ---------------------------------------------------------------------
  Line   app/Test/TestA.php                                                    
 ------ ---------------------------------------------------------------------
         Class App\Test\TestInterfaceA not found and could not be autoloaded.  
 ------ ---------------------------------------------------------------------

理由は分からないけれども、オートロードしてくれないようで、以下のコマンドをとりあえず実行しました。

 $ composer dump-autoload

このコマンドを叩いた後、テストを実行すると正常に完了しました。

え?毎回実行しなきゃいけないの?

dump-autoload を実行した時にそれなりに時間がかかりました。(数十秒〜約1分)

クラスを新しく追加した時に毎回実行しないといけないのか、いやそんなことはないよね、何かしらの設定が足りてないのか間違ってるのか・・・?

ということで、調査し、解決できたので分かった事などを書きます。

ファイルの構成

ファイル構成は以下のようになっていました。

interface 2 つを TestInterfaces.php で定義。それぞれの interface を実装するクラスを 2 つ用意、という構成です。

  • ファイル構成
app
  ┗ Test
     ┣ TestA.php
     ┣ TestB.php
     ┗ TestInterfaces.php
namespace App\Test;

class TestA implements TestInterfaceA
{
    public function outputA()
    {
        echo 'Test A !!!';
    }
}
namespace App\Test;

class TestB implements TestInterfaceB
{
    public function outputB()
    {
        echo 'Test B !!!';
    }
}
  • TestInterfaces.php
namespace App\Test;

interface TestInterfaceA
{
    public function outputA();
}

interface TestInterfaceB
{
    public function outputB();
}

調査開始!

まず確認したことは、composer.json です。

autoload に関する部分は以下のように定義していて、app 以下はちゃんとオートロードしてくれるはずの設定になっています。

ふむふむ、PSR-4 の規約にのっとってオートロードしてくれるのね。その規約ってなんだろ?が、正直なところでした。

    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    },

次に確認したことは、vendor/composer/autoload_classmap.php でした。

$ composer dump-autoload

を実行する前と後でどう変わるのか、を確認したところ、実行後には以下のようなマッピングがファイル内に追加されていました。

     'App\\Test\\TestA' => $baseDir . '/app/Test/TestA.php',
     'App\\Test\\TestB' => $baseDir . '/app/Test/TestB.php',
     'App\\Test\\TestInterfaceA' => $baseDir . '/app/Test/TestInterfaces.php',
     'App\\Test\\TestInterfaceB' => $baseDir . '/app/Test/TestInterfaces.php',

ここで思ったことは、composer.json でオートロードするための設定があるのに、autoload_classmap.php にも定義が必要なの?です。


次に、そのクラスのみで完結する TestC を追加してみました。(継承などしないクラス)

namespace App\Test;

class TestC
{
    public function outputC()
    {
        echo 'Test C !!!';
    }
}

TestA, TestB はいったん無視し、TestC の function のみを実行してみたところ正常に完了しました。

TestC を追加した後、$ composer dump-autoload は実行していないので、autoload_classmap.php にはもちろん TestC に関する定義はありません。

設定などの環境まわりというよりも、クラス内の構成について疑いが・・・

ここでようやっと基本に立ち返り、composer.json 内の autoload で指定している PSR-4 について調べました。

結論:ファイル名とクラス名を同じにしよう!

PSR-4 について参考にしたサイトがこちらです。

www.ritolab.com

私が今回ハマった内容に該当する箇所は、「ファイルのロード」項目の 4. でした。

4. ファイル名は終端クラス名の大文字と一致する必要があります。

このルールに沿って TestInterfaces.php 内に定義していた各 interface を、インターフェース名と同じファイルを用意し、以下のような構成に作り替えました。

  • ファイル構成
app
  ┗ Test
     ┣ TestA.php
     ┣ TestB.php
     ┣ TestInterfaceA.php
     ┗ TestInterfaceB.php
  • TestInterfaceA.php
interface TestInterfaceA
{
    public function outputA();
}
  • TestInterfaceB.php
interface TestInterfaceB
{
    public function outputB();
}

作り替えた後、autoload_classmap.phpAPP\Test に関する定義が存在しないことを確認し、テスト実行をすると、無事に正常完了することができました!

まとめ

  • PSR に興味を持ちました

    • 今までは、PHP について調べた時に、PSR の内容が書かれている記事がヒットして参考にする程度でした。 PSR を必ず守らないとダメ!というものでもないようですが、どんな内容なのか一度熟読してみようと思います。
  • PSR に興味を持つついでに PHP もちゃんと勉強しよう!と思いました

参考サイト