本番環境をPHP7.4にアップグレードした後、Laravelで Trying to access array offset on value of type … を消せてなかった話

顧問IT業者の必要性と選び方

概要

\Illuminate\Foundation\Bootstrap\HandleExceptionserror_reporting が上書きされて php.ini での設定が無視されるので上書きし返した話

本文

PHP 8.0がリリースされて4ヶ月が経ちました。Laravel本体はPHP 8.0に対応しているそうなので、もう少し様子見をして問題なさそうであれば新規プロジェクトから入れていきたいですね。 match 式が待ち遠しいです。

ところで、本記事は PHP 7.4 とLaravelの error_reporting に関する挙動についてになります。

PHP 7.4 で null や数値への配列アクセスが E_NOTICE レベルの警告が出るようになりました。Laravelでは hasOne リレーションの先のモデルに対してうっかり配列アクセスしたりすると出たりします(とくに、この場合はDB内容に依存して出たり出なかったり……)

ある環境でPHPのバージョンを7.3から7.4に上げるアップデートを行い、マネージャーに確認して本番環境のみ php.ini の error_reporting 設定で E_NOTICE を上げないように設定しました。このときは一旦エラーを出さないことを優先し、開発環境側で順次修正する計画としました。
(本来ならテスト自動化と静的解析とを用いて発見、修正するのが正しいアプローチ、だと思います。特にこの警告に関しては PHP 8.0 で E_WARNING レベルに上がることが分かっているので、次回もこのアプローチを使って警告を握りつぶす範囲が広がるのは避けたいところです)

ところが、ブラウザからWebアプリケーションを開いてみると消したはずの Trying to access array offset on value of type null 警告が出ます。コードの該当箇所では E_NOTICE をエラーとして上げているようです。

(error_reporting() & E_NOTICE) !== 0

調査すると、 \Illuminate\Foundation\Bootstrap\HandleExceptionsfunction bootstrap($app)

error_reporting(-1);

と設定されていることが分かりました。

さいわい、\App\{Http,Console}\Kernel$bootstrappers を変更することで HandleExceptions を差し替えられそうだったので、

    protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \App\HandleExceptions::class, // ここで差し替え
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ];
<?php

namespace App;

use Illuminate\Contracts\Foundation\Application;

/**
 * Class HandleExceptions
 * @package App
 *
 * php.ini で error_reporting = ... & ~E_NOTICE と設定されていた場合に、
 * Laravelによって上書きされた error_reporting を再度上書きして ... & ~E_NOTICE する
 */
class HandleExceptions extends \Illuminate\Foundation\Bootstrap\HandleExceptions {

    public function bootstrap(Application $app)
    {
        $php_error_reporting = error_reporting();
        parent::bootstrap($app);
        $laravel_error_reporting = error_reporting();

        // php.ini で ~E_NOTICE されていた場合
        if (!($php_error_reporting & E_NOTICE)) {
            // parent::bootstrap($app) 内で error_reporting(-1); されるので、
            // E_NOTICE を再度除外
            error_reporting($laravel_error_reporting & ~E_NOTICE);
        }
    }
}

と変更し、E_NOTICE がエラーにならないようになりました。

(このコードだとLaravelインスタンスごとに変更できず php.ini の変更でまとめて変わってしまうので、実際にはconfigも読むようにしました)