エラーの内容
PHPアプリは正常に動いていた――ある日突然、そうでなくなる。ログを確認すると、こんなエラーが目に入る:
PHP Fatal error: Uncaught Error: Call to a member function getName() on null
in /var/www/html/app/Controllers/UserController.php:42
メソッド名はプロジェクトによって異なる。しかしパターンは変わらない――オブジェクトであるべき変数がnullを保持した状態でメソッドを呼び出してしまっている。PHPは即座に停止する。リカバリも部分的な出力もなく、ただ致命的エラーが発生するだけだ。
根本原因
代入からメソッド呼び出しまでの間のどこかで、変数からオブジェクトが失われている。ほとんどのケースは以下のいずれかに当てはまる:
- データベースクエリが結果を返さず(
nullまたは空)、確認せずにメソッドを呼び出した。 - ファクトリメソッドやDIコンテナが、バインドの欠如や設定ミスにより
nullを返した。 - クラスのプロパティが使用前に初期化されていなかった。
- ある処理ルートで関数が
nullを返すケースを見落としていた――json_decode()やpreg_match()でよくある。 - 例外を握りつぶすカスタムコンストラクタのせいで、
new ClassName()の呼び出しがサイレントに失敗した。
ステップ1:何がnullなのかを正確に特定する
推測はしないこと。PHPが報告した行に直接移動し、問題のある呼び出しの直前で変数をダンプする:
<?php
// エラーはここで発生している:
$user->getName();
// この直上に追加する:
var_dump($user); // NULLなら、それが原因だ。
die();
何がnullかを確認したら、それがどこで代入されたかを遡って調べる。問題の本質は、メソッド呼び出しの行ではなく、代入の行にある。
修正1:nullチェックでガードする
nullが正当な状態(ユーザーが存在しない、レコードが削除済みなど)である場合、明示的なチェックが適切な対処法だ:
<?php
$user = getUserById($id);
if ($user !== null) {
echo $user->getName();
} else {
echo 'User not found';
}
PHP 8.0以降を使用しているなら、nullsafe演算子で1行にまとめられる:
<?php
// $userがnullの場合、getName()は呼び出されずnullを返す
$name = $user?->getName();
echo $name ?? 'User not found';
nullsafe演算子はチェーン全体をショートサーキットする。オブジェクトがなければ、メソッドも呼ばれず、致命的エラーも起きない。
修正2:nullを返すクエリやデータソースを修正する
nullチェックは往々にして応急処置にすぎない。本質的な問いは「なぜデータソースが何も返さないのか」だ:
<?php
// 悪い例: findById()はIDが存在しない場合nullを返す
$user = User::findById($id);
$user->getName(); // $idが0、空文字、または削除済みレコードなら爆発する
// オプションA: findOrFail()はnullを返す代わりに適切な例外をスローする
$user = User::findOrFail($id); // 見つからない場合はModelNotFoundExceptionをスロー
$user->getName(); // 安全
// オプションB: デフォルトオブジェクトにフォールバックする
$user = User::findById($id) ?? new GuestUser();
$user->getName();
まずクエリのパラメータを確認しよう。空文字、ゼロ、削除済み行の古いIDが原因のケースがおよそ80%を占める。
修正3:オブジェクトのプロパティを使用前に初期化する
初期化されていないクラスプロパティは静かな罠だ。プロパティは存在している――ただ何かがセットするまでnullを保持しているだけだ:
<?php
class OrderProcessor
{
private ?Logger $logger = null; // 宣言されているが、インジェクトされていない
public function process(): void
{
$this->logger->log('Processing...'); // loggerがセットされていなければ致命的エラー
}
}
// 修正A: コンストラクタで依存関係を必須にする
class OrderProcessor
{
private Logger $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function process(): void
{
$this->logger->log('Processing...');
}
}
// 修正B: フォールバックで遅延初期化する
public function process(): void
{
if ($this->logger === null) {
$this->logger = new NullLogger();
}
$this->logger->log('Processing...');
}
修正4:制御できない関数のnull戻り値を処理する
標準ライブラリの関数は、想像以上に多くの人がここで痛い目を見る。json_decode()はその典型例だ:
<?php
// json_decode()はパース失敗時(不正なJSON、空文字など)にnullを返す
$data = json_decode($response);
$data->status; // $responseが不正なJSONなら致命的エラー
// 修正: 結果を使う前にjson_last_error()を確認する
$data = json_decode($response);
if (json_last_error() !== JSON_ERROR_NONE || $data === null) {
throw new \RuntimeException('Invalid JSON: ' . json_last_error_msg());
}
echo $data->status; // 安全
// PHP 8: preg_match()も正規表現エラー時にnullを返すことがある
$result = preg_match('/pattern/', $subject, $matches);
if ($result === false || $result === null) {
// 正規表現エラーを処理する
}
修正5:依存性注入・サービスコンテナの問題
LaravelやSymfonyでは、コンテナバインドの欠如がまさにこのエラーを引き起こす。コンストラクタのシグネチャは問題なく見えても、注入されるオブジェクトが登録されていないだけだ:
<?php
// Laravelのコントローラ
public function __construct(private UserRepository $repo)
{
// UserRepositoryにバインドがなければ$repoはnull
}
// AppServiceProviderで登録する:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->bind(UserRepository::class, EloquentUserRepository::class);
}
サービスバインドを変更した後は、キャッシュされたコンテナをクリアすること。古いキャッシュが残っていると、以前の(壊れた)設定が有効なままになる:
php artisan clear-compiled
php artisan optimize:clear
予防策
このエラーをコードベースから根絶するための4つの習慣:
- 厳格な戻り値の型指定。 nullが有効な戻り値でない場合は
: ?Userではなく: Userを宣言する。PHPは5回のコール下流での紛らわしい致命的エラーではなく、発生源でTypeErrorをスローしてくれる。 - strict_types=1。 すべてのファイルの先頭に
declare(strict_types=1);を追加する。型の不一致が積み重なる前に、早い段階で表面化する。 - 静的解析。 PHPStanをレベル5以上で使用すれば、1行も実行する前にnull参照を検出できる。Psalmも同様だ。どちらも実行時のオーバーヘッドはない。
- nullを返すな、例外をスローせよ。 処理を完了できないメソッドは例外をスローすべきだ。サイレントなnullはバグを隠蔽し、例外はバグを露わにする。
修正の確認
まず元の失敗を再現し、それが解消されたことを確認する:
# 1. エラーが発生した正確なシナリオを再現する
php artisan tinker
>>> $user = App\Models\User::find(99999); // 存在しないID
>>> $user?->getName(); // 例外をスローせず、nullを返すはず
# 2. 関連するテストを実行する
php artisan test --filter UserTest
# 3. エラーログがクリーンであることを確認する
tail -f /var/log/php/error.log
# またはLaravelの場合:
tail -f storage/logs/laravel.log
致命的エラーが消え、nullの経路が期待するフォールバック値を返していれば――作業完了だ。

