【Laravel Validated DTO】RequestなどにもDTOを使って型プログラミンをしよう！

神ライブラリを見つけてしまった...。

https://wendell-adriel.gitbook.io/laravel-validated-dto

しかも待望の `TypeScript` アプデ...。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/991bb623-7ee2-4de6-8539-77198963228b.png)

## はじめに

`DTO` とは `Data Transfer Object` の略で、要するにメソッド間クラス間でデータをやり取りする箱のようなもの。
すでに [spatie/laravel\-data](https://github.com/spatie/laravel-data) のようなライブラリはあるが、このライブラリは `Http Request` や `Artisan arguments` に標準対応しているのが優位点。

私の考えだと Web アプリにガチガチな設計は必要ないと思っている。
ただ、データの箱はしっかり用意して、TypeHintを多用するプログラミングがベスト。
（要するに TypeScript なのだが）

私のプロジェクトで標準採用したいくらいに良かったので紹介する。
ドキュメントを見る限り、非常に多くの書き方があるが、使いそうなものをピックアップする。


## 環境構築

ちゃちゃっと `asdf` と `Laravel Installer` を使って必要な環境を作る。

<details><summary>環境構築はこちらから</summary>

`php` などは `asdf` を利用した。
動けば `Docker` でもなんでもOK。

```bash
$ php -v
PHP 8.4.8 (cli) (built: Jun 12 2025 20:46:14) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.8, Copyright (c) Zend Technologies
$ composer -V
Composer version 2.8.9 2025-05-13 14:01:37
PHP version 8.4.8 (/home/wsl/.asdf/installs/php/8.4.8/bin/php)
Run the "diagnose" command to get more detailed diagnostics output.
$ node -v
v24.2.0

$ composer global require laravel/installer
$ asdf reshim
```

`Laravel` 環境を作る。
CSRF とか面倒なので API で実験する。

```bash
$ laravel new test-laravel-dto
 ┌ Which starter kit would you like to install? ────────────────┐
 │ Vue                                                          │
 └──────────────────────────────────────────────────────────────┘
 ┌ Which authentication provider do you prefer? ────────────────┐
 │ Laravel`s built-in authentication                            │
 └──────────────────────────────────────────────────────────────┘
 ┌ Which testing framework do you prefer? ──────────────────────┐
 │ Pest                                                         │
 └──────────────────────────────────────────────────────────────┘
 ┌ Would you like to run npm install and npm run build? ────────┐
 │ Yes                                                          │
 └──────────────────────────────────────────────────────────────┘

$ php artisan install:api
> One new database migration has been published. Would you like to run all pending database migrations? (yes/no) [yes]:
> yes

$ npm run build
$ php artisan serve
```

`Vue` や `React` をそのままインストールできるようになったの神やな...
そこに `Validated DTO for Laravel` を導入する。

```bash
$ composer require wendelladriel/laravel-validated-dto
```

</details>





## Http Request で使ってみる

ブログを投稿する場面を想定して、 `FormRequest` の代わりに利用する例を書く。
ちなみに make コマンドに対応している。

```bash
$ php artisan make:dto PostDto
```

```php:app/DTOs/PostDto.php
<?php

namespace App\DTOs;

use WendellAdriel\ValidatedDTO\Attributes\Rules;
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
use WendellAdriel\ValidatedDTO\ValidatedDTO;

class PostDto extends ValidatedDTO
{
    use EmptyRules;
    use EmptyCasts;
    use EmptyDefaults;

    #[Rules(['required', 'string', 'min:3', 'max:16'])]
    public string $title;

    #[Rules(['nullable', 'string', 'max:255'])]
    public ?string $content;
}
```

ルールは PHP の新機能 [Attributes](https://www.php.net/manual/ja/language.attributes.overview.php) を利用して記載できる。
生成時点では `rules()` や `defaults()` があるが、必須ではないので、これらは Empty トレイトを差し込んでおく。 

これを `/test` ルートの引数に書き込む。

```php:routes/api.php
<?php

use App\DTOs\PostDto;
use Illuminate\Support\Facades\Route;

Route::post('/test', function (PostDto $request) {
    return response()->json([
        'status' => 'ok',
        'request' => $request,
    ]);
});
```

ではこの API を叩いてみる。

### API の実行（成功）

`Postman` か何かを使うと簡単。
Qiita なので、以下は `curl + jq` で記載する。

```bash
$ curl -s -X POST \
--location 'http://localhost:8000/api/test' \
--header 'X-Requested-With: XMLHttpRequest' \
--header 'Content-Type: application/json' \
--data '{
    "title": "タイトル",
    "content": "ブログの記事です。"
}' \
| jq
```

```json
{
  "status": "ok",
  "request": {
    "title": "タイトル",
    "content": "ブログの記事です。"
  }
}
```


### API の実行（エラー）

ということで、今度は空の body を送り付けてみる。

```bash
$ curl -s -X POST \
--location 'http://localhost:8000/api/test' \
--header 'X-Requested-With: XMLHttpRequest' \
--header 'Content-Type: application/json' \
| jq
```

```json
{
  "message": "The title field is required.",
  "errors": {
    "title": [
      "The title field is required."
    ]
  }
}
```

`Form Request` と同じように `422 Validation Exception` が表示された。
（英語なのは、日本語化を一切行っていないから）

で、なにが凄いのか？


## Artisan コマンドの引数に使う

先ほど `FormRequest` に利用したが、なんと `Artisan Command` にも利用できる。
git のような名前とメールアドレスを引数に持つコマンドを作成してみる。

```bash
$ php artisan make:command TestCommand
$ php artisan make:dto TestCommandDto
```

```php:app/DTOs/TestCommandDto.php
<?php

