Laravel المتقدم

التصميم الموجه بالنطاق في Laravel

20 دقيقة الدرس 21 من 40

فهم التصميم الموجه بالنطاق (DDD)

التصميم الموجه بالنطاق هو نهج استراتيجي لتطوير البرمجيات يركز على نمذجة تطبيقك حول نطاق العمل بدلاً من الاهتمامات التقنية. في Laravel، يساعد DDD في تنظيم التطبيقات المعقدة من خلال فصل منطق الأعمال عن كود الإطار.

مفاهيم DDD الأساسية

يقدم DDD العديد من المفاهيم الرئيسية التي تساعد في هيكلة تطبيقات المؤسسات:

السياق المحدود (Bounded Context): حدود منطقية يتم فيها تعريف وتطبيق نموذج نطاق معين. كل سياق محدود له لغته الموحدة ونماذجه الخاصة.
// هيكل 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);
    }
}

الكيانات والتجميعات

الكيانات هي كائنات ذات هوية مميزة تستمر عبر الزمن. التجميعات هي مجموعات من الكيانات وكائنات القيمة التي تُعامل كوحدة واحدة لتغييرات البيانات.

جذر التجميع (Aggregate Root): الكيان الرئيسي الذي يجب أن تمر جميع العمليات على التجميع من خلاله. يفرض القيود وقواعد الأعمال عبر التجميع بالكامل.
<?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.
تمرين 1: أنشئ جذر تجميع Product مع كائن قيمة Price. أضف طرقًا لـ:
  • تطبيق خصم نسبة مئوية (يعيد Price جديد)
  • تغيير السعر (مع التحقق من قاعدة الأعمال)
  • التحقق مما إذا كان السعر ضمن النطاق المقبول
تمرين 2: صمم سياق محدود Customer مع:
  • كيان Customer (جذر التجميع)
  • كائنات قيمة Email و Address
  • حدث نطاق CustomerRegistered
  • قاعدة الأعمال: يجب أن يكون لدى العميل بريد إلكتروني صالح وعنوان واحد على الأقل
تمرين 3: نفذ خدمة نطاق PaymentService تقوم بـ:
  • التنسيق بين تجميعات Order و Payment
  • التحقق من أن مبلغ الدفع يطابق إجمالي الطلب
  • إرسال حدث OrderPaid عند النجاح
  • التعامل مع سيناريوهات فشل الدفع
فوائد DDD: تنظيم أفضل للكود، منطق أعمال أوضح، قابلية اختبار محسنة، تعاون أسهل مع خبراء النطاق، وكود أكثر قابلية للصيانة مع نمو التعقيد.