Laravel 標準搭載のデバッグ機能 tinker コマンドのご紹介

## Laravel tinker(REPL)

Laravelフレームワークに標準搭載されている対話シェルです。
また、[psysh](https://psysh.org) のラッパーライブラリです。

## REPL(リプル)

Read Eval Print Loop の略で対話型実行環境のことです。
キーボードから打ち込まれた命令を読み込み(Read)、評価・実行し(Eval)、結果を画面に表示し(Print)、また命令待ちの状態に戻る(Loop)処理系。

手軽にコードを実行できますが、誤字があればもちろんエラーになるので短いコードを実行して結果を確認する使い方をされます。

## PHP の対話シェル

`php` コマンドの `-a` オプションを付けるとPHP標準の対話シェルが使用できます。

Laravelのコードはオートローダーを読み込んでいないので実行できません。
PHPの標準関数等を試す場合はこちらで十分でしょう。

```shell-session
$ php -a
```

- https://www.php.net/manual/ja/features.commandline.interactive.php

## tinker 使い方

軽くtinkerの使い方をご紹介します。

```shell-session
$ php artisan tinker
```

```shell-session
>>> exit
```

tinkerから抜けたい場合は `exit` で抜けれます。
tinker上で実行していることを分かりやすくするように `>>> ` を表記してます。
また、tinker上では `;` を省略できます。今回は丁寧に `;` 書いてます☺️

### tinkerの使いどころ？

- テストデータを作るときFakerの値を確認したい時
  - [Laravel tinker で Faker を試す方法](https://qiita.com/ucan-lab/items/bc421788d4a352afc6ea)
- 試しにコントローラを動かしたい時
  - [Laravel コントローラをtinkerから実行する](https://qiita.com/ucan-lab/items/da3441b7a9e5c79f759d)
- EloquentのSQLの実行結果を見たい時
  - https://qiita.com/ucan-lab/items/753cb9d3e4ceeb245341
- 実行時のconfigの値を確認したい時

私はちょっとした確認やブラウザ叩くの面倒だった時とかにtinker使ってます。

## help コマンド

`help` コマンドを実行すると利用できるコマンド一覧が表示されます。

```
>>> help
  help             Show a list of commands. Type `help [foo]` for information about [f
  ls               List local, instance or class variables, methods and constants.    
  dump             Dump an object or primitive.                                       
  doc              Read the documentation for an object, class, constant, method or pr
  show             Show the code for an object, class, constant, method or property.  
  wtf              Show the backtrace of the most recent exception.                   
  whereami         Show where you are in the code.                                    
  throw-up         Throw an exception or error out of the Psy Shell.                  
  timeit           Profiles with a timer.                                             
  trace            Show the current call stack.                                       
  buffer           Show (or clear) the contents of the code input buffer.             
  clear            Clear the Psy Shell screen.                                        
  edit             Open an external editor. Afterwards, get produced code in input buf
  sudo             Evaluate PHP code, bypassing visibility restrictions.              
  history          Show the Psy Shell history.                                        
  exit             End the current session and return to caller.                      
  clear-compiled   Remove the compiled class file                                     
  down             Put the application into maintenance mode                          
  env              Display the current framework environment                          
  optimize         Cache the framework bootstrap files                                
  up               Bring the application out of maintenance mode                      
  migrate          Run the database migrations                                        
  inspire          Display an inspiring quote
```

コマンド名を引数に渡すとそのコマンドの詳細な説明を確認できます。

```
>>> help ls
Usage:
 ls [--vars] [-c|--constants] [-f|--functions] [-k|--classes] [-I|--interfaces] [-t|--

Aliases: dir

Arguments:
 target             A target class or object to list.

Options:
 --vars             Display variables.
 --constants (-c)   Display defined constants.
 --functions (-f)   Display defined functions.
 --classes (-k)     Display declared classes.
 --interfaces (-I)  Display declared interfaces.
 --traits (-t)      Display declared traits.
 --no-inherit       Exclude inherited methods, properties and constants.
 --properties (-p)  Display class or object properties (public properties by default).
 --methods (-m)     Display class or object methods (public methods by default).
 --grep (-G)        Limit to items matching the given pattern (string or regex).
 --insensitive (-i) Case-insensitive search (requires --grep).
 --invert (-v)      Inverted search (requires --grep).
 --globals (-g)     Include global variables.
 --internal (-n)    Limit to internal functions and classes.
 --user (-u)        Limit to user-defined constants, functions and classes.
 --category (-C)    Limit to constants in a specific category (e.g. "date").
 --all (-a)         Include private and protected methods and properties.
 --long (-l)        List in long format: includes class names and method signatures.
 --help (-h)        Display this help message.

Help:
 List variables, constants, classes, interfaces, traits, functions, methods,
 and properties.
 
 Called without options, this will return a list of variables currently in scope.
 
 If a target object is provided, list properties, constants and methods of that
 target. If a class, interface or trait name is passed instead, list constants
 and methods on that class.
 
 e.g.
 >>> ls
 >>> ls $foo
 >>> ls -k --grep mongo -i
 >>> ls -al ReflectionClass
 >>> ls --constants --category date
 >>> ls -l --functions --grep /^array_.*/
 >>> ls -l --properties new DateTime()
```

### ls コマンド(変数の表示)

`ls` コマンドはローカル、インスタンス、またはクラスの変数、メソッド、定数をリスト表示します。

```
>>> $foo = 'bar';
=> "bar"
```

`$foo` ローカル変数を定義した場合

```
>>> ls
Variables: $foo

>>> ls -l
Variables:
  $foo   "bar" 
```

`-g` グローバル変数オプションを付けるとグローバル変数も覗けます。

```
>>> ls -gl

Global Variables:
  $app                         Illuminate\Foundation\Application {#2 …15}       
  $argc                        2                                                
  $argv                        [ …2]                                            
  $GLOBALS                     [ …14]                                           
  $input                       Symfony\Component\Console\Input\ArgvInput {#25}  
  $kernel                      App\Console\Kernel {#28}                         
  $_COOKIE                     []                                               
  $_ENV                        [ …39]                                           
  $_FILES                      []                                               
  $_GET                        []                                               
  $_POST                       []                                               
  $_REQUEST                    []                                               
  $_SERVER                     [ …152]                                          
  $__composer_autoload_files   [ …22] 
```

## REPLデバッグ

### 準備

`server.php` に `define('STDIN', fopen('php://stdin', 'r'));` の行をファイルの先頭に追加します。

```php:server.php
<?php

define('STDIN', fopen('php://stdin', 'r'));

// ... 省略
```

### ブレークポイントの設定

`eval(\Psy\sh());` のコードを1行追加します。
例として `routes/web.php` に置きます。

```php:routes/web.php
Route::get('/{name}', function (string $name) {
    $message = 'Hello ' . $name;
    eval(\Psy\sh());

    return $message;
});
```

#### 補足: PhpStorm コードスニペット

`eval(\Psy\sh());` を手入力するのは割と手間なので、コードスニペットとして登録すると便利です。

- `Editor` > `Live Templates` > `PHP` を選択して `+`
  - `Abbreviation`: `tinker`
  - `Template text`: `eval(\Psy\sh());`
  - `Define`: `PHP` を選択

`tinker` を打ったあとに`tab`キー押下で変換できます。

### ビルトインサーバーを利用してステップ実行する

Laravelビルトインサーバーを起動します。
http://127.0.0.1:8000/yourname

```
$ php artisan serve
Laravel development server started: http://127.0.0.1:8000
[Wed May  6 17:10:33 2020] PHP 7.4.5 Development Server (http://127.0.0.1:8000) started
[Wed May  6 17:10:36 2020] 127.0.0.1:50088 Accepted
[Wed May  6 17:10:36 2020] 127.0.0.1:50089 Accepted
Psy Shell v0.10.3 (PHP 7.4.5 — cli-server) by Justin Hileman
From /laravel/routes/web.php:18:
    16: Route::get('/{name}', function (string $name) {
    17:     $message = 'Hello ' . $name;
  > 18:     eval(\Psy\sh());
    19: 
    20:     return $message;

```

ブレークポイントで処理が一時停止します。(ブラウザの描画も停止します)
`exit` を入力すると処理を継続します。

`help` コマンドなどtinker上のコマンドも使用できます。
`whereami`, `show`, `trace` 等を使うとデバッグが便利です。

## Laravelコードのお試し実行

tinkerではLaravelのコードも実行できます。
今回はEloquentモデルからデータの作成と取得の一例をご紹介します。
簡単に試せることをお伝えしたいだけですので、詳しいことは公式ドキュメントをご参照ください。

- https://readouble.com/laravel/7.x/ja/eloquent.html

### ファクトリーメソッドでUserモデルクラスを作成

```shell-session
>>> factory(App\User::class)->create();

=> App\User {#3042
     name: "Prof. Armani Eichmann Jr.",
     email: "porter.feil@example.com",
     email_verified_at: "2020-05-05 10:00:49",
     updated_at: "2020-05-05 10:00:49",
     created_at: "2020-05-05 10:00:49",
     id: 1,
   }
```

### Userモデルクラスを new して作成

```shell-session
>>> $user = new App\User();
>>> $user->name = 'foo';
>>> $user->email = 'foo@example.com';
>>> $user->email_verified_at = now();
>>> $user->password = bcrypt('secret');
>>> $user->remember_token = Str::random(10);
>>> $user->save();

>>> $user; // 変数名で中身を表示できます。
=> App\User {#3039
     updated_at: "2020-05-05 10:08:52",
     created_at: "2020-05-05 10:08:52",
     name: "foo",
     email: "foo@example.com",
     email_verified_at: "2020-05-05 10:09:23",
     id: 2,
   }
```

### Userモデルクラスを create して作成

```shell-session
>>> $attributes = [
    'name' => 'bar',
    'email' => 'bar@example.com',
    'email_verified_at' => now(),
    'password' => bcrypt('secret'),
    'remember_token' => Str::random(10),
];

>>> App\User::create($attributes);
=> App\User {#3072
     name: "bar",
     email: "bar@example.com",
     updated_at: "2020-05-05 10:15:31",
     created_at: "2020-05-05 10:15:31",
     id: 3,
   }
```

`User::create` の場合の補足です。

`$fillable` 複数代入を許可するかのプロパティに記述のない`remember_token` と `email_verified_at` のフィールドは値が入力されていません。

`$hidden` 非表示するかのプロパティに記述されている `password` と `remember_token` は tinker上でモデルを表示する際にもフィールドは出力されません。
`$user->toArray()`, `$user->all()`, `$user->toJson()` にも影響します。

`$casts` プロパティでキャスト宣言が行えます。主に日付の形式を設定することが多いです。

```php:app/User.php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
```

### モデルの取得(全件)

```shell-session
>>> App\User::all();
=> Illuminate\Database\Eloquent\Collection {#3024
     all: [
       App\User {#3025
         id: 1,
         name: "Prof. Armani Eichmann Jr.",
         email: "porter.feil@example.com",
         email_verified_at: "2020-05-05 10:00:49",
         created_at: "2020-05-05 10:00:49",
         updated_at: "2020-05-05 10:00:49",
       },
       App\User {#3026
         id: 2,
         name: "foo",
         email: "foo@example.com",
         email_verified_at: "2020-05-05 10:09:23",
         created_at: "2020-05-05 10:08:52",
         updated_at: "2020-05-05 10:08:52",
       },
       App\User {#3027
         id: 3,
         name: "bar",
         email: "bar@example.com",
         email_verified_at: null,
         created_at: "2020-05-05 10:15:31",
         updated_at: "2020-05-05 10:15:31",
       },
     ],
   }
```

### モデルの取得(ID指定)

```shell-session
>>> App\User::find(2);
=> App\User {#3030
     id: 2,
     name: "foo",
     email: "foo@example.com",
     email_verified_at: "2020-05-05 10:09:23",
     created_at: "2020-05-05 10:08:52",
     updated_at: "2020-05-05 10:08:52",
   }
```

### モデルの取得(条件指定)

```shell-session
>>> App\User::where('name', 'bar')->get();
=> Illuminate\Database\Eloquent\Collection {#3021
     all: [
       App\User {#3016
         id: 3,
         name: "bar",
         email: "bar@example.com",
         email_verified_at: null,
         created_at: "2020-05-05 10:15:31",
         updated_at: "2020-05-05 10:15:31",
       },
     ],
   }
```

### SQLの取得

```shell-session
>>> App\User::where('name', 'bar')->toSql();
=> "select * from `users` where `name` = ?"
```

## 応用

### show コマンド(クラスを表示)

```shell-session
>>> show App\User
 9: class User extends Authenticatable
10: {
11:     use Notifiable;
12: 
13:     /**
14:      * The attributes that are mass assignable.
15:      *
16:      * @var array
17:      */
18:     protected $fillable = [
19:         'name', 'email', 'password',
20:     ];
21: 
22:     /**
23:      * The attributes that should be hidden for arrays.
24:      *
25:      * @var array
26:      */
27:     protected $hidden = [
28:         'password', 'remember_token',
29:     ];
30: 
31:     /**
32:      * The attributes that should be cast to native types.
33:      *
34:      * @var array
35:      */
36:     protected $casts = [
37:         'email_verified_at' => 'datetime',
38:     ];
39: }
```

### show コマンド(メソッドを表示)

```shell-session
>>> show App\Providers\RouteServiceProvider::map
38:     /**
39:      * Define the routes for the application.
40:      *
41:      * @return void
42:      */
43:     public function map()
44:     {
45:         $this->mapApiRoutes();
46: 
47:         $this->mapWebRoutes();
48: 
49:         //
50:     }
```

### doc コマンド(PHPマニュアル)

- https://github.com/bobthecow/psysh/wiki/PHP-manual
  - 日本語マニュアル: http://psysh.org/manual/ja/php_manual.sqlite

日本語ファイルをダウンロードして、
`~/.local/share/psysh/` または `/usr/local/share/psysh/` に配置します。

```
$ mkdir ~/.local/share/psysh/
$ curl -Lo ~/.local/share/psysh/php_manual.sqlite http://psysh.org/manual/ja/php_manual.sqlite
```

docコマンドが使用できるようになり、PHPマニュアルをtinker上で確認できます。

```
>>> doc abs
function abs($number)

Description:
  絶対値
  
  $number の絶対値を返します。

Param:
  mixed  $number  処理する数値。

Return:
  number  $number の絶対値を返します。もし $number の型が float であった場合、 返り値
          なります。それ以外の場合は 返り値の型は integer となります（float は、 integ
          り大きい値をとることがありえるからです）。

See Also:
   * gmp_abs()
   * gmp_sign()
```

## tinker 構成ファイルの公開

```shell-session
$ php artisan vendor:publish --provider="Laravel\Tinker\TinkerServiceProvider"
```

`config/tinker.php` ファイルが出力されます。
名前空間のエイリアス設定が行えます。

- https://github.com/laravel/tinker/blob/2.x/config/tinker.php

## tinkerコマンド履歴

`~/.config/psysh/psysh_history` に tinkerのコマンド履歴が残っていきます。
開発環境でdockerを利用する際はここを永続化してあげれば幸せになれます。

## 参考

- https://readouble.com/laravel/7.x/ja/artisan.html#tinker
- [インタラクティブシェル | ウィキペディア](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%82%B7%E3%82%A7%E3%83%AB)
- [REPL | ウィキトレイトペディア](https://ja.wikipedia.org/wiki/REPL)