【PHP9】ついに未定義変数が使えなくなる


ついに来るべき時が来ました。

	echo $undefined;

PHP9以降、致命的エラーになります。

以下は該当のRFC、Undefined Variable Error Promotionの紹介です。

投票期間は2022/03/14から2022/03/28です。
2022/03/21時点では賛成30反対6の賛成多数であり、ほぼ確実に可決されます。

Undefined Variable Error Promotion

Introduction

未定義変数とは、使用する前に値がまだ初期化されていない変数のことです。
未定義変数にアクセスすると、現在はWarning: Undefined variable $varnameの警告E_WARNINGが表示され、その変数値はNULLであるかのように扱われますが、実行が中断されることはありません。
しかし、これは意図しない挙動である可能性が高いでしょう。

この挙動を変更して例外を出すためにカスタムエラーハンドラを使用することはできますが、ユーザランドでのコード追加を必要とします。
また後述しますが、カスタムエラーハンドラの呼び出しをサポートすること自体がエンジンにさらなる複雑化をもたらしており、increasingly complex gamesとなっています。

RFC History / Previous Votes

かつてのRFC、Reclassifying engine warningsにおいて、未定義変数アクセスを例外にすることは56%の賛成を得ましたが却下されました。
このRFCが却下されたのは、当時は未定義変数アクセスは通知E_NOTICEであったため、通知からエラーへ一気に変更するのはギャップが大きすぎると感じられたからでしょう。
結果として、未定義変数アクセスはPHP8においてE_WARNINGになりました。
PHP9がリリースされる頃には未定義変数アクセスがE_WARNINGになってから既に5年以上が経つことになるため、経過としては十分でしょう。

Proposal

このRFCは、次のメジャーバージョンアップにおいて未定義変数へのアクセスを禁止し、アクセスした場合はError例外をスローすることを提案しています。
式で使用するためにエンジンが変数値を読み込もうとするときに、変数が未定義である場合で、現在はWarning: Undefined variable $varnameという警告が表示されます。

isset() / empty() / null合体演算子は、変数が未定義であるかを考慮するため、本RFCによる影響はありません。

未定義変数へのアクセスは、主に3種類のミスで発生します。

Mechanism 1.

特定のコードパスでのみ変数が定義される場合。
たとえばif文の中で変数値を定義したりする場合です。

  if ($user->admin) { 
     $restricted = false;
  }
 
  if ($restricted) { 
     die('You do not have permission to be here');
  }

Mechanism 2.

単なるtypo。

  $name = 'Joe';
  echo 'Welcome, ' . $naame;

変数を読むときだけではなく、定義する際に間違える可能性もあります。

  if ($user->admin) { 
     $restricted = false;
  } else { 
     $restrictedd = true;
  }
 
  if ($restricted) { 
     die('You do not have permission to be here');
  }

Mechanism 3.

未定義変数への後置インクリメント。

  while ($item = $itr->next()) { 
     /* 何かの処理 */
     $counter++; /* $counterはここまで未定義 */
  }
 
  /* こっちはMechanism1と同じ問題がある */
  echo 'You scanned ' . $counter . ' items'; 

1つめと2つめは意図しないバグであることがほとんどです。
3つめは意図的に行われることもありますが、コーディングミスの結果であることもよくあります。

これらに対しては、初期値を定義することが最も簡単な解決策です。

  $restricted = true;
  if ($user->admin) { 
     $restricted = false;
  }
 
  if ($restricted) { 
     die('You do not have permission to be here');
  }
  $counter = 0;
  while ($item = $itr->next()) { 
     $counter++;
  }
 
  echo 'You scanned ' . $counter . ' items';

Benefits

利点。

主な利点は、未定義変数へのアクセスというユーザランドのバグを一掃できることです。
これにより、PHPアプリケーションが意図しない動作になったときに実行の継続を防ぐことができます。

これだけでも十分ですが、NikitaによるとPHPエンジン側にも大きな利があることを挙げています。

実装の観点からの大きな問題として、警告を投げつつ実行を継続する必要があることです。
警告はカスタムエラーハンドラを呼び出す可能性があり、PHP VMが想定していない状態を書き替えられる可能性があります。
PHP VMはそれらを防ぐために複雑な処理を行っていますが、しかし完全ではありません。
例外ではなく警告である限り、完全に解決されることはありません。

未定義変数は最大の問題であり、この警告はほとんど全ての操作において現れる可能性があります。
この問題とJITコンパイラを組み合わせたときに発生する問題については、読者への課題となります。

Backward Incompatible Changes

互換性のない変更。

未定義変数へアクセスすると例外がスローされます。

未定義変数へのアクセスは良い習慣とは考えられておらず、PHP8以降はE_WARNINGになっています。
PHP9が登場するころはそれも5年以上前のことになっているでしょうが、この変更により動作しなくなるコードはそれなりに現れるでしょう。

Proposed PHP Version(s)

PHP9.0。

ターゲットのバージョン指定としては類を見ないものですが、このRFCの意図として、この変更をあらかじめ告知しておくことで開発者へのコード修正の期間を最大限に増やす目的があります。

次のマイナーバージョンアップにおいて、PHP9においてエラーになる旨の警告メッセージを表示します。

Unaffected Functionality

対象外について。

現在Warning: Undefined variable $varnameのE_WARININGが発生しないものについては対象外です。
たとえば、存在しない配列インデックスにアクセスした場合などは、本RFCによる影響はありません。

感想

あくまで『使用するときに未定義であればエラー』です。
すなわち、以下のコードは許されます。

if(condition()){
	$flag = true;
}else{
	$flag = false;
}

echo $flag;

他の静的言語ではifの外で定義しておかないとエラーになるので、それに比べれば緩めです。

PHPは2021年冬にPHP8.1が出たばかりであり、次のバージョンは2022年冬に出るPHP8.2であることが決定しています。
その後は不明ですが、8.2の後いきなり9.0になるとはあまり思えないので、PHP9が来るのはまだ数年は先のことです。
それなのに現時点でこのRFCを出したということは、今から準備をしておけという意志表示ですね。

それにしてもまあ、なかなか思い切った決断をしましたね。
実際、未定義変数にアクセスするコードなんて長らく見ていないので、個人的にはあまり影響しないと思うのですよ。
しかしこのあたりはPHPのアバウトさの象徴だったので、それが消えるということは、Nikita Popovが夢見ていたガチガチStrictPHPへの方向性を決定的にしたといってよいでしょう。