namespace App\DTOs;

use WendellAdriel\ValidatedDTO\Attributes\Rules;
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
use WendellAdriel\ValidatedDTO\ValidatedDTO;

class TestCommandDto extends ValidatedDTO
{
    use EmptyRules;
    use EmptyCasts;
    use EmptyDefaults;

    #[Rules(['required', 'string', 'max:16'])]
    public string $username;

    #[Rules(['required', 'string', 'email', 'max:255'])]
    public ?string $email;
}
```

作った DTO をコマンドから呼び出す。

```php:app/Console/Commands/TestCommand.php
<?php

namespace App\Console\Commands;

use App\DTOs\TestCommandDto;
use Illuminate\Console\Command;

class TestCommand extends Command
{
    protected $signature = 'app:test {--username=} {--email=}';
    protected $description = 'Command description';

    public function handle()
    {
        $dto = TestCommandDto::fromCommand($this);
        dd($dto->toArray());
    }
}

```

```bash
$ php artisan app:test --username 山田 --email yamada@example.com
array:2 [
  "username" => "山田"
  "email" => "yamada@example.com"
]
```

すごい。
エラーが発生するパラメタを送ってみると、ちゃんと `Validation Exception` で止まる。

```bash
$ php artisan app:test --username 山田 --email yamada
  Illuminate\Validation\ValidationException 
  The email field must be a valid email address.
```

これがあれば、面倒なバリデーション処理を一切自作しなくて済む。
`fromCommand` だと引数とオプション引数がまとめて取得されるので、搾りたい場合は `fromCommandArguments` `fromCommandOptions` を利用しよう。






## TypeHint が使える

Laravel の Request 最大の欠点は、`$request->input('value')` や `$request->validated()` という形で、文字列や配列を利用しなければならないところだと思う。
つまりエディタの支援をフルに使えない。（PHPStorm はどうだったっけ...）

しかし、この DTO を挟むと TypeHint が利用でき、一般 Class と同等に扱うことができる。
素晴らしい。。。
AI 時代に堅牢なプログラミングを行うにも、有用なロジックだと思う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/9d15ae38-53fd-4c69-ab8b-6f09c3d0089e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/1d6914b4-1454-4df0-ac1b-d469cd49bd41.png)

ちなみに今回使用した `ValidatedDTO` は `FormRequest` と同等な機能が備わっている。
なので `messages()` や `after()` もそのまま利用可能。

従来の `FormRequest` をそのまま置き換えられるのが神ポイント。




## TypeScript の型を生成できる

今回見つけるに至った神機能。
`Laravel` と `TypeScript` を連動させようと思ったとき、型の同期問題が付きまとう。
[spatie/typescript\-transformer](https://github.com/spatie/typescript-transformer)  や [typegen](https://github.com/7nohe/laravel-typegen) を使って生成できるが、やはり `FormRequest` からの動的生成は微妙にうまくいかない事が多い...。

この機能は [spatie/laravel\-typescript\-transformer](https://github.com/spatie/laravel-typescript-transformer?tab=readme-ov-file) を利用するようなので導入する。

```bash
$ composer require spatie/laravel-typescript-transformer
$ php artisan vendor:publish --provider="Spatie\LaravelTypeScriptTransformer\TypeScriptTransformerServiceProvider"
```

そして `config` に DTO の機能を紐づける。

```diff_php:config/typescript-transformer.php
      'collectors' => [
          Spatie\TypeScriptTransformer\Collectors\DefaultCollector::class,
          Spatie\TypeScriptTransformer\Collectors\EnumCollector::class,
+         WendellAdriel\ValidatedDTO\Support\TypeScriptCollector::class,
      ],
  
      'transformers' => [
          Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer::class,
          Spatie\TypeScriptTransformer\Transformers\EnumTransformer::class,
          Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer::class,
          Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer::class,
+         WendellAdriel\ValidatedDTO\Support\TypeScriptTransformer::class,
      ],
