اشیاء انتقال داده (DTOs)
DTOها اشیاء سادهای هستند که دادهها را بین فرآیندها منتقل میکنند و مرزهای مشخصی بین لایههای سیستم ایجاد کرده و یکپارچگی داده را تضمین میکنند.
این سند استانداردها و بهترین شیوههای تیم ما برای پیادهسازی اشیاء انتقال داده (DTOs) در پروژههایمان را مشخص میکند.
مقدمه
اشیاء انتقال داده (DTOs) اشیاء سادهای هستند که دادهها را بین فرآیندها منتقل میکنند. آنها برای ایجاد مرزهای مشخص بین لایههای سیستم و تضمین یکپارچگی داده در سراسر برنامه ضروری هستند. این راهنما استانداردهای تیم ما برای پیادهسازی DTOها را تعیین میکند.
DTOها از مفاهیم سطح بالاتری مانند Data Structures، Data Types و Custom Types متمایز هستند. گاهی اوقات آنچه به عنوان DTO استفاده میشود ممکن است در واقع یک Custom Type باشد. این سند به طور خاص بر روی DTOها تمرکز دارد.
اصول اساسی
۱. الزام سازنده (Constructor)
هر DTO باید یک سازنده داشته باشد. یک DTO بدون سازنده بیمعنی است زیرا نمیتواند یکپارچگی دادههای خود را تضمین کند.
- ❌ نادرست
- ✅ درست
// DTO بدون سازنده
class UserDTO
{
public string $name;
public ?string $email;
}
// DTO با سازنده
class UserDTO
{
public function __construct(
public readonly string $name,
public readonly ?string $email = null
) {}
}
مهم: یک DTO بدون سازنده نمیتواند قوانین یکپارچگی داده یا اعتبارسنجی را اعمال کند.
۲. ویژگیهای اجباری
هر DTO باید حداقل یک ویژگی اجباری در سازنده خود داشته باشد. این تضمین میکند که وقتی یک بلوک داده ایجاد میشود، حاوی دادههای ضروری است.
- ✅ درست
- ❌ نادرست
// DTO با ویژگیهای اجباری
class ArticleDTO
{
public readonly string $title;
public readonly string $content;
public ?string $author = null;
public function __construct(
string $title,
string $content
) {
$this->title = $title;
$this->content = $content;
}
}
// DTO با همه ویژگیهای اختیاری
class ArticleDTO
{
public function __construct(
public ?string $title = null,
public ?string $content = null
) {}
}
۳. بدون رفتار
DTOها نباید حاوی منطق کسبوکار یا رفتار باشند. آنها صرفاً حاملهای داده هستند.
ما باید بین "منطق کسبوکار" و "منطق ساخت" تمایز قائل شویم. DTOها نباید حاوی منطق کسبوکار باشند، اما میتوانند حاوی منطق ساخت باشند.
تفاوت بین منطق کسبوکار و منطق ساخت
- 🛑 منطق کسبوکار (ممنوع)
- ✅ منطق ساخت (مجاز)
منطق کسبوکار نیازمند دانش دامنه، قوانین کسبوکار یا سرویسهای خارجی برای تصمیمگیری یا انجام یک عملیات است.
ویژگیهای منطق کسبوکار:
- نیاز به وابستگیهای خارجی دارد
- قوانین کسبوکار را پیادهسازی میکند
- وضعیت سیستم را تغییر میدهد
- به پایگاههای داده یا سایر سرویسها متصل میشود
// DTO با منطق کسبوکار (الگوی ضد)
class OrderDTO
{
public readonly float $amount;
public function __construct(float $amount)
{
$this->amount = $amount;
}
// منطق کسبوکار نباید در DTO باشد
public function applyDiscount(DiscountService $discountService, User $user): float
{
// نیاز به سرویس خارجی و دانش دامنه دارد
$discount = $discountService->getDiscountFor($user);
return $this->amount * (1 - $discount);
}
}
منطق ساخت به DTO کمک میکند تا از یک منبع داده دیگر (معمولاً یک Entity) ایجاد شود یا برعکس. این منطق هیچ وابستگی خارجی ندارد و فقط با دادههای ورودی و وضعیت داخلی خود کار میکند.
ویژگیهای منطق ساخت:
- بدون وابستگیهای خارجی
- فقط با دادههای ورودی و فیلدهای داخلی کار میکند
- هدف آن تبدیل داده است، نه اجرای قوانین کسبوکار
// DTO با منطق ساخت (الگوی مناسب)
class UserDTO
{
public readonly string $fullName;
public readonly string $email;
private function __construct(string $fullName, string $email)
{
$this->fullName = $fullName;
$this->email = $email;
}
// متد کارخانهای استاتیک -> این منطق ساخت است
public static function fromEntity(User $user): self
{
// منطق خودمحتوا، بدون سرویسهای خارجی یا فراخوانیهای پایگاه داده
$fullName = $user->getFirstName() . ' ' . $user->getLastName();
return new self($fullName, $user->getEmail());
}
}
قانون طلایی برای شناسایی
برای اجرای یک متد در DTO، آیا به چیزی فراتر از پارامترهای ورودی و فیلدهای داخلی خود کلاس نیاز دارد؟
- خیر؟ احتمالاً منطق ساخت یا یک کمککننده ساده است. (✅ مجاز)
- بله؟ این قطعاً منطق کسبوکار است. (🛑 ممنوع)
- ❌ نادرست
- ✅ درست
// DTO با منطق کسبوکار
class OrderDTO
{
public readonly float $amount;
public function __construct(float $amount)
{
$this->amount = $amount;
}
// منطق کسبوکار نباید در DTO باشد
public function calculateTax(): float
{
return $this->amount * 0.2;
}
}
// DTO بدون رفتار
class OrderDTO
{
public function __construct(
public readonly float $amount
) {}
}
به یاد داشته باشید: DTOها باید ساختارهای داده ساده بدون هیچگونه منطق کسبوکار باشند.
۴. سازماندهی پارامترهای سازنده
پارامترهای سازنده را با این دستورالعملها سازماندهی کنید:
- ابتدا پارامترهای اجباری (بدون مقادیر پیشفرض)
- در آخر پارامترهای اختیاری (با مقادیر پیشفرض)
- پارامترهای مرتبط را گروهبندی کنید
- تعداد کل پارامترها را محدود کنید - در صورت زیاد بودن، استفاده از DTOهای تودرتو را در نظر بگیرید
نکته: از ویژگی constructor property promotion در PHP 8 برای کد تمیزتر استفاده کنید.
// ❌ غیر بهینه: ترکیب ویژگیهای اجباری و اختیاری در سازنده
class ProductDTO
{
public function __construct(
public readonly string $name,
public readonly float $price,
public readonly ?string $description = null,
public readonly ?string $category = null,
public readonly ?array $tags = null
) {}
}
// ✅ بهتر: فقط ویژگیهای اجباری در سازنده
class ProductDTO
{
public readonly ?string $description = null;
public readonly ?string $category = null;
public readonly ?array $tags = null;
public function __construct(
public readonly string $name,
public readonly float $price
) {}
}
ترتیب پارامترهای سازنده
هنگام سازماندهی پارامترها در یک سازنده، این ترتیب را دنبال کنید:
- پارامترهای اجباری بدون مقادیر پیشفرض
- پارامترهای nullable بدون مقادیر پیشفرض
- پارامترهای با مقادیر پیشفرض
class UserProfileDTO
{
public function __construct(
// 1. پارامترهای اجباری بدون مقادیر پیشفرض
public readonly string $userId,
public readonly string $username,
// 2. پارامترهای nullable بدون مقادیر پیشفرض
public readonly ?string $email = null,
public readonly ?string $phone = null,
// 3. پارامترهای با مقادیر پیشفرض
public readonly string $country = 'USA',
public readonly bool $isActive = true
) {}
}
۵. بدون Getter و Setter
DTOها نیازی به getter و setter ندارند زیرا رفتاری ندارند. Getter و setter زمانی معنادار هستند که بخواهید رفتاری برای یک کلاس ایجاد و کنترل کنید.
- ❌ غیرضروری
- ✅ بهتر
// DTO با getter و setter
class ProductDTO
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
// getter غیرضروری
public function getName(): string
{
return $this->name;
}
// setter غیرضروری
public function setName(string $name): void
{
$this->name = $name;
}
}
// DTO با ویژگی public readonly
class ProductDTO
{
public function __construct(
public readonly string $name
) {}
}
انواع ویژگیها
✅ ویژگیهای Public Readonly
- توصیه شده برای اکثر موارد
- تغییرناپذیری را فراهم میکند
- از تغییر تصادفی جلوگیری میکند
- کد تمیزتر و مختصرتر
⚠️ ویژگیهای Public
- فقط زمانی استفاده کنید که تغییرپذیری لازم باشد
- کمتر از ویژگیهای readonly امن هستند
- میتوانند منجر به رفتار غیرمنتظره شوند
- نیاز به مدیریت دقیق دارند
ویژگیهای خصوصی در DTOها در صورت افزودن یک getter، عملاً همان ویژگیهای public readonly هستند. برای جلوگیری از کد غیرضروری، مستقیماً از ویژگیهای public readonly استفاده کنید.
- ❌ غیرضروری
- ✅ بهتر
// ویژگی خصوصی با getter
class CustomerDTO
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
}
// ویژگی public readonly
class CustomerDTO
{
public function __construct(
public readonly string $name
) {}
}
ویژگیهای Readonly
بهترین شیوه: هر زمان که ممکن است، ویژگیهای DTO را readonly کنید. این یک ساختار قویتر ایجاد میکند و باگها را کاهش میدهد، زیرا دادهها نمیتوانند در طول مسیر دستکاری شوند.
با ویژگیهای readonly، میتوانید با اطمینان DTO خود را از چندین لایه عبور دهید بدون اینکه نگران تغییر تصادفی دادهها باشید.
اعتبارسنجی
اعتبارسنجیهایی که در سازنده انجام میشوند و به عوامل خارجی (مانند پایگاه داده یا سرویسهای شخص ثالث) وابسته نیستند، بخشی از ماهیت DTO محسوب میشوند، نه رفتار.
- اعتبارسنجی پایه
- اعتبارسنجی پیشرفته
class EmailDTO
{
public readonly string $address;
public function __construct(string $address)
{
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
$this->address = $address;
}
}
class UserDTO
{
public readonly string $username;
public readonly string $email;
public readonly int $age;
public function __construct(string $username, string $email, int $age)
{
// اعتبارسنجی نام کاربری
if (strlen($username) < 3 || strlen($username) > 20) {
throw new InvalidArgumentException(
'Username must be between 3 and 20 characters'
);
}
// اعتبارسنجی ایمیل
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
// اعتبارسنجی سن
if ($age < 18 || $age > 120) {
throw new InvalidArgumentException('Age must be between 18 and 120');
}
$this->username = $username;
$this->email = $email;
$this->age = $age;
}
}
نمونههای عملی از منطق ساخت و منطق کسبوکار
تشخیص صحیح بین منطق ساخت (مجاز) و منطق کسبوکار (ممنوع) در DTOها یکی از مهمترین مهارتها در طراحی معماری لایهای است.
نمونههایی از منطق ساخت (مجاز در DTOها)
- متدهای کارخانهای
- متدهای کمکی ساده
class ProductDTO
{
public readonly string $name;
public readonly float $price;
public readonly string $formattedPrice;
public readonly array $categories;
private function __construct(
string $name,
float $price,
string $formattedPrice,
array $categories
) {
$this->name = $name;
$this->price = $price;
$this->formattedPrice = $formattedPrice;
$this->categories = $categories;
}
// ✅ منطق ساخت: فقط از دادههای ورودی استفاده میکند
public static function fromEntity(Product $product): self
{
return new self(
$product->getName(),
$product->getPrice(),
number_format($product->getPrice(), 2) . ' USD',
array_map(fn($cat) => $cat->getName(), $product->getCategories())
);
}
// ✅ منطق ساخت: DTO را به آرایه تبدیل میکند
public function toArray(): array
{
return [
'name' => $this->name,
'price' => $this->price,
'formatted_price' => $this->formattedPrice,
'categories' => $this->categories,
];
}
}
class AddressDTO
{
public function __construct(
public readonly string $street,
public readonly string $city,
public readonly string $zipCode,
public readonly string $country
) {}
// ✅ منطق ساخت: فقط از دادههای داخلی استفاده میکند
public function getFullAddress(): string
{
return "$this->street, $this->city, $this->country, $this->zipCode";
}
// ✅ منطق ساخت: تبدیل ساده بدون وابستگیهای خارجی
public function isInternational(string $userCountry): bool
{
return $this->country !== $userCountry;
}
}
نمونههایی از منطق کسبوکار (ممنوع در DTOها)
- وابستگیهای خارجی
- دسترسی به پایگاه داده
- تغییر وضعیت سیستم
// ❌ الگوی ضد: DTO با وابستگی سرویس خارجی
class OrderDTO
{
public function __construct(
public readonly string $orderId,
public readonly float $amount,
public readonly array $items
) {}
// ❌ منطق کسبوکار: نیاز به سرویسهای خارجی دارد
public function calculateFinalPrice(TaxService $taxService, DiscountService $discountService): float
{
$taxRate = $taxService->getTaxRateForOrder($this);
$discount = $discountService->getApplicableDiscount($this->orderId);
return $this->amount * (1 + $taxRate) * (1 - $discount);
}
}
// ❌ الگوی ضد: DTO با دسترسی به پایگاه داده
class UserDTO
{
public function __construct(
public readonly int $userId,
public readonly string $username,
public readonly string $email
) {}
// ❌ منطق کسبوکار: دسترسی مستقیم به پایگاه داده
public function getUserOrders(Database $db): array
{
return $db->query(
"SELECT * FROM orders WHERE user_id = ?",
[$this->userId]
)->fetchAll();
}
}
// ❌ الگوی ضد: DTO با قابلیت تغییر وضعیت سیستم
class PaymentDTO
{
public function __construct(
public readonly string $transactionId,
public readonly float $amount,
public readonly string $status
) {}
// ❌ منطق کسبوکار: وضعیت سیستم را تغییر میدهد
public function processPayment(PaymentGateway $gateway): bool
{
if ($this->status === 'pending') {
return $gateway->processTransaction($this->transactionId);
}
return false;
}
}
نمودار جریان تصمیمگیری
ترتیب پارامترهای سازنده
هنگام سازماندهی پارامترها در یک سازنده، این ترتیب را دنبال کنید:
- پارامترهای اجباری بدون مقادیر پیشفرض
- پارامترهای nullable بدون مقادیر پیشفرض
- پارامترهای با مقادیر پیشفرض
class PersonDTO
{
public readonly string $name;
public readonly int $age;
public readonly ?string $email;
public readonly ?string $phone;
public readonly string $country;
public readonly bool $isActive;
public function __construct(
// 1. پارامترهای اجباری بدون مقادیر پیشفرض
string $name,
int $age,
// 2. پارامترهای nullable بدون مقادیر پیشفرض
?string $email = null,
?string $phone = null,
// 3. پارامترهای با مقادیر پیشفرض
string $country = 'USA',
bool $isActive = true
) {
$this->name = $name;
$this->age = $age;
$this->email = $email;
$this->phone = $phone;
$this->country = $country;
$this->isActive = $isActive;
}
}
مثال دنیای واقعی
- DTO پایه
- DTO پیشرفته با اعتبارسنجی
class AddressDTO
{
public readonly string $street;
public readonly string $city;
public readonly string $zipCode;
public readonly ?string $state;
public readonly string $country;
public function __construct(
string $street,
string $city,
string $zipCode,
?string $state = null,
string $country = 'USA'
) {
$this->street = $street;
$this->city = $city;
$this->zipCode = $zipCode;
$this->state = $state;
$this->country = $country;
}
}
class UserRegistrationDTO
{
public readonly string $username;
public readonly string $email;
public readonly string $password;
public readonly ?string $referralCode;
public readonly bool $acceptTerms;
public function __construct(
string $username,
string $email,
string $password,
?string $referralCode = null,
bool $acceptTerms = false
) {
// اعتبارسنجی نام کاربری
if (strlen($username) < 3) {
throw new InvalidArgumentException('نام کاربری باید حداقل 3 کاراکتر باشد');
}
// اعتبارسنجی ایمیل
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('آدرس ایمیل نامعتبر است');
}
// اعتبارسنجی رمز عبور
if (strlen($password) < 8) {
throw new InvalidArgumentException('رمز عبور باید حداقل 8 کاراکتر باشد');
}
// اعتبارسنجی پذیرش شرایط
if (!$acceptTerms) {
throw new InvalidArgumentException('شرایط باید پذیرفته شوند');
}
$this->username = $username;
$this->email = $email;
$this->password = $password;
$this->referralCode = $referralCode;
$this->acceptTerms = $acceptTerms;
}
}
خلاصه
راهنماییهای کلیدی DTO
- هر DTO باید یک سازنده داشته باشد
- هر DTO باید حداقل یک ویژگی اجباری داشته باشد
- DTOها نباید حاوی منطق کسبوکار یا رفتار باشند
- منطق ساخت (مانند متدهای کارخانهای استاتیک) در DTOها مجاز است
- به جای ویژگیهای خصوصی با getter، از ویژگیهای public readonly استفاده کنید
- هر زمان که ممکن است، از ویژگیهای readonly استفاده کنید
- اعتبارسنجیها را در سازنده برای حفظ یکپارچگی داده قرار دهید
- ترتیب توصیه شده پارامترها را در سازندهها رعایت کنید
برای تعیین اینکه یک متد در DTO مجاز است یا خیر، از خود بپرسید:
آیا این متد به چیزی فراتر از پارامترهای ورودی و وضعیت داخلی خود کلاس برای انجام کارش نیاز دارد؟
- خیر؟ احتمالاً منطق ساخت یا یک کمککننده ساده است. (✅ مجاز)
- بله؟ این قطعاً منطق کسبوکار است. (🛑 ممنوع)
تفاوت بین منطق کسبوکار و منطق ساخت
🛑 منطق کسبوکار (ممنوع)
- نیازمند وابستگیهای خارجی
- اجرای قوانین کسبوکار
- تغییر وضعیت سیستم
- اتصال به پایگاههای داده یا سایر سرویسها
✅ منطق ساخت (مجاز)
- بدون وابستگیهای خارجی
- فقط با دادههای ورودی و فیلدهای داخلی کار میکند
- هدف آن تبدیل داده است، نه اجرای قوانین کسبوکار
- مثالها: متدهای کارخانهای استاتیک، تبدیل آرایه
قوانین عملی:
- DTOهای خود را در بررسیهای کد برای اطمینان از پیروی از این استانداردها بررسی کنید
- از قانون طلایی برای تعیین اینکه یک متد به DTO تعلق دارد یا خیر استفاده کنید
- استفاده از الگوی متد کارخانهای استاتیک برای ساخت DTO را در نظر بگیرید
- از ابزارهای تحلیل استاتیک برای اعمال خودکار این قوانین استفاده کنید