40
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-06

はじめに

開発ポリシーとして

<?php
declare(strict_types=1);

を指定し、強い型付け制約をしています。

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

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

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

そのコードがこれ。

// 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でしょ?

実験コード

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すればいいじゃん

いやーそうなんですけど、1個や2個のパラメータならともかく、ちょっと多くなると面倒くさいし見づらくなりません?

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と似てるな」
ということ。

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

やってみた

こんな感じのFormRequest拡張を作って・・・

<?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
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',
        ];
    }
}

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

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はちゃんと型を解決します。
40
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?