2020-12-10: PolyfillでPHP8にPHP4の魂を吹き込む
さて、この記事の主題は一般のユーザーにはまったく不要のものなので、先に役に立つ情報を紹介します。
PHP7にPHP8の魂を吹き込む
みなさまPHP8は既にお使いになってますでしょうか。PHP8リリースアナウンスページはご覧になりましたでしょうか。
これらのページを見ればPHP8によって何がもたらされるのかということがばっちりわかる構成になっているので、まだご覧になっていない方は、ぜひ見てください。
さて、このうち、「新しいクラス、インターフェイス、関数」に属するものの一部Polyfillによって再現することができます。 このように完璧にPHP 8.0を再現するということはできないものの「業務で使っているのは7.3だから8.0を使えるのはまだ先だよ〜」という私たちの皆さんも、地味に便利な新関数がこのパッケージを導入するだけで使えるようになるのは嬉しいですね。いますぐ入れましょう。
これは蛇足ですが、まだ仕事ではPHP5を使わざるを得ないという場合はsymfony/polyfill-php70を使うことでintdiv()、さらにPHP 5.3や5.4を保守せざるを得ないというひとはsymfony/polyfill-55を入れることで array_column() のような便利な関数を使うことができます。使いましょう。ただしsymfony/polyfill-php80は最初からPHP 7.0以上が対象であり、2020年10月からはそれ以外のPolyfillの最新版はPHP 7.1以上専用になりました。 PHP8にPHP4の魂を吹き込む
さて、ここまで話してきたのは、PHP 7.xからPHP 8.0にアップデートするまでの繋ぎとしてPolyfillを使って一部の機能を先行して取り込もうという実用的な話でした。
さて、来るものがあれば去るものもあります。
create_function()関数は削除されました。 無名関数が代わりに使えます。
each()関数は削除されました。 代わりに foreach や ArrayIteratorを使うべきです。
これらの関数はPHP 4時代からあった機能ですが、function式(クロージャ)やforeach文の導入によってPHPの制御構造の重要な位置から外されていったものたちです。このようなものをただ見送るのは一抹の寂しさがありますよね。 そうです、墓穴を暴きましょう。
create_function()の再実装
create_function()は次のように使うことができます。せっかくなので歴代の無名関数の記法を使って比較してみましょう。
code:php
// 引数を二倍にして返す関数
$x2 = create_function('$n', 'return $n * 2;'); // PHP 4〜
$x2 = function ($n) { return $n * 2; }; // PHP 5.3.0〜
$x2 = fn($n) => $n * 2; // PHP 7.4.0〜
function式およびfn式(アロー関数)はどちらもClosureクラスのインスタンスオブジェクトを返すのに対して、元祖create_function()は文字列を返します。これはPHPが関数抽象を取り扱う方法に起因します。 ここは本筋ではないので別に読まなくてよいのですが、
誤解を恐れずに言うと値としての関数、変数に入れられる関数のことです。ほかの言いかたをすると「ラムダ」とか「ラムダ式」とか言われることもあります。関数を値として扱うための方法ですが、大きく分けると「関数と変数が同じ名前空間に属する言語」「関数と変数が別の名前空間に属する言語」に分けられます。ここでいう名前空間とはnamespaceのことではなく、単に変数名と関数名が被るかという話です。たとえばPythonのdef foo:やJavaScriptのfunction foo()で関数を定義すると、それはfooという変数で参照できるfunctionオブジェクトになります。この性質によりfoo = 1のような変数代入をするとその変数スコープにおいてfooが上書きされ、foo()という関数呼び出しはできなくなります()。一方でPHPではfunction foo()の形式で関数を定義したとしてもどのようなオブジェクトが生成されることもありませんし、JavaScriptやPythonのように変数を使ってそのまま呼び出せるということはありません。そもそもPHPの関数名はvar_dumpのような名前で、変数名は$vのような違いがあり、明らかに別の名前空間に属します。そこでPHPにおいて出てくるのがcallableという考えかたで、関数名の文字列を変数に代入することで、変数を関数のように呼び出せるようにしました。つまりPHPでは$f = 'foo'; $f();のように関数を呼び出しできるのです。文字列が関数のように振る舞うことを奇妙に感じるでしょうか。Lisp族に属する言語では関数名のシンボルを使って関数を呼び出すということが普通に行なわれます。シンボルはPHPには存在しない種類のオブジェクトですが、「変数名」や「関数名」のようなものです。このシンボルはLisp族では一般に実行時に文字列と相互変換できます。PHPにはシンボル値がないので文字列をシンボルの代用として扱っていると考えることはできないでしょうか。私見ではRubyはシンボルと文字列を別のものとして持っているせいで複雑になっておりRailsでActiveSupport::HashWithIndifferentAccessのようなものが横行しているようなことを考えると不要な区別なのではないかとすら感じています。余談ですがRubyではさまざまなものがオブジェクトとして実現されていますが、実はメソッドそのものはオブジェクトではありません。ではRubyではどうやって関数の動的呼び出しやオブジェクトでの抽象化を実現しているかは読者への課題とします。 さきほどうっかりPHPでは関数はvar_dumpのような名前で変数は$vの形式なのであきらかに別の名前空間だというようなことを書いてしまったのですが、そうなると定数はどうなるのでしょう。一般的に定数はconst VAR = 1;のようにすべて大文字で書かれますがこれは慣習に過ぎず、const var_dump = 1;のような定数を宣言することもできます。しかしPHPでは「関数名と同じ定数が定義されていてそこに関数オブジェクトが代入されている」ということもなく、完全に別物です。また const bar = 'foo'; bar();のような関数呼び出しをすることもできません。bar()という関数呼び出しをしたときに定数が参照されることは一切ありません。ただ、PHP7以降では(bar)()という(奇妙な)呼び出しをすることはいちおう可能です。これは("foo")()と同じ意味になります。
さて、どうでもよい話題が長くなったので話を戻しましょう。↑は読まなくていいです。
PHPでは $f = 'foo'; のような文字列が代入された変数を介して $f()のような関数呼び出しができるのでした。ただし、この形式で呼び出しさせるためにはグローバル関数が必要になります。そもそもPHP4にはグローバル関数とメソッドしかなかったのですが。さておき、この制約を超えるのがcreate_function()なのでした。 $x2 = create_function('$n', 'return $n * 2;'); と書くことで、functionという構文で書かなくても一時的に使う関数を取得できるのです。
ではcreate_function()は何をやっているのでしょうか。それはcreate_function()の返り値を表示してみるとわかります。
code:php
$x2 = create_function('$n', 'return $n * 2;');
var_dump($x); //=> string(9) "lambda_1"
よく見てほしいのですが奇妙な点はないでしょうか。lambdaは6文字の単語です。_1を足して8文字ですね。だというのに、var_dump()の出力はstring(9)になっています。この数字は特別なものではなく、単なるバイト数です。ではこれは何が起こっているのかというと、関数名の冒頭に"\0"(ヌルバイト)が入っています。PHPの文字列にとって \0は文字列の終端ではありませんが、通常の関数定義構文で function \0lambda_1() {}のような名前で定義する方法はありません。このようなPHPコードを生成したとしてもパーサーがパースできないので生成できません。つまりcreate_functionは通常のevalだけでは再現できない方法でグローバル関数を定義をしています。
これがPHPマニュアルで「警告」されている理由のひとつです。
警告
この関数は、内部的に eval()を実行しているので、eval()と同様にセキュリティ上のリスクがあります。 さらに、パフォーマンスやメモリ使用効率の面でも問題があります。
PHP 5.3.0 以降を使っている場合は、この関数ではなく、ネイティブの 無名関数 を使うべきです。
create_function()で生成した関数はグローバル関数として定義されています。本当に一瞬しか使わないものであっても延々と蓄積されていきます。一方でPHP 5.3で導入されたクロージャはClosureクラスのインスタンスオブジェクトであり、不要なオブジェクトがいつまでも蓄積されることは基本的にありません。ガベージコレクションの対象です。 Closureはevalやcreate_function()の用途を完全に置き換えることはできるのでしょうか。いえいえ、そうとは限りません。evalを使わないfunctionだけで任意のパラメータを持った関数を動的に生成することはできないのです。そうです。PHP 5.6と7の新機能を使った画期的バリデータの実装 - Qiitaの実現にはevalまたはcreate_function()のような機能が必要なのです。 では新生create_function()はクロージャベースで定義すべきなのでしょうか。\0から始まる関数名をユーザー定義できないのは仕方ありません。ここは妥協するポイントでしょう。しかしながらクロージャベースの実装にするとオリジナルの一部の 挙動を再現できなくなります。ということでprefixは "\0"ではなく別の文字列にすることにします。
ちなみに話をここで終わらせようと思うと実装はこれで完了です。
code:php
function create_closure($args, $code)
{
return eval("return function ({$args}) { {$code} };");
}
しかしこの実装では以下のコードが非互換になります。
code:php
$x2 = create_function('$n', 'return $n * 2;} if (true) {var_dump(1);'); // 1が表示
$x2 = create_closure('$n', 'return $n * 2;} if (true) {var_dump(1);'); // syntax error
$x2 = create_closure('$n', 'return $n * 2;}; if (true) {var_dump(1);'); // 何も表示されない
そもそも元々の実装の時点で function () { ... }の壁を超えられてしまう
「セキュリティ上のリスクがある」という…
create_closure()はevalの直後にreturnがあるので function { ... };を乗り越えたとしても副作用を起こすことはできません。
create_closure()の方がちょっぴりだけセキュア…?
別にそんなことはない。よくないことができるのは大差ない。
こういう呼び出しをするとグローバル関数を定義できる。同様にクラスも定義できる。
code:php
create_closure('$n', 'return $n * 2;}; function hogehoge() {var_dump("a");')
hogehoge(); // "a" と出力
割とどうでもいいことを書いてきたのですが、せっかくなのでcreate_function()と挙動を合わせていきましょう。ということで作ったのがこのパッケージ。
ながったらしいのでコメントなどは省いて抜粋
code:php
namespace Php5Friends
{
function create_function($args, $code)
{
if (PHP_MAJOR_VERSION <= 7) {
return @\create_function($args, $code);
}
static $i;
$namespace = __NAMESPACE__;
do {
$i++;
$name = "__{$namespace}_lambda_{$i}";
} while (\function_exists($name));
eval("function {$name}({$args}) { {$code} }");
return $name;
}
}
namespace {
if (!function_exists('create_function')) {
function create_function($args, $code)
{
return Php5Friends\create_function($args, $code);
}
}
}
PHP 8未満なら組み込みのcreate_function()が使えるので独自実装は使わない
PHP 7.3と7.4ではE_DEPRECATEDエラーが発行されるので@で握り潰す
do-whileで関数名が重複しないようにチェック
create_function()を迂回して同じ命名規則で関数が定義されてしまった場合にエラーを避ける
if (!function_exists('create_function'))でチェックしてから関数定義
このようにガードすることでPHP 5.xや7.xでだけ関数を定義できる
既存のPolyfillをはじめ、Composerで関数を提供するパッケージはこの方式
というわけで、無事にcreate_function()をPHP 8.0で現役として使えるようになりました。うれしいですね。
each()の再実装
いちおう説明しますが、each()とはこういうものです。
code:php
$array = array_column(array_map(null, range(0, 9), range('a', 'j')), 0, 1);
while (list($key, $value) = each($array)) {
var_dump(array($key => $value));
}
// やりたいのは foreach と同じ
foreach ($array as $key => $value) {
var_dump(array($key => $value));
}
これもPHP4時代からあった機能でした
配列の内部ポインタというものを操作して走査を進めたり逆に戻したりできるところがforeachと違う が、内部ポインタってやつを操作したする機能はPHP7の時代を経てどんどんフェードアウトしていきました
foreachの10倍くらい遅くて最適化できなかったらしい
というわけでeach()関数を引き続き使いたい場合は composer require nanasess/php8-compatすればよさそうです。
まとめ
今回はPHP 4の魂を失ってしまったPHP 8にPHP 4の魂を吹き込んでPHP 12にする方法をお伝えしました。僕はアロー関数やforeachで十分だと思うので、これからPHP 8を使っていきます!