Slide 1

Slide 1 text

PsySH から紐解くREPL の仕組み 2025/3/22 PHPerKaigi 2025 day1 @muno_92 1

Slide 2

Slide 2 text

自己紹介 X ( 旧Twitter): @muno_92 所属: スパイダープラス株式会社 Web エンジニア PHPerKaigi 2025 コアスタッフ PHP カンファレンス小田原2025 コアスタッフ 来月4/12( 土) 開催。来てね! 2

Slide 3

Slide 3 text

こんな場面、どうしていますか? プログラムを手元でサクッと動かしたい・その結果を確認したい 依存ライブラリの挙動を確認したい 実装はあるけど手で叩くのは面倒な複雑なSQL の結果を確認したい 「User モデルのあのメソッドを使えば一発で取れるんだけど な〜」 3

Slide 4

Slide 4 text

そんな場合にREPL はどうでしょう? REPL (Read Eval Print Loop の略) 入力の読み取り (Read) 入力されたコードの評価 (Eval) 評価結果の出力 (Print) を繰り返し (Loop) ながらインタラ クティブにコードを実行できる環境 0:00/ 0:20 4

Slide 5

Slide 5 text

REPL を普段使ってる人 5

Slide 6

Slide 6 text

「REPL って聞いてもピンと来ないけど laravel/tinker なら使ったことがある」人 6

Slide 7

Slide 7 text

REPL の例 php -a (Interactive shell) ※Print 機能は備えていない PsySH (PHP) IRB (Ruby) IPython (Python) Node.js REPL (JavaScript) 7

Slide 8

Slide 8 text

Web フレームワークの機能を使えるREPL も Laravel Tinker (laravel/tinker) REPL plugin for CakePHP (cakephp/repl) Rails Console 8

Slide 9

Slide 9 text

PsySH の実装を通してREPL の仕組みを深堀り します 9

Slide 10

Slide 10 text

目次 前提 REPL で行う主な操作 内部で何が起こっているのか 〜PsySH の実装〜 PsySH の拡張方法 10

Slide 11

Slide 11 text

目次 前提 REPL で行う主な操作 内部で何が起こっているのか 〜PsySH の実装〜 PsySH の拡張方法 11

Slide 12

Slide 12 text

今回の発表のきっかけ 普段からREPL を愛用 「PsySH の実装ってどうなっているんだろう?」 12

Slide 13

Slide 13 text

PsySH とは PHP 製の開発者向けコンソール デバッガ機能やREPL 機能を備えている 「A runtime developer console, interactive debugger and REPL for PHP. 」 https://github.com/bobthecow/psysh PsySH をベースとしてlaravel/tinker やcakephp/repl などが作られて いる 13

Slide 14

Slide 14 text

PsySH を活用したツール https://github.com/bobthecow/ps ysh/wiki/Integrations 14

Slide 15

Slide 15 text

PsySH はphp -a よりも便利 大きな違い:PsySH は式 ( 後述) の評価結果が出力される 15

Slide 16

Slide 16 text

話すこと PsySH の仕組み PsySH の実装例から見る、REPL の構成要素 laravel/tinker やcakephp/repl がPsySH をどのように拡張しているか 16

Slide 17

Slide 17 text

話さないこと PsySH のREPL 以外の機能 デバッガ non-interactive モード 17

Slide 18

Slide 18 text

検証環境 PsySH: 0.12.7 cakephp/repl: 2.0.1 laravel/tinker: 2.10.1 スライド中で紹介するコードは実際の実装から簡略化しています 18

Slide 19

Slide 19 text

目次 REPL で行う主な操作 前提 内部で何が起こっているのか 〜PsySH の実装〜 PsySH の拡張方法 19

Slide 20

Slide 20 text

REPL で行う主な操作 シェルの起動 文字の入力 左右にカーソルを移動 入力した文字を編集 tab キーで補完 上下キーでhistory を表示 Enter で実行 0:00/ 0:20 20

Slide 21

Slide 21 text

目次 内部で何が起こっているのか 〜PsySH の実装〜 前提 REPL で行う主な操作 PsySH の拡張方法 21

Slide 22

Slide 22 text

( 再掲) REPL で行う主な操作 シェルの起動 文字の入力 左右にカーソルを移動 入力した文字を編集 tab キーで補完 上下キーでhistory を表示 Enter で実行 22

