mcpField
L
laravel-dtos

laravel-dtos

github.com/leeovery開発1549 回インストール2 週間前に更新
リポジトリ

laravel-dtos について

Spatie Laravel Data を使う Laravel 開発者向けに、型付きデータオブジェクトの変換と利用方法を定める DTO ガイドです。配列変換、複雑なマッピング用の Transformer、モデルキャスト、テストファクトリーを扱います。

主な機能

  1. 01Spatie Laravel Data の利用
  2. 02from() による配列変換
  3. 03型付き・ネストデータオブジェクト
  4. 04複雑なマッピングに Transformer を利用
  5. 05DTO テストファクトリーの作成
  6. 06モデルフィールドを DTO にキャスト

活用シーン

  1. 01リクエストデータを Action に渡す
  2. 02外部 API や webhook のデータ変換
  3. 03モデルの JSON フィールドで DTO を保存・取得
  4. 04テスト用に値を設定した DTO を生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/leeovery/agentic-skills --skill laravel-dtos

インストール後、Claude Code で/laravel-dtosを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
AddressDataFactory.php
AppServiceProvider.php
Data.php
DataTestFactory.php
dto-transformers.md
EmailFormatter.php
HasTestFactory.php
helpers.php
test-factories.md
TraceDataFactory.php

詳細

Laravel 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}DataOrderData、UserData、ProductData
リクエスト DTO{Action}{Entity}DataCreateOrderData、UpdateUserData
ネストした DTO{Descriptor}{Entity}DataShippingAddressData、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、フィールドマッピングなどの複雑な変換には、専用のトランスフォーマークラスを使用します。

→ 詳細ガイド:dto-transformers.md

// External system data
$data = PaymentDataTransformer::fromStripePaymentIntent($webhook['data']);

// Request with version-specific field names
$data = OrderDataTransformer::fromRequest($request);

優先順位:

  1. Data::from($array) - シンプルなケース、直接マッピング
  2. Data::fromRequest() - 小規模なアプリ向けの、DTO 上の静的メソッド
  3. Transformer::from*() - 複雑な変換、複数のソース

テストファクトリー

HasTestFactory トレイトを使用して、テスト用のデータを設定した DTO を作成します。

→ 詳細ガイド:test-factories.md

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

コメント