laravel-dtos について
Spatie Laravel Data を使う Laravel 開発者向けに、型付きデータオブジェクトの変換と利用方法を定める DTO ガイドです。配列変換、複雑なマッピング用の Transformer、モデルキャスト、テストファクトリーを扱います。
主な機能
- 01Spatie Laravel Data の利用
- 02from() による配列変換
- 03型付き・ネストデータオブジェクト
- 04複雑なマッピングに Transformer を利用
- 05DTO テストファクトリーの作成
- 06モデルフィールドを DTO にキャスト
活用シーン
- 01リクエストデータを Action に渡す
- 02外部 API や webhook のデータ変換
- 03モデルの JSON フィールドで DTO を保存・取得
- 04テスト用に値を設定した DTO を生成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ leeovery/ agentic-skills --skill laravel-dtosインストール後、Claude Code で/laravel-dtosを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/AddressDataFactory.phpAppServiceProvider.phpData.phpDataTestFactory.phpdto-transformers.mdEmailFormatter.phpHasTestFactory.phphelpers.phptest-factories.mdTraceDataFactory.php詳細
Laravel DTO
複数のプリミティブ値を渡さないでください。 必ずデータオブジェクトでラップしてください。
関連ガイド:
- dto-transformers.md - 外部データを DTO に変換します
- test-factories.md - テスト用にデータを設定済みの DTO を作成します
考え方
DTO には次の利点があります。
- 型安全性と IDE の自動補完
- レイヤー間の明確な契約
- テストデータを簡単に生成するテストファクトリー
- バリデーションとの連携
- リクエストからドメインオブジェクトへの変換
Spatie Laravel Data パッケージ
Spatie Laravel Data を使用します。パッケージの機能については公式ドキュメントを参照してください。このガイドでは、プロジェクト固有のパターンと推奨事項を説明します。
配列には ::from() を使用する
キーがコンストラクターのプロパティ名と一致する配列には、常に ::from() を優先して使用してください。プロパティの型に基づくキャストはパッケージに任せます。
// ✅ PREFERRED - Let package cast automatically
$data = CreateOrderData::from([
'customerEmail' => $request->input('customer_email'),
'deliveryDate' => $request->input('delivery_date'), // String → CarbonImmutable
'status' => $request->input('status'), // String → OrderStatus enum
'items' => $request->collect('items'), // Array → Collection<OrderItemData>
]);
// ❌ AVOID - Manual casting in calling code
$data = new CreateOrderData(
customerEmail: $request->input('customer_email'),
deliveryDate: CarbonImmutable::parse($request->input('delivery_date')),
status: OrderStatus::from($request->input('status')),
items: OrderItemData::collect($request->input('items')),
);
::from() を優先する理由:
- コンストラクターのプロパティ型に基づく型キャストをパッケージが自動で処理します
- 手動キャストが不要になり、呼び出し側のコードがすっきりします
- 変換の動作に一貫性が生まれます
- パッケージの機能を最大限に活用できます
new を使用してもよい場合:
- すべての値を制御できるテストファクトリー
- 値がすでに正しい型になっている場合
- DTO コンストラクター内のフォーマッター
ケースマッパー属性を避ける
#[MapInputName] やケースマッパー属性は使用しないでください。 呼び出し側のコードでフィールド名を明示的にマッピングします。
// ❌ AVOID - Case mapper attributes on the class
#[MapInputName(SnakeCaseMapper::class)]
class CreateOrderData extends Data
{
public function __construct(
public string $customerEmail, // Auto-maps from 'customer_email'
) {}
}
// ✅ PREFERRED - Explicit mapping in calling code
CreateOrderData::from([
'customerEmail' => $request->input('customer_email'),
]);
ケースマッパーを避ける理由:
- 明示的なマッピングのほうが分かりやすく、保守しやすいため
- API のバージョンによってフィールド名が異なる場合があるため
- 変換クラスを見れば、すべてのマッピングを一か所で確認できるため
- 追跡しにくい暗黙的な動作を避けられるため
日付のキャストは自動で行われる
プロパティの型に基づいて、パッケージが日付文字列を Carbon または CarbonImmutable に自動でキャストします。想定する日付形式はパッケージの設定で指定してください。
// config/data.php
return [
'date_format' => 'Y-m-d H:i:s', // Or ISO 8601: 'Y-m-d\TH:i:s.u\Z'
];
class OrderData extends Data
{
public function __construct(
public CarbonImmutable $createdAt, // Automatically cast from string
public ?CarbonImmutable $shippedAt, // Nullable dates work too
) {}
}
// ✅ Just pass the string - package handles casting
$data = OrderData::from([
'createdAt' => '2024-01-15 10:30:00',
'shippedAt' => null,
]);
基本構造
<?php
declare(strict_types=1);
namespace App\Data;
use App\Enums\OrderStatus;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
/**
* @see \Database\Factories\Data\CreateOrderDataFactory
*/
class CreateOrderData extends Data
{
public function __construct(
public string $customerEmail,
public ?string $notes,
public ?CarbonImmutable $deliveryDate,
public OrderStatus $status,
/** @var Collection<int, OrderItemData> */
public Collection $items,
public ShippingAddressData $shippingAddress,
public BillingAddressData $billingAddress,
) {
// Apply formatters in constructor
$this->customerEmail = EmailFormatter::format($this->customerEmail);
}
}
主要なパターン
コンストラクターのプロパティプロモーション
常にプロモートプロパティを使用してください。
public function __construct(
public string $name,
public ?string $description,
public bool $active = true,
) {}
使用しない例:
public string $name;
public ?string $description;
public function __construct(string $name, ?string $description)
{
$this->name = $name;
$this->description = $description;
}
すべてに型を指定する
public string $email; // Required string
public ?string $phone; // Nullable string
public CarbonImmutable $createdAt; // DateTime (immutable)
public OrderStatus $status; // Enum
public Collection $items; // Collection
public AddressData $address; // Nested DTO
PHPDoc を使用したコレクション
/** @var int[] */
public array $productIds;
/** @var Collection<int, OrderItemData> */
public Collection $items;
ネストしたデータオブジェクト
class OrderData extends Data
{
public function __construct(
public CustomerData $customer,
public ShippingAddressData $shipping,
public BillingAddressData $billing,
/** @var Collection<int, OrderItemData> */
public Collection $items,
) {}
}
フォーマッター
コンストラクターでフォーマットを適用してください。
public function __construct(
public string $email,
public ?string $phone,
public ?string $postcode,
) {
$this->email = EmailFormatter::format($this->email);
$this->phone = $this->phone ? PhoneFormatter::format($this->phone) : null;
$this->postcode = $this->postcode ? PostcodeFormatter::format($this->postcode) : null;
}
フォーマッターの例(app/Data/Formatters/EmailFormatter.php):
<?php
declare(strict_types=1);
namespace App\Data\Formatters;
class EmailFormatter
{
public static function format(string $email): string
{
return strtolower(trim($email));
}
}
DTO の静的ファクトリーメソッド
小規模なアプリケーションや開発の初期段階では、DTO クラスに直接 from* メソッドを追加します。これにより、個別の変換クラスが必要になるほど複雑になる前に、ファクトリーのような動作を実現できます。
メソッドの命名規則: from{SourceType}(例:fromArray、fromRequest、fromModel)
class OrderData extends Data
{
public function __construct(
public string $customerEmail,
public ?string $notes,
public OrderStatus $status,
/** @var Collection<int, OrderItemData> */
public Collection $items,
) {}
public static function fromRequest(CreateOrderRequest $request): self
{
return self::from([
'customerEmail' => $request->input('customer_email'),
'notes' => $request->input('notes'),
'status' => $request->input('status'),
'items' => $request->input('items'),
]);
}
public static function fromModel(Order $order): self
{
return self::from([
'customerEmail' => $order->customer_email,
'notes' => $order->notes,
'status' => $order->status,
'items' => $order->items->toArray(),
]);
}
}
DTO の静的メソッドを使用する場合:
- DTO の数が少ない小規模なアプリケーション
- 専用のテストを必要としない単純な変換
- マッピングが単一の DTO と密接に結び付いている場合
個別の変換クラスを使用する場合:
- 複数の外部ソースを同じ DTO にマッピングする場合
- 広範なテストを必要とする複雑な変換ロジック
- 関心の分離が明確な大規模アプリケーション
モデルのキャスト
モデルの JSON カラムを DTO にキャストします。
class Order extends Model
{
protected function casts(): array
{
return [
'metadata' => OrderMetadataData::class,
'status' => OrderStatus::class,
];
}
}
使用例:
// Store
$order = Order::create([
'metadata' => $metadataData, // OrderMetadataData instance
]);
// Retrieve
$metadata = $order->metadata; // Returns OrderMetadataData instance
命名規則
| 種類 | パターン | 例 |
|---|---|---|
| レスポンス DTO | {Entity}Data | OrderData、UserData、ProductData |
| リクエスト DTO | {Action}{Entity}Data | CreateOrderData、UpdateUserData |
| ネストした DTO | {Descriptor}{Entity}Data | ShippingAddressData、OrderMetadataData |
ディレクトリー構造
app/Data/
├── CreateOrderData.php
├── UpdateOrderData.php
├── OrderData.php
├── Concerns/
│ └── HasTestFactory.php
├── Formatters/
│ ├── EmailFormatter.php
│ ├── PhoneFormatter.php
│ └── PostcodeFormatter.php
└── Transformers/
├── PaymentDataTransformer.php
├── Web/
│ └── OrderDataTransformer.php
└── Api/
└── V1/
└── OrderDataTransformer.php
コントローラーでの使用
コントローラーでは、変換クラスを介してリクエストを DTO に変換します。
<?php
declare(strict_types=1);
namespace App\Http\Web\Controllers;
use App\Actions\Order\CreateOrderAction;
use App\Data\Transformers\Web\OrderDataTransformer;
use App\Http\Web\Requests\CreateOrderRequest;
use App\Http\Web\Resources\OrderResource;
class OrderController extends Controller
{
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$order = $action(
user(),
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
}
アクションでの使用
アクションはパラメーターとして DTO を受け取ります。
class CreateOrderAction
{
public function __invoke(User $user, CreateOrderData $data): Order
{
return DB::transaction(function () use ($user, $data) {
return $user->orders()->create([
'customer_email' => $data->customerEmail,
'notes' => $data->notes,
'status' => $data->status,
]);
});
}
}
Transformers
外部 API、Webhook、フィールドマッピングなどの複雑な変換には、専用のトランスフォーマークラスを使用します。
// External system data
$data = PaymentDataTransformer::fromStripePaymentIntent($webhook['data']);
// Request with version-specific field names
$data = OrderDataTransformer::fromRequest($request);
優先順位:
Data::from($array)- シンプルなケース、直接マッピングData::fromRequest()- 小規模なアプリ向けの、DTO 上の静的メソッドTransformer::from*()- 複雑な変換、複数のソース
テストファクトリー
HasTestFactory トレイトを使用して、テスト用のデータを設定した DTO を作成します。
PHPDoc で DTO をファクトリーに関連付けます:
/**
* @see \Database\Factories\Data\CreateOrderDataFactory
* @method static CreateOrderDataFactory testFactory()
*/
class CreateOrderData extends Data
{
// ...
}
使用方法:
$data = CreateOrderData::testFactory()->make();
$collection = OrderItemData::testFactory()->collect(count: 5);
// With overrides
$data = CreateOrderData::testFactory()->make([
'customerEmail' => '[email protected]',
]);
DTO のテスト
use App\Data\CreateOrderData;
it('can create DTO from array', function () {
$data = CreateOrderData::from([
'customerEmail' => '[email protected]',
'notes' => 'Test notes',
'status' => 'pending',
]);
expect($data)
->customerEmail->toBe('[email protected]')
->notes->toBe('Test notes');
});
it('formats email in constructor', function () {
$data = new CreateOrderData(
customerEmail: ' [email protected] ',
notes: null,
status: OrderStatus::Pending,
);
expect($data->customerEmail)->toBe('[email protected]');
});