Laravel Requestが全部string型なのを何とかする

# はじめに
開発ポリシーとして

```php
<?php
declare(strict_types=1);
```
を指定し、[強い型付け](https://www.php.net/manual/ja/functions.arguments.php#functions.arguments.type-declaration.strict)制約をしています。

その環境下で開発していたControllerでこんなエラーが。

```
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to show() must be of the type integer, string given,

// 訳：おい、show()メソッドの引数にstring渡してるけど、integerしか受け付けねーから！
```

そのコードがこれ。

```php
// Controller
public function index(Request $request)
{
    return $this->service->show($request->get('id'));
}

// Service
public function show(int $id): array
{
    return $this->model->detail($id)->toArray();
}
```

？？？
$request->idはintでしょ？

## 実験コード

```PHP
public function index(Request $request)
{
    var_dump($request-all());
}
```

## GET

```
// idが数字、is_deletedがbooleanを指定しているつもり
http://xxxx.com/show?id=1&is_deleted=true
```

結果

```
  'id' => string '1' (length=1)
  'is_deleted' => string 'true' (length=4)
```

## POST

```
<form method="POST" action="http://xxxx.com/show">
    <input type="text" name="id">
    <input type="text" name="id_deleted">
</form>
```

結果

```
  'id' => string '1' (length=1)
  'is_deleted' => string 'true' (length=4)
```

### 悲報

`全部stringだった ＼(^o^)／`


# じゃあcastすればいいじゃん
いやーそうなんですけど、１個や2個のパラメータならともかく、ちょっと多くなると面倒くさいし見づらくなりません？

```php
public function index(Request $request)
{
    return $this
        ->service
        ->show(
            (int)$request->get('id'),
            (bool)$request->get('is_deleted'),
            (float)$request->get('lat'),
            (float)$request->get('lon')
        );
}
//　なんかダサい
```

# よしなにキャストしてくれればいいのになー
コントローラに着弾した時点でRequestの値が適切にキャストされていて、安全に使えないないか、と考えていました。
そのときに気づいたのは、
「あれ、`コントローラに着弾した時点で`、`安全な値が担保されている`って、Laravelの[FromRequestValidation](https://laravel.com/docs/6.x/validation#form-request-validation)と似てるな」 
ということ。

なるほど、FormRequestで値のクリーニングもしちゃえばいいのか。

# やってみた
こんな感じのFormRequest拡張を作って・・・

```php
<?php
declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest as BaseFromRequest;

class FormRequest extends BaseFromRequest
{
    /**
     * @var array キャストルール
     */
    protected $casts = [];

    /**
     * 整形したパラメータを返却する
     * @param string $key
     * @return bool|int|array|string|mixed
     */
    public function shaped(string $key)
    {
        $target = $this->trim($this->get($key, null));
        if (empty($this->casts)) {
            return $target;
        }
        if (!isset($this->casts[$key])) {
            return $target;
        }
        return $this->cast($this->casts[$key], $target);
    }

    /**
     * パラメータ前後の空白等を削除
     * @param mixed $target
     * @return array|string|null
     */
    private function trim($target)
    {
        if (is_null($target)) {
            return null;
        }
        if (!is_array($target)) {
            return trim($target);
        }
        $trimmed = [];
        foreach ($target as $value) {
            $trimmed[] = trim($value);
        }
        return $trimmed;
    }

    /**
     * パターンに応じてキャストする
     * @param string $castPattern
     * @param mixed $target
     * @return bool|int|array|string|mixed
     */
    private function cast(string $castPattern, $target)
    {
        switch ($castPattern) {
            case 'integer':
                return (int)$target;
                break;

            case 'bool':
                if ($target === 'true') {
                    return true;
                }
                if ($target === 'false') {
                    return false;
                }
                throw new Exception();
                break;

            case 'json_decode':
                return json_decode($target, true);
                break;

            case 'string':
            default:
                return $target;
        }
    }
}
```

バリデーションを書くFormRequestクラスは↑のクラスをextendするように・・・

```php
<?php
declare(strict_types=1);

namespace App\Http\Requests;

class CustomerRequest extends FormRequest
{
    /**
     * @var array キャストルール
     */
    protected $casts = [
        'id' => 'integer',
        'is_deleted' => 'bool',
    ];

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'id' => 'integer',
            'is_deleted' => 'bool',
        ];
    }
}
```

それをコントローラでインジェクションしたならば・・・

```php
public function index(CustomerRequest $request)
{
    var_dump($request->shaped('id'));
    var_dump($request->shaped('is_deleted'));
}
```

```
ExampleController.php:34:int 1
ExampleController.php:35:boolean true
```

これでクールなコントローラが書けそうです。


# 注釈
- 題名にはLaravelと記載しましたが、そもそもPHPへ送られてくるGETリクエストや、POSTリクエストのContent-Typeに”application/x-www-form-urlencoded”、”multipart/form-data”が指定されていた場合全部string型（かarray）です。
- 抜本的に解決するためには、”application/json”でリクエストするとLaravelはちゃんと型を解決します。