DTO در Laravel — Data Transfer Object برای داده ساختیافته
ساخت DTO با readonly class، Spatie Laravel Data و map از Request — جایگزین array پراکنده بین لایهها.
DTO (Data Transfer Object) در Laravel یک کلاس ساده و ترجیحاً immutable است که داده را با type مشخص بین لایهها جابهجا میکند — از Controller به Action یا Service، از Command به همان Action، یا به Job. DTO نه به دیتابیس وصل است، نه منطق business دارد؛ فقط «قرارداد» داده است.
چرا لازم میشود؟ $request->validated() یک array است — کار میکند تا وقتی کلید اشتباه تایپ شود، IDE autocomplete ندهد و refactor بشکند. با DTO امضای متد میگوید دقیقاً چه دادهای لازم است.
ساخت DTO با readonly class (PHP 8.2+)
readonly class CreatePostData
{
public function __construct(
public string $title,
public string $slug,
public string $content,
public ?string $excerpt = null,
) {}
public static function fromArray(array $data): self
{
return new self(
title: $data['title'],
slug: $data['slug'],
content: $data['content'],
excerpt: $data['excerpt'] ?? null,
);
}
}
readonly یعنی بعد از ساخت، هیچ property قابل تغییر نیست؛ دادهای که Action دریافت میکند همان است که validate شده.
تبدیل Form Request به DTO
class StorePostRequest extends FormRequest
{
public function rules(): array { /* ... */ }
public function toDto(): CreatePostData
{
return CreatePostData::fromArray($this->validated());
}
}
Validation و authorization در Form Request میماند و DTO فقط داده تأییدشده را حمل میکند — جزئیات validation در Form Request پیشرفته.
استفاده از DTO در Controller و Action
// Controller
public function store(StorePostRequest $request, CreatePostAction $createPost): RedirectResponse
{
$post = $createPost->handle($request->toDto(), $request->user());
return redirect()->route('posts.show', $post);
}
// CreatePostAction
public function handle(CreatePostData $data, User $author): Post
{
return $this->posts->create([
'title' => $data->title,
'slug' => $data->slug,
'content' => $data->content,
'excerpt' => $data->excerpt,
'user_id' => $author->id,
]);
}
Controller نازک میماند و Action Class امضای واضحی دارد — CreatePostData، نه array $data.
یک DTO، چند نقطه ورود
فایده واقعی DTO وقتی دیده میشود که همان Action از جایی غیر از HTTP صدا زده شود — مثلاً import از فایل CSV در یک Artisan command:
foreach ($rows as $row) {
$createPost->handle(CreatePostData::fromArray($row), $author);
}
Action نمیداند داده از فرم آمده یا از فایل. DTO readonly با propertyهای ساده (string، int، array) را میتوانید به Job هم پاس بدهید؛ موقع صف شدن مثل بقیه propertyهای Job serialize میشود.
Spatie Laravel Data
پکیج محبوب برای DTO: validation، cast و ساخت از Request یا Model:
use Spatie\LaravelData\Data;
class CreatePostData extends Data
{
public function __construct(
public string $title,
public string $slug,
public string $content,
) {}
}
// CreatePostData::from($request);
برای پروژه بزرگ با DTO زیاد ارزش دارد؛ برای CRUD کوچک همان readonly class کافی است.
DTO در مقابل Form Request، Model، Resource و Action
- Form Request: ورودی HTTP را validate و authorize میکند
- DTO: داده validateشده را با type مشخص به لایه بعد میبرد
- Action / Service: کار اصلی (use case) را با DTO انجام میدهد
- Model: داده را در دیتابیس ذخیره میکند و relationها را دارد
- API Resource: خروجی JSON را برای کلاینت شکل میدهد
DTO vs Model
| Model (Eloquent) | DTO |
|---|---|
| persist + relation + DB | فقط حمل داده بین لایه |
| mutable | ترجیحاً immutable |
| Active Record | input/output contract |
DTO vs Value Object
DTO مجموعهای از فیلدها برای جابهجایی داده است؛ Value Object یک مفهوم domain با قاعده و اعتبارسنجی درونی است (مثلاً Email یا Money). گاهی VO داخل DTO قرار میگیرد.
DTO vs API Resource
مسیر معمول: Request → DTO → Action → Model → Resource برای JSON. DTO ورودی را مرتب میکند و API Resource خروجی را؛ یکی جای دیگری را نمیگیرد.
اشتباهات رایج در استفاده از DTO
- DTO با ۵۰ فیلد — god object؛ آن را به چند DTO کوچکتر (مثلاً AddressData داخل CreateOrderData) بشکنید
- منطق business داخل DTO — جایش Action یا Service است
- گذاشتن Request یا Model داخل DTO — هدف جدا کردن لایهها از بین میرود
- DTO برای هر CRUD ساده بدون فایده واقعی
جمعبندی — کی DTO بسازیم؟
وقتی یک داده از چند لایه یا چند نقطه ورود (فرم، API، Command، Job) عبور میکند و type safety برایتان مهم است، DTO ارزشش را دارد؛ برای فرم سادهای که مستقیم ذخیره میشود، همان validated() کافی است. ادامه: Clean Code در PHP و اینکه DTO را به Service یا Action بدهید.