mcpField
L
laravel-dtos

laravel-dtos

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

laravel-dtos について

laravel-dtosは、LaravelアプリケーションでDTOを構成するための開発スキルです。Spatie Laravel Dataを使うチーム向けに、型付きDTO、明示的なマッピング、データ変換、モデルキャスト、テストファクトリーの指針をまとめています。

主な機能

  1. 01型付きプロパティとコンストラクタープロモーション
  2. 02配列からの`::from()`変換
  3. 03明示的なフィールドマッピング
  4. 04複雑な入力向けDTOトランスフォーマー
  5. 05モデルキャストとテストファクトリー

活用シーン

  1. 01リクエストデータのDTO変換
  2. 02外部APIやwebhookデータのマッピング
  3. 03型を使ったネストされたドメインデータの表現
  4. 04テスト用の値設定済みDTOの作成

インストール

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

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

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

同梱ファイル

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

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

詳細

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

コメント