```

コマンド実行！

```bash
$ php artisan typescript:transform --format
+-------------------------+-------------------------+
| PHP class               | TypeScript entity       |
+-------------------------+-------------------------+
| App\DTOs\TestCommandDto | App.DTOs.TestCommandDto |
| App\DTOs\PostDto        | App.DTOs.PostDto        |
+-------------------------+-------------------------+
Transformed 2 PHP types to TypeScript
```

すると DTO を自動認識して type ファイルを生成してくれる。
（この二つは後続で紹介するものです。）

```ts:resources/types/generated.d.ts
declare namespace App.DTOs {
    export type PostDto = {
        title: string;
        content: string | null;
    };
    export type TestCommandDto = {
        username: string;
        email: string | null;
    };
}
```

すばらしい...。
世の中では OpenAPI を使ってフロントとバックを制限する風潮があるが、私の考えだと Laravel 基準ですべてを制御すべきと考えているので、ベストマッチなライブラリである...。

デフォルトだと `App.DTOs` の名前空間に生成されるので、もし必要ないなら、`sed` で消すとよい。
適当スクリプトなので、各自調整を。

```bash
$ php artisan typescript:transform --format
$ sed -i -r -e '/^declare namespace App.DTOs \{/d' -e '/^\}/d' -e 's/^    //' ./resources/types/generated.d.ts
```





## DB の JSON 型をマッピングする

さらに！これは通常のDTOとしても利用できる。
昨今 DB の性能が上がって、RDBの `JSON列` を雑に使っても大丈夫になってきた。
そんなとき問題になるのが、キーの管理。

先ほどの Post にメタデータを追加した Model と Migration を作ってみる。

```bash
$ php artisan make:model -m Post
```

```php:database/migrations/2025_xx_xx_xxxxxx_create_posts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content')->nullable();
            $table->json('meta')->comment('metadata');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
```

```php:app/Models/Post.php
<?php

namespace App\Models;

use App\DTOs\PostMetaDto;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'content',
        'meta',
    ];

    protected $casts = [
        'meta' => PostMetaDto::class,
    ];
}
```

この中で出てきている `PostMetaDto` はこんな感じに作ってみた。

```php:app/DTOs/PostMetaDto.php
<?php

namespace App\DTOs;

use Illuminate\Validation\Rules\Enum;
use WendellAdriel\ValidatedDTO\Attributes\Rules;
use WendellAdriel\ValidatedDTO\Concerns\EmptyCasts;
use WendellAdriel\ValidatedDTO\Concerns\EmptyDefaults;
use WendellAdriel\ValidatedDTO\Concerns\EmptyRules;
use WendellAdriel\ValidatedDTO\ValidatedDTO;

enum PostVisibility: string
{
    case Public = 'public';
    case Private = 'private';
    case Unlisted = 'unlisted';
}

class PostMetaDto extends ValidatedDTO
{
    use EmptyRules;
    use EmptyCasts;
    use EmptyDefaults;

    #[Rules(['required', 'string', 'size:6'])]
    public string $deleteKey;
    
    #[Rules(['required', new Enum(PostVisibility::class)])]
    public string $visiblity;
}
```

これを使ってみる。

使うときに Model の TypeHint を使いたいため、[barryvdh/laravel\-ide\-helper](https://github.com/barryvdh/laravel-ide-helper)　を入れる。

```bash
$ php artisan migrate

$ composer require --dev barryvdh/laravel-ide-helper
# ほんとは --nowrite を付けたほうがきれい
$ php artisan ide-helper:model --write
```

すると、なんと JSON にも TypeHint がつく。
神だねぇ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/e2d68ccc-fd7c-4e2d-969b-4ef0fdfdef62.png)




















## おわりに

久しぶりに神パッケージを見つけてしまったので、ちゃんとした記事を書きました。
AI 時代こそ、堅牢なつくりにする必要があるので、今後必須機能になると思いましたまる。

ほかにも `Cast` や `Transform` 、`FieldMapping` にも対応してるので、

やっぱりバックエンドを想定すると、Laravel が欲しくなるんだよね...。
コミュニティが偉大すぎる。