PsySHをカスタマイズしよう

[PHP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/php) の17日目の記事です！

Laravel tinkerでも利用されている PsySH のカスタマイズについて紹介します。


## PsySH

いわゆる [REPL(Read-Eval-Print Loop)](https://ja.wikipedia.org/wiki/REPL)環境です。
Laravelを使っている方は`tinker`でおなじみですね。
tinkerはpsyshをラップしていて、psyshと同じカスタマイズ方法が可能です。

使い方は @rana_kualu さんのステキ記事 [PsySH公式サイト日本語訳 #PHP - Qiita](https://qiita.com/rana_kualu/items/99b316f41143411f1385) を読もう！


## 設定ファイル

### 設定ファイルの場所

ユーザのHomeディレクトリや、プロジェクトの基準ディレクトリに設定ファイルを書いておくと読んでくれます。
プロジェクト毎の設定はgit管理してチームで共有すると良いかも。

- グローバル(ユーザ毎)設定 
    - `~/.config/psysh/config.php`
- ローカル(プロジェクト毎)設定
    - プロジェクトの基準ディレクトリ/`.psysh.php`
        - ※グローバル設定を **オーバーライド** して拡張する

### 設定ファイルの書き方

phpファイルなので割りと自由に書けます。
ファイルの先頭に `<?php`を書くのを忘れずに。

設定項目は公式ドキュメントのページを参照ください。

- [Config options · bobthecow/psysh Wiki](https://github.com/bobthecow/psysh/wiki/Config-options)

設定ファイルのサンプルは、公式や前述の@rana_kualu さんの記事を参考に。
- [Sample config · bobthecow/psysh Wiki](https://github.com/bobthecow/psysh/wiki/Sample-config)
- [PsySH公式サイト日本語訳 #PHP - Qiita](https://qiita.com/rana_kualu/items/99b316f41143411f1385#configuration)

## プロンプトのカスタマイズ

デフォルトのプロンプトは `> ` ですが、設定でカスタマイズできます。
例えば、Laravel tinkerを開発環境だけでなく、ステージング環境や本番環境でも扱う場合は、見分け易くするためにプロンプトを変えておくと良いかも。
私は以下のような設定をしています。

```php:.psysh.php
<?php

return [
    'theme' => [
        // ローカル環境以外ではプロンプトに環境名を表示する
        'prompt' => ($_ENV['APP_ENV'] === 'local' ? '' : $_ENV['APP_ENV'] ) . '> ',
    ],
];
```

ローカル環境外では環境名を表示するので事故防止に役立てば良いな。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/62ce7364-cdd2-7358-d38c-244789cd6e1b.png)


## ページャーのカスタマイズ

### 折返しが読みにくい問題

長い文字列が折り返されて、とても読みにくいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/91ce55f1-7afa-66d2-ccb0-dd9f8cf1c750.png)

### lessのオプションを変えよう

psyshではデフォルトのぺージャーとして`less`が使われています。
キーボードで `-` `S` と続けて入力すると、折り返さないモードで表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/05b59609-f67d-e39f-6224-89abee6412e4.png)

less内で毎回インタラクティブコマンドを叩くのも面倒なので、デフォルトの設定を変えましょう。
設定の`pager`に、lessコマンドの好きなオプションをつけて設定しておきます。

以下のオプションは特に有用です。

- `-S` 折り返さない
- `-R` ANSI colorエスケープシーケンス対応 (色をつける)
- `--quit-if-one-screen` 1画面に収まる場合はすぐ終了

```php:.psysh.php
<?php

return [
    // ...

    // pager less
    //   -S  折り返さない
    //   -R  ANSI color escape sequence対応
    //   --quit-if-one-screen 1画面に収まる場合はすぐ終了
    'pager' => 'less -S -g -j10 -M -R -i --no-init --quit-if-one-screen --RAW-CONTROL-CHARS',
];
```


### Vimをページャーとして使ってリッチな体験

Vimを使われている方なら、もっとリッチな検索や移動、編集、
折り畳みなどの高度な表示が欲しいと思うかもしれません。
いっそ、Vimをページャーにしてみましょう。

ポイントは以下です。

* 読み込み専用`view`コマンドを実行
* Vim側でシンタックスハイライトする
    * 元の色付けしてあるエスケープシーケンスを削除
    * vimデフォルトの`perl`のシンタックス定義を使う
        * ※ファイルタイプ定義と色定義を自作で頑張るのは大変なので
        * キー名、文字列、数値などがそれなりに色付けされる
* 終了時にvimの画面を残す (お好みで)

```php:.psysh.php
<?php

return [
    // ...

    // pager vim
    //   sed "s/\x1b\[[0-9;]*m//g" ANSI color escape sequenceを削除
    //   -c "set filetype=perl" perlシンタックスハイライトが見やすい
    //   -c "set t_ti= t_te=" vim終了時に元画面を復帰させなない(vimの画面を残す)
    'pager' => 'sed "s/\x1b\[[0-9;]*m//g" | view -c "set filetype=perl" -c "set t_ti= t_te=" -',
];
```

やったー折り畳み放題だ！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/1555da6a-38a9-0374-7c71-ff0bd30b9306.png)

Vimなので、好きなようにカスタマイズできます。
以下を意識して、あとはお好みで。

* psyshのdump出力に合わせてインデントを`2`にする
* 折り畳み方法は`indent`
* 折返ししない

```vimscript:~/.vimrc
" インデント、タブ
set tabstop=2
set softtabstop=2
set shiftwidth=2
set expandtab
set smarttab
set autoindent
set smartindent

" 折り畳み
set foldmethod=indent
set foldlevel=50

" 折返ししない
set nowrap

" 行番号を表示する
set number

" 対応する括弧やブレースを表示する
set showmatch

" カラースキーマ
colorscheme darkblue
```

## ダンプをカスタマイズ

### オブジェクトのダンプが冗長すぎる問題

値をダンプする際に、そのプロパティや子要素を再帰的に表示するので、ダンプ出力はかなり冗長になるので、
あまり重要ではない項目で画面がいっぱいになりがちです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/77d1400b-21ff-921c-c049-9524a9f027a7.png)


