التصميم الموجه بالنطاق في Laravel
فهم التصميم الموجه بالنطاق (DDD)
التصميم الموجه بالنطاق هو نهج استراتيجي لتطوير البرمجيات يركز على نمذجة تطبيقك حول نطاق العمل بدلاً من الاهتمامات التقنية. في Laravel، يساعد DDD في تنظيم التطبيقات المعقدة من خلال فصل منطق الأعمال عن كود الإطار.
مفاهيم DDD الأساسية
يقدم DDD العديد من المفاهيم الرئيسية التي تساعد في هيكلة تطبيقات المؤسسات:
// هيكل Laravel التقليدي
app/
├── Models/
│ ├── User.php
│ ├── Order.php
│ └── Product.php
// هيكل DDD مع السياقات المحدودة
app/
├── Domain/
│ ├── Sales/ // سياق محدود
│ │ ├── Models/
│ │ │ ├── Order.php
│ │ │ └── OrderItem.php
│ │ ├── ValueObjects/
│ │ │ ├── Money.php
│ │ │ └── OrderStatus.php
│ │ ├── Events/
│ │ │ └── OrderPlaced.php
│ │ └── Services/
│ │ └── OrderService.php
│ ├── Catalog/ // سياق محدود آخر
│ │ ├── Models/
│ │ │ └── Product.php
│ │ └── ValueObjects/
│ │ └── Price.php
│ └── Identity/ // سياق إدارة المستخدمين
│ ├── Models/
│ │ └── User.php
│ └── ValueObjects/
│ └── Email.php
كائنات القيمة (Value Objects)
كائنات القيمة هي كائنات غير قابلة للتغيير تمثل جوانب وصفية للنطاق بدون هوية مفاهيمية. يتم تعريفها بواسطة سماتها بدلاً من معرّف.
<?php
namespace App\Domain\Sales\ValueObjects;
class Money
{
private float $amount;
private string $currency;
public function __construct(float $amount, string $currency = 'USD')
{
if ($amount < 0) {
throw new \InvalidArgumentException('لا يمكن أن يكون المبلغ سالبًا');
}
$this->amount = $amount;
$this->currency = strtoupper($currency);
}
public function amount(): float
{
return $this->amount;
}
public function currency(): string
{
return $this->currency;
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('عدم تطابق العملة');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function multiply(float $multiplier): self
{
return new self($this->amount * $multiplier, $this->currency);
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
public function format(): string
{
return $this->currency . ' ' . number_format($this->amount, 2);
}
}
الكيانات والتجميعات
الكيانات هي كائنات ذات هوية مميزة تستمر عبر الزمن. التجميعات هي مجموعات من الكيانات وكائنات القيمة التي تُعامل كوحدة واحدة لتغييرات البيانات.
<?php
namespace App\Domain\Sales\Models;
use App\Domain\Sales\ValueObjects\Money;
use App\Domain\Sales\Events\OrderPlaced;
use Illuminate\Database\Eloquent\Model;
class Order extends Model // جذر التجميع
{
protected $fillable = ['customer_id', 'status', 'total_amount', 'currency'];
protected $casts = [
'placed_at' => 'datetime',
];
// أحداث النطاق
protected $dispatchesEvents = [
'created' => OrderPlaced::class,
];
// العلاقات (أعضاء التجميع)
public function items()
{
return $this->hasMany(OrderItem::class);
}
// طرق منطق الأعمال
public function addItem(int $productId, int $quantity, Money $price): void
{
// التحقق من قواعد الأعمال
if ($quantity <= 0) {
throw new \InvalidArgumentException('يجب أن تكون الكمية موجبة');
}
if ($this->status !== 'draft') {
throw new \DomainException('لا يمكن إضافة عناصر لطلب غير مسودة');
}
$this->items()->create([
'product_id' => $productId,
'quantity' => $quantity,
'unit_price' => $price->amount(),
'currency' => $price->currency(),
]);
$this->recalculateTotal();
}
public function place(): void
{
if ($this->items->isEmpty()) {
throw new \DomainException('لا يمكن إنشاء طلب بدون عناصر');
}
if ($this->status !== 'draft') {
throw new \DomainException('تم إنشاء الطلب بالفعل');
}
$this->update([
'status' => 'placed',
'placed_at' => now(),
]);
}
private function recalculateTotal(): void
{
$total = $this->items->sum(function ($item) {
return $item->quantity * $item->unit_price;
});
$this->update(['total_amount' => $total]);
}
// محدد كائن القيمة
public function total(): Money
{
return new Money($this->total_amount, $this->currency);
}
}
أحداث النطاق (Domain Events)
أحداث النطاق تمثل شيئًا مهمًا حدث في النطاق. تُستخدم للتواصل بين السياقات المحدودة وتشغيل الآثار الجانبية.
<?php
namespace App\Domain\Sales\Events;
use App\Domain\Sales\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderPlaced
{
use Dispatchable, SerializesModels;
public Order $order;
public \DateTimeImmutable $occurredAt;
public function __construct(Order $order)
{
$this->order = $order;
$this->occurredAt = new \DateTimeImmutable();
}
public function orderId(): int
{
return $this->order->id;
}
public function customerId(): int
{
return $this->order->customer_id;
}
public function totalAmount(): float
{
return $this->order->total_amount;
}
}
// مستمع الحدث
namespace App\Domain\Sales\Listeners;
use App\Domain\Sales\Events\OrderPlaced;
use App\Domain\Inventory\Services\InventoryService;
use App\Domain\Notifications\Services\NotificationService;
class HandleOrderPlaced
{
private InventoryService $inventoryService;
private NotificationService $notificationService;
public function __construct(
InventoryService $inventoryService,
NotificationService $notificationService
) {
$this->inventoryService = $inventoryService;
$this->notificationService = $notificationService;
}
public function handle(OrderPlaced $event): void
{
// حجز المخزون
$this->inventoryService->reserveForOrder($event->order);
// إرسال إشعار
$this->notificationService->notifyOrderPlaced($event->order);
}
}
خدمات النطاق (Domain Services)
تلخص خدمات النطاق منطق الأعمال الذي لا يناسب بشكل طبيعي داخل الكيانات أو كائنات القيمة، خاصة العمليات التي تتضمن تجميعات متعددة.
<?php
namespace App\Domain\Sales\Services;
use App\Domain\Sales\Models\Order;
use App\Domain\Catalog\Repositories\ProductRepository;
use App\Domain\Inventory\Services\InventoryService;
class OrderService
{
private ProductRepository $productRepository;
private InventoryService $inventoryService;
public function __construct(
ProductRepository $productRepository,
InventoryService $inventoryService
) {
$this->productRepository = $productRepository;
$this->inventoryService = $inventoryService;
}
public function createOrderFromCart(int $customerId, array $cartItems): Order
{
// التحقق من توفر المخزون عبر منتجات متعددة
foreach ($cartItems as $item) {
if (!$this->inventoryService->isAvailable($item['product_id'], $item['quantity'])) {
throw new \DomainException(
"المنتج {$item['product_id']} غير متوفر بالكمية المطلوبة"
);
}
}
// إنشاء طلب (جذر التجميع)
$order = Order::create([
'customer_id' => $customerId,
'status' => 'draft',
'currency' => 'USD',
]);
// إضافة عناصر للطلب
foreach ($cartItems as $item) {
$product = $this->productRepository->findById($item['product_id']);
$order->addItem(
$product->id,
$item['quantity'],
$product->price()
);
}
// إنشاء الطلب
$order->place();
return $order;
}
public function calculateDiscount(Order $order): float
{
// قاعدة أعمال عبر التجميعات
$total = $order->total()->amount();
if ($total >= 1000) {
return 0.15; // خصم 15%
} elseif ($total >= 500) {
return 0.10; // خصم 10%
} elseif ($total >= 200) {
return 0.05; // خصم 5%
}
return 0;
}
}
اللغة الموحدة (Ubiquitous Language)
اللغة الموحدة هي مفردات مشتركة يستخدمها كل من المطورين وخبراء النطاق. يجب أن يعكس الكود هذه اللغة بالضبط.
()placeOrder أو ()place، وليس ()createOrder أو ()submitOrder.
Product مع كائن قيمة Price. أضف طرقًا لـ:
- تطبيق خصم نسبة مئوية (يعيد Price جديد)
- تغيير السعر (مع التحقق من قاعدة الأعمال)
- التحقق مما إذا كان السعر ضمن النطاق المقبول
Customer مع:
- كيان Customer (جذر التجميع)
- كائنات قيمة Email و Address
- حدث نطاق CustomerRegistered
- قاعدة الأعمال: يجب أن يكون لدى العميل بريد إلكتروني صالح وعنوان واحد على الأقل
PaymentService تقوم بـ:
- التنسيق بين تجميعات Order و Payment
- التحقق من أن مبلغ الدفع يطابق إجمالي الطلب
- إرسال حدث OrderPaid عند النجاح
- التعامل مع سيناريوهات فشل الدفع