はじめに
開発ポリシーとして
<?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はちゃんと型を解決します。