Slide 23

Slide 23 text

文字の入力〜Enter でループ1 回分 つまりこうまとめられる シェルの起動 文字の入力〜Enter で実行 Read Eval Print 23

Slide 24

Slide 24 text

シェルの起動 (psysh コマンドの実態) 1. Phar アーカイブ リリースページなどからダウンロードした場合 https://www.php.net/manual/ja/intro.phar.php 2. Phar アーカイブにしていないPHP スクリプト composer (global) require でインストールした場合 Phar アーカイブを使用すれば、PHP のアプリケーションをひ とつのファイルとして配布できるようになります。 “ “ 24

Slide 25

Slide 25 text

シェルの起動 (psysh コマンドの内部実装) call_user_func(function () { // オートロード // psyshコマンドを実行したディレクトリにpsyshがインストールされていたらローカルのpsyshを使う }); // オートロードできなかったらPsySH自身が保持している依存ライブラリを使う if (!class_exists('Psy\Shell')) { Phar::mapPhar('psysh.phar'); require 'phar://psysh.phar/.box/bin/check-requirements.php'; require 'phar://psysh.phar/vendor/autoload.php'; } call_user_func(Psy\bin()); 25

Slide 26

Slide 26 text

Psy\bin() を深掘り (1/2) $input = new ArgvInput(); $input->bind(new InputDefinition(\array_merge(Configuration::getInputOptions(), [ new InputOption('help', 'h', InputOption::VALUE_NONE), new InputOption('version', 'V', InputOption::VALUE_NONE), new InputOption('self-update', 'u', InputOption::VALUE_NONE), new InputArgument('include', InputArgument::IS_ARRAY), ]))); symfony/console を使ってコマンドラインオプションをパース 26

Slide 27

Slide 27 text

Psy\bin() を深掘り (2/2) $config = Configuration::fromInput($input); $shell = new Shell($config); $shell->run(); コマンドラインオプションから読み取った設定をセットした状態で シェルを起動 27

Slide 28

Slide 28 text

Shell クラス PsySH のコア laravel/tinker やcakephp/repl も内部ではShell クラスを実行している https://github.com/laravel/tinker/blob/102bfc19b79817022e9f b1d3dd235d43d42f1954/src/Console/TinkerCommand.php#L8 5 https://github.com/cakephp/repl/blob/6790873a9d1e93dcd4a5 860bc5afe404b77c652c/src/Command/ConsoleCommand.php# L49 28

Slide 29

Slide 29 text

symfony/console https://github.com/symfony/console コマンドラインアプリケーションの作成を簡単にしてくれる Symfony 製コンポーネント PsySH での使用 コマンドライン引数のパース Symfony\Component\Console\Input\ArgvInput コマンドの定義 (Shell クラスの基底クラス) Symfony\Component\Console\Application\Application 29

Slide 30

Slide 30 text

Shell クラスの実装 class Shell extends Application { private function doInteractiveRun(): int { $this->initializeTabCompletion(); $this->readline->readHistory(); // 省略 $loop = new ExecutionLoopClosure($this); $loop->execute(); } } 30

Slide 31

Slide 31 text

ExecutionLoopClosure ? while (true) { // Loop $__psysh__->getInput(); // Read $_ = eval($__psysh__->flushCode()); // Eval $__psysh__->writeReturnValue($_); // Print } シェルが起動した後、while ループでユーザーの入力を待ち受ける closure を宣言しているクラス 文字通りRead Eval Print Loop してる! 31

Slide 32

Slide 32 text

〜シェルの起動終わり〜 32

Slide 33

Slide 33 text

Read ( 入力値を読み取り) readline を使っている カーソル移動して入力文字を編集した場合 そこにPsySH は関与していない Enter を押した後の文字がPsySH に入ってくる 33

Slide 34

Slide 34 text

GNU Readline / libedit ラインエディタ 対話型のツールを作るための様々な機能を提供している ユーザーの入力の読み取り tab 補完 history GNU Readline はGPL ライセンスのためMac では代替のlibedit を使用 PHP からはあまり違いを意識せずに使える https://www.php.net/manual/ja/ref.readline.php 34

Slide 35

Slide 35 text

PHP でのreadline 利用サンプル while (true) { $line = readline(); echo "line: {$line}\n"; } 1 行ごとに入力を受け取るだけならこれだけでOK とはいえ、複数行入力したい場合もある 35

Slide 36

Slide 36 text

どうやって複数行の入力を受け付けている? PHP のコードとしてパースが成功するまでreadline Parse Error のrawMessage を見て入力途中かを判別している 複数行をまとめて読み取っているのではなく1 行読み取りを繰り返 している do { $input = $this->readline(); $this->addCode($input); // この中でコードがパースされる } while (!$this->hasValidCode()); 36

Slide 37

Slide 37 text

つまり for ($i = 0; $i < 10; $i++) { echo $i . PHP_EOL; まで入力してから「$i < 100 に変更したいな」 ↓ 出来ない 37

Slide 38

Slide 38 text

( ちなみに) 複数行編集できるREPL も IRB readline をRuby 製のreline に置き換えている IPython 38

Slide 39

Slide 39 text

PsySH 全体から見るとこうなっている // Read-Eval-Printのループ while (true) { // コードが有効になるまで行を読み続けるループ do { } while () // Eval // Print } 39

Slide 40

Slide 40 text

tab 補完 readline_completion_function() に補完用の関数を登録 登録した関数の戻り値が候補として表示される 補完対象 変数名 インスタンスメソッド PsySH が提供している各種コマンド etc 40

Slide 41

Slide 41 text

history ( 入力履歴) の管理・表示 大体readline がやってくれる readline_read_history() readline_add_history() readline_clear_history() etc デフォルトでは$HOME/.config/psysh/psysh_history に保存 41

Slide 42

Slide 42 text

〜これで入力値を受け取れるように〜 42

Slide 43

Slide 43 text

Eval ( 入力されたコードを評価) eval() 引数で渡した文字列をPHP のコードとして評価する return を付けるとeval の戻り値で評価結果が返ってくる php > var_dump(eval('$a = 1;')); NULL php > var_dump(eval('return $a = 1;')); int(1) この値を出力時に使用 43

Slide 44

Slide 44 text

return を入力した覚え無いけど? 入力されたコードに自動でreturn を付けている 44

Slide 45

Slide 45 text

自動でreturn を付けるには 単純に文字列結合すると return if () { } のようになりかねない return を付けて良い時だけreturn を付けたい = 式の場合 45

Slide 46

Slide 46 text

文と式 文 (statement) if 文、switch 文など 式 (expression) https://www.php.net/manual/ja/language.expressions.php コードを評価( 実行) した結果として値が返ってくるもの 今はこれだけ分かっていればOK 最も簡単で最も正確な式の定義は、" 値があるもの全て" “ “ 46

Slide 47

Slide 47 text

AST を見れば式を判別できる! Abstract Syntax Tree ( 抽象構文木) https://phpstan.org/developing-extensions/abstract-syntax-tree PHP のソースコードを文字列処理しているのではなく、PHP Parser でパースした後のオブジェクトを処理 47

Slide 48

Slide 48 text

PHP Parser https://github.com/nikic/PHP-Parser PHP のコードの解析や操作をやりやすくしてくれる神ライブラリ PHPStan やRector など様々なPHP 製ツールで使われている 48

Slide 49

Slide 49 text

AST はノードがツリー状に構成されている $a = 1 PhpParser\Node\Stmt\Expression PhpParser\Node\Expr\Assign PhpParser\Node\Expr\Variable name: "a" PhpParser\Node\Scalar\Int_ value: 1 49

Slide 50

Slide 50 text

AST は加工できる PHP Parser のtraverser https://github.com/nikic/PHP- Parser/blob/master/doc/component/Walking_the_AST.markdow n AST を辿って独自定義の処理を実行できる 50

Slide 51

Slide 51 text

元々のノードをReturn ノードで包む // Psy\CodeCleaner\ImplicitReturnPass } elseif ($last instanceof Expression && !($last->expr instanceof Exit_)) { $nodes[\count($nodes) - 1] = new Return_($last->expr, [ 'startLine' => $last->getStartLine(), 'endLine' => $last->getEndLine(), ]); } return $nodes; → ユーザーが入力したコードにreturn が付く! 51

Slide 52

Slide 52 text

〜入力されたコードの評価まで終わり〜 52

Slide 53

Slide 53 text

Print ( 評価結果の出力) symfony/var-dumper を使用 https://github.com/symfony/var-dumper var_dump()/var_export() よりも見やすく出力してくれる 53

Slide 54

Slide 54 text

var_export() を使って出力した場合 54

Slide 55

Slide 55 text

var-dumper を使って出力した場合 ( デフォルト) 55

Slide 56

Slide 56 text

〜読み取り・評価・出力が完了〜 56

Slide 57

Slide 57 text

これまでの内容を踏まえて改めて動作を確認 0:00/ 0:15 57

Slide 58

Slide 58 text

目次 PsySH の拡張方法 前提 REPL で行う主な操作 内部で何が起こっているのか 〜PsySH の実装〜 58

Slide 59

Slide 59 text

そもそも、何故PsySH を拡張しているか PsySH は任意のPHP プロジェクトのファイルをオートロードしてく れるが、それだけ 例えばフレームワークのDB 接続設定は読み込まない フレームワークの便利な機能を使うのは、それらを読み込む必要 がある 59

Slide 60

Slide 60 text

各FW はコマンドラインツールを作る方法を提供している それに則って作成したコマンドではフレームワークの設定が読み込 まれる https://laravel.com/docs/12.x/artisan#writing-commands https://book.cakephp.org/5/en/console- commands/commands.html 定義したコマンドからShell クラスを使う それによって、フレームワークの設定が生きた状態でシェルを使 える 60

Slide 61

Slide 61 text

cakephp/repl class ConsoleCommand extends Command { public function execute(Arguments $args, ConsoleIo $io) { $psy = new Shell(); $psy->run(); } } ログの設定などは行っているが、ほとんどこれだけ シンプル。必要十分 61

Slide 62

Slide 62 text

laravel/tinker class TinkerCommand extends Command { public function handle() { $config->getPresenter()->addCasters( $this->getCasters() ); $loader = ClassAliasAutoloader::register(引数は省略); $shell->execute($code); } } 62

Slide 63

Slide 63 text

Caster 出力形式を拡張する仕組みをPsySH が提供している (addCasters) それを利用しModel やCollection などを見やすくキャストしている 63

Slide 64

Slide 64 text

Collection ( キャストなし) 64

Slide 65

Slide 65 text

Collection ( キャストあり) 65

Slide 66

Slide 66 text

ClassAliasAutoloader > User::find(1) [!] Aliasing 'User' to 'App\Models\User' for this Tinker session. これ spl_autoload_register() にalias を解決するcallable を登録している spl_autoload_register([$loader, 'aliasClass']); . . . class_alias($fullName, $class); 66

Slide 67

Slide 67 text

まとめ PsySH は文字通りRead Eval Print Loop を体現する実装になっていた 対話型ツールの実装はreadline / libedit が支えている PHP には文と式がある REPL に出力される実行結果は式を評価した値 PHP のソースコードを解析・操作したい場合はPHP Parser が便利 67

Slide 68

Slide 68 text

感想 普段使っている便利なツールも色々なOSS ・技術を組み合わせて作 られている 深堀りしてみると面白い PHP Parser は色んな所に出てくるので知ってるとお得 読む前はPHP Parser が使われているとは想像してなかった Symfony のコンポーネントも色んな所で使われている 68

Slide 69

Slide 69 text

ご清聴ありがとうございました 69

Slide 70

Slide 70 text

参考資料 PHP の文と式 https://phpstan.org/developing-extensions/abstract-syntax-tree https://github.com/nikic/PHP- Parser/blob/master/doc/component/Walking_the_AST.markdown https://symfony.com/doc/current/components/var_dumper.html https://symfony.com/doc/current/components/console.html 70

Slide 71

Slide 71 text

付録: 「ここ読んでて面白かった」ポイント ( 再掲) Psy\ExectionLoopClosure Psy\CodeCleaner getDefaultPasses() 今回紹介したImplicitReturnPass も含め、ユーザーが入力した コードに対して様々なバリデーション・加工を行っている parse() どのパースエラーの場合は入力途中とみなすか Psy\Shell::getDefaultMatchers() 補完用のクラスが色々ある 71

Slide 72

Slide 72 text

付録: PHP 標準関数だけでRead を再実装 $input = []; $editing = true; do { $input[] = readline(); try { eval(implode("\n", $input)); $editing = false; } catch (ParseError) { } } while ($editing); printf("input: %s\n", implode("\n", $input)); ポイント eval をパーサー代わ りに使っている 不正な構文の場合 にParse Error を投 げるのを利用 72