### カスタムCaster

psyがオブジェクトをダンプする際に `Symfony\Component\VarDumper` が利用されています。
psyshでは設定でcasterを定義することで、クラスに応じたダンプ出力方法をカスタマイズできます。

VarDumperのcaster用関数 `varDumperCast()`を書いて、
それを、 UserモデルとMessageモデルの共通の親クラス`App\Models\Base`に適用してみます。

```php:.psysh.php
<?php

// VarDumper で出力する際に、オブジェクトのプロパティをフィルタリングする
function varDumpCast($object, $array)
{
    // 後方一致するキーを抽出
    $matchedKeys = array_filter(array_keys($array), function ($key) use ($array) {
        // キーを分解してプロパティ名を取得
        [, , $prop] = explode("\0", $key);

        // 以下なら残す
        //   - プロパティ名が `id`か`name`か末尾が`_id`
        //   - オブジェクト、配列
        return $prop === 'id'
            || $prop === 'name'
            || substr_compare($prop, '_id', -3) === 0
            || is_object($array[$key])
            || is_array($array[$key]);
    });

    // マッチしたキーのみを持つ配列を作成
    $filteredArray = array_intersect_key($array, array_flip($matchedKeys));

    return $filteredArray;
}


return [
    // ...

    'casters' => [
        'App\Models\Base' => 'varDumpCast',
    ],
];
```


めっちゃスッキリした！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/7d20188d-f519-fc7b-9855-9779cd2f5fff.png)

### 他のアトリビュートをコンパクトに出したい

他のアトリビュートを完全に隠すのではなく、コンパクトに出したい場合は、
1行のjsonにして加えてあげたらどうでしょう。

```php:varDumpCastを少し修正
function varDumpCast($object, $array)
{
    ...

    // return $filteredArray;
    return [
        // 1行のjsonサマリを追加
        "#" => json_encode($object->getAttributes(), JSON_UNESCAPED_UNICODE, 1),
        ...$filteredArray,
    ];
}
```

以下のように1行の長いjson文字列が表示されるようになりますが、折り返さない設定のページャーだったら邪魔にはならないでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111017/ac16d3ee-a3b5-ca88-d61b-93b3c759ddb0.png)


### モデル毎にcasterを使い分ける

casterはクラスに対して複数定義できるので、
モデル毎にcaster用関数を使い分けて、モデルに応じてもっと細かな制御をしても良いですね。

```php:.psysh.php(各モデル毎にcasterを使い分ける)
    'casters' => [
        // 各モデル用に出力をカスタマイズ
        'App\Models\User' => 'App\Models\User::varDumpCast',
        'App\Models\Message' => 'App\Models\Message::varDumpCast',
    ],
```


## 最後に

それでも皆さん、良い年末を！
明日は @ockeghem センセです！