😇

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 は、生成されたモッククラスの定義コードを直接メモリ上で評価します。

https://github.com/mockery/mockery/blob/73a9714716f87510a7c2add9931884188e657541/library/Mockery/Loader/EvalLoader.php#L24-L31

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(身代わりクラス)」の生成です。

https://github.com/symfony/dependency-injection/blob/58ab71379f14a741755717cece2868bf41ed45d8/ContainerBuilder.php#L1104-L1124

// 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 という特殊なクロージャ内でコードを実行します。ここでは単なる実行だけでなく、スコープの魔術が行われています。

https://github.com/bobthecow/psysh/blob/85057ceedee50c49d4f6ecaff73ee96adb3b3625/src/ExecutionClosure.php#L28-L60

// ExecutionClosure.php
// ユーザー入力を実行し、結果を $_ に保持
$_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: self::NOOP_INPUT));
  1. スコープの復元: 前回の実行状態から変数を extract() で展開
  2. 実行: eval() でユーザーコードを実行
  3. スコープの保存: 実行後の変数を 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互換のテストクラスを動的に生成しています。

https://github.com/pestphp/pest/blob/47fb1d77631d608022cc7af96cac90ac741c8394/src/Factories/TestCaseFactory.php#L151-L176

// 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