eval()による黒魔術:PHPの深淵を覗く
はじめに
"eval() is Evil"
PHPプログラマなら誰もが一度は耳にする格言です。
リフレクションやマジックメソッド(__call, __getなど)といったメタプログラミングは、柔軟な設計のために私たちもよく利用します。これらは強力ですが、あくまで言語仕様の枠内に収まる「白魔術」です。
しかし、eval() は別格です。任意の文字列をコードとして実行してしまうこの関数は、セキュリティホールそのものであり、決して触れてはいけない「悪魔」として忌み嫌われてきました。
しかし、私はある日、普段何気なく使っている信頼性の高いライブラリたちのコードを読み解いている最中に、戦慄しました...
そこには、あの「悪魔」が鎮座していたのです。
Mockery、Symfony、PsySH、Pest...。
彼らは禁忌とされる eval() を排除するどころか、むしろ積極的に飼いならし、その強大な魔力を利用して、通常の手段では到達できないパフォーマンスと開発者体験を実現していました。
彼らはなぜ、あえて禁忌を犯すのでしょうか?
その答えは、「動的コード生成」「パフォーマンス最適化」「開発者体験(DX)の向上」 という、通常の手段では到達できない高みを目指すためです。
ということで、私が出会った「制御された黒魔術」をご紹介します。
Case 1. Mockery: 虚無からの創造
PHPのテストにおいて、モックオブジェクトの作成は日常的な作業です。
// 存在しないクラスやインターフェースを即座にモック化
$mock = Mockery::mock(HogeService::class);
$mock->shouldReceive('process')->once()->andReturn(true);
このように直感的に使えるMockeryですが、裏側ではテスト実行時に「存在しないクラス」を動的に生成するために eval() を使用しています。
目的:高速なモック生成
大量のモックオブジェクトを作成するテストスイートにおいて、いちいちファイルを生成・読み込み(I/O)していてはテストが遅くなります。Mockeryは eval() を使うことで、I/Oをスキップし、メモリ上で瞬時にクラスを定義します。
魔術の正体:EvalLoader
Mockeryの中核である EvalLoader は、生成されたモッククラスの定義コードを直接メモリ上で評価します。
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}
eval('?>' . $definition->getCode());
}
生成されるクラスの幻影
実際に eval() されるコードは、対象クラスを継承し、全てのメソッドをインターセプトする「影のクラス」です。
class Mockery_0_HogeService extends HogeService implements MockInterface
{
// 全てのメソッドが期待値を処理するようにオーバーライドされる
public function process(): string {
$argc = func_num_args();
$argv = func_get_args();
$ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);
return $ret;
}
}
Case 2. Symfony DependencyInjection: 構築の魔術
SymfonyのDIコンポーネントは、「物理ファイルを作らずにクラスを定義する」 という離れ業をやってのけます。
目的:I/Oレスなクラス定義
通常、PHPでクラスを利用するには .php ファイルを作成して require する必要があります。しかし、フレームワーク内部で一時的に必要な「小さなクラス」のためにI/Oを発生させるのは無駄です。
Symfonyは eval() を使い、メモリ上で直接クラスを定義することで、このボトルネックを解消しています。
魔術の正体:Lazy Proxyの動的生成
特に重要なのが、重いサービスの初期化を遅らせる「Lazy Proxy(身代わりクラス)」の生成です。
// ContainerBuilder.php
// LazyClosure::getCode() で生成されたプロキシコードを eval で即時評価
$proxy = eval('return '.LazyClosure::getCode('$initializer', $callable, $definition, $this, $id).';');
仕組み
初期化に時間がかかるサービスの代わりに、この軽量なプロキシクラスを eval() で瞬時に生成して渡します。
また、コンテナ定義の検証時にも、ダンプされたPHPコードをファイル保存せずに eval() で読み込むことで、「設計図通りにコンテナが動くか」をその場で瞬時にチェックしています。
Case 3. PsySH: 幻影の支配 (REPL)
PHPの対話型シェルであるPsySHにとって、eval() はその心臓部であり、ユーザーの世界とPHPの世界を繋ぐゲートです。
Laravelユーザーにおなじみの php artisan tinker も、実は裏側でこのPsySHが動いています。つまり、Tinkerでコードを実行するたびに、あなたは間接的に eval() の恩恵を受けているのです。
目的:対話的なコード実行
REPL(Read-Eval-Print Loop)の名前が示す通り、ユーザーが入力したコードを読み取り、評価(Eval)し、結果を表示するためには eval() が不可欠です。
魔術の正体:ExecutionClosure
PsySHは ExecutionClosure という特殊なクロージャ内でコードを実行します。ここでは単なる実行だけでなく、スコープの魔術が行われています。
// ExecutionClosure.php
// ユーザー入力を実行し、結果を $_ に保持
$_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: self::NOOP_INPUT));
-
スコープの復元: 前回の実行状態から変数を
extract()で展開 -
実行:
eval()でユーザーコードを実行 -
スコープの保存: 実行後の変数を
get_defined_vars()で取得して保存
さらに、eval() 内で致命的なエラーが発生してもシェルが終了しないよう、別プロセスをフォークして実行する(Process Forking)という高度な安全装置も備えています。
Case 4. Pest: 聖なる矛盾
新進気鋭のテストフレームワーク Pest は、eval() に対して「ユーザーには禁止するが、自分たちは使う」という興味深いスタンスを取っています。
目的:究極のDX(開発者体験)
Pestは、PHPUnitの堅牢な基盤の上に、美しくシンプルな関数型APIを提供することを目指しています。
// Pestのテストコード
test('sum', function () {
expect(1 + 2)->toBe(3);
});
魔術の正体:動的クラス生成
このシンプルな構文をPHPUnitに理解させるため、Pestは裏側で eval() を使い、PHPUnit互換のテストクラスを動的に生成しています。
// TestCaseFactory.php
$classCode = <<<PHP
namespace $namespace;
use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
$attributesCode
#[\AllowDynamicProperties]
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode
private static \$__filename = '$filename';
$methodsCode
}
PHP;
eval($classCode);
生成例:
<?php
namespace P\Tests;
use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
#[\PHPUnit\Framework\Attributes\TestDox(["/path/to/project/tests/ExampleTest.php"])]
#[\AllowDynamicProperties]
final class ExampleTest extends \PHPUnit\Framework\TestCase implements \Pest\Contracts\HasPrintableTestCaseName {
use \Pest\Concerns\Testable, \Pest\Concerns\Expectable;
private static $__filename = '/path/to/project/tests/ExampleTest.php';
#[\PHPUnit\Framework\Attributes\Test()]
#[\PHPUnit\Framework\Attributes\TestDox(["can add two numbers"])]
public function __pest_evaluable_can_add_two_numbers(...$arguments)
{
return $this->__runTest(
$this->__test,
...$arguments,
);
}
#[\PHPUnit\Framework\Attributes\Test()]
#[\PHPUnit\Framework\Attributes\TestDox(["can multiply two numbers"])]
public function __pest_evaluable_can_multiply_two_numbers(...$arguments)
{
return $this->__runTest(
$this->__test,
...$arguments,
);
}
}
聖なる矛盾
Pestは自身のアーキテクチャテスト機能(Security Preset)で eval() の使用を禁止することを推奨しています。「危険な魔術は我々が管理する。君たちは安全な世界でコードを書いてくれ」という、フレームワーク作者の矜持が見て取れます。
?> の秘密
これらのライブラリのコードを見ると、eval() の呼び出しで共通する奇妙なパターンに気づきます。
eval('?>' . $code);
理由:モードのリセット
eval() は実行時に「PHPモード」で始まります。しかし、動的に生成されたコード($code)は、しばしば <?php タグから始まります。
そのまま結合すると eval('<?php ...') となり、PHPモード内でさらにPHPタグを開こうとして構文エラーになります。
そこで、?> を前置して一度PHPモードを強制的に終了させます。すると、直後の <?php タグで再び正常にPHPモードが開始されます。
これにより、ファイルとして保存可能な完全なPHPコードを、そのまま eval() で実行できるのです。
結論
eval() は確かに危険な「黒魔術」です。しかし、Mockery、Symfony、PsySH、Pestといった主要ライブラリは、その危険性を深く理解し、制御した上で利用しています。
- Mockery: 高速なテストダブル生成
- Symfony: コンパイル済みキャッシュの即時利用
- PsySH: 対話的実行環境の実現
- Pest: 理想的なDXと互換性の両立
彼らは eval() を単なる「便利な関数」としてではなく、言語の制約を超え、パフォーマンスと体験を極限まで高めるための「魔法の杖」として振るっているのです。
とはいえ、この強大な力を完全に制御しきる自信は私にはありません。
下手に手を出せば大火傷するのは目に見えているので、私が自分のコードで eval() を使うことは、たぶん今後もないと思います(笑)
Discussion