المشكلة مع البنية الافتراضية لـ Laravel
البنية الافتراضية لـ Laravel مثالية للمشاريع الصغيرة والمتوسطة. لكن مع نمو تطبيقك، تصبح مجلدات app/Models وapp/Http/Controllers وapp/Services مستودعات لأكواد غير مترابطة. قد يتولى نموذج User المصادقة والفوترة وتفضيلات الإشعارات وإدارة الفرق في آن واحد.
يمنحنا التصميم المبني على النطاق (DDD) مصطلحات ومجموعة من الأنماط لتنظيم الكود حول المفاهيم التجارية بدلاً من الطبقات التقنية.
إعادة الهيكلة: من الطبقات إلى النطاقات
بدلاً من التنظيم حسب الاهتمام التقني، نظّم حسب نطاق العمل:
app/
├── Domain/
│ ├── Billing/
│ │ ├── Models/
│ │ │ ├── Invoice.php
│ │ │ └── Subscription.php
│ │ ├── Actions/
│ │ │ ├── CreateInvoice.php
│ │ │ └── ProcessPayment.php
│ │ ├── Events/
│ │ │ └── PaymentReceived.php
│ │ ├── ValueObjects/
│ │ │ └── Money.php
│ │ └── Exceptions/
│ │ └── PaymentFailedException.php
│ ├── Identity/
│ │ ├── Models/
│ │ │ ├── User.php
│ │ │ └── Team.php
│ │ └── Actions/
│ │ ├── RegisterUser.php
│ │ └── InviteTeamMember.php
│ └── Catalog/
│ ├── Models/
│ └── Actions/
├── App/
│ ├── Http/Controllers/ ← thin controllers
│ ├── Console/Commands/
│ └── Providers/
كائنات القيمة: تغليف قواعد العمل
كائن القيمة (Value Object) هو كائن غير قابل للتغيير يمثل مفهوماً بدون هوية — يُعرَّف بالكامل من خلال خصائصه. Money وEmail وAddress أمثلة كلاسيكية.
class Money
{
public function __construct(
private readonly int $amount,
private readonly string $currency = 'USD'
) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new CurrencyMismatchException();
}
return new self($this->amount + $other->amount, $this->currency);
}
public function multiply(int $factor): self
{
return new self($this->amount * $factor, $this->currency);
}
public function formatted(): string
{
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
الآن بدلاً من تمرير أعداد صحيحة خام والأمل بأن الجميع يتذكر "هل هذا بالسنتات أم بالدولارات؟"، لديك نوع بيانات يوثّق نفسه ويتحقق من صحته ذاتياً.
الإجراءات: عمليات تجارية أحادية الغرض
الإجراءات (Actions) (وتسمى أيضاً "حالات الاستخدام" أو "الأوامر") تغلّف عملية تجارية واحدة. وهي نقطة الدخول إلى منطق النطاق الخاص بك.
class CreateInvoice
{
public function __construct(
private readonly InvoiceRepository $invoices,
private readonly TaxCalculator $taxCalculator,
) {}
public function execute(CreateInvoiceData $data): Invoice
{
$subtotal = $data->lineItems->sum(
fn (LineItem $item) => $item->total()->amount
);
$tax = $this->taxCalculator->calculate(
new Money($subtotal),
$data->taxRegion
);
$invoice = $this->invoices->create([
'customer_id' => $data->customerId,
'subtotal' => $subtotal,
'tax' => $tax->amount,
'total' => $subtotal + $tax->amount,
'due_date' => $data->dueDate,
'status' => InvoiceStatus::Draft,
]);
event(new InvoiceCreated($invoice));
return $invoice;
}
}
أحداث النطاق: فصل الآثار الجانبية
عندما يحدث شيء مهم في نطاقك، أطلق حدثاً. هذا يبقي منطقك الأساسي نظيفاً ويسمح لأجزاء أخرى من النظام بالاستجابة بشكل مستقل.
// The event — a simple data carrier
class PaymentReceived
{
public function __construct(
public readonly int $invoiceId,
public readonly Money $amount,
public readonly string $paymentMethod,
public readonly Carbon $paidAt,
) {}
}
// Listeners handle side effects
class SendPaymentConfirmation
{
public function handle(PaymentReceived $event): void
{
$invoice = Invoice::findOrFail($event->invoiceId);
Mail::to($invoice->customer->email)->send(
new PaymentConfirmationMail($invoice, $event->amount)
);
}
}
class UpdateAccountBalance
{
public function handle(PaymentReceived $event): void
{
// Update the customer's balance
}
}
class NotifyAccountingTeam
{
public function handle(PaymentReceived $event): void
{
// Send Slack notification
}
}
كائنات نقل البيانات
كائنات نقل البيانات (DTOs) تحمل البيانات بين الطبقات بدون سلوك. تجعل توقيعات الدوال صريحة وسهلة إعادة الهيكلة.
class CreateInvoiceData
{
public function __construct(
public readonly int $customerId,
public readonly Collection $lineItems,
public readonly string $taxRegion,
public readonly Carbon $dueDate,
) {}
public static function fromRequest(Request $request): self
{
return new self(
customerId: $request->integer('customer_id'),
lineItems: collect($request->input('line_items'))->map(
fn ($item) => new LineItem($item['description'], $item['quantity'], $item['unit_price'])
),
taxRegion: $request->string('tax_region'),
dueDate: Carbon::parse($request->input('due_date')),
);
}
}
متى تطبّق DDD (ومتى لا تفعل)
- استخدم DDD عندما يكون منطق العمل معقداً، ويتضمن قواعد متعددة، ويتغير بشكل متكرر
- تجاوز DDD لتطبيقات CRUD البسيطة، والنماذج الأولية، ولوحات الإدارة
- ابدأ ببساطة — يمكنك دائماً إعادة الهيكلة نحو DDD مع نمو التعقيد
- لا تطبّق كل أنماط DDD — اختر الأنماط التي تحل مشاكلك الفعلية
"الهدف من DDD ليس استخدام كل نمط في الكتاب. بل بناء فهم مشترك للنطاق بين المطورين وخبراء المجال، وعكس هذا الفهم في الكود."