راهنمای کامل طراحی مبتنی بر دامنه (DDD) در لاراول

پیاده‌سازی DDD در Laravel به معنای کنار گذاشتن چارچوب نیست، بلکه به معنای سازماندهی متفاوت کد و استفاده هوشمندانه از Laravel به عنوان یک لایه زیرساخت قدرتمند است. این مقاله شما را از معماری MVC سنتی Laravel به سمت یک معماری مبتنی بر دامنه (Clean/Hexagonal) هدایت می‌کند.

۱. معماری و ساختار پوشه‌ها (The New Structure)

اولین قدم، خداحافظی با ساختار پیش‌فرض Laravel برای منطق کسب‌وکار است. ما مدل‌های Eloquent را به لایه Infrastructure منتقل می‌کنیم.

ساختار پیشنهادی پوشه‌ها (app/):

app/
├── Domain/
│   ├── Models/              # Entityها و Aggregate Rootهای خالص PHP (بدون وابستگی به Eloquent)
│   ├── ValueObjects/        # شیء ارزش‌ها
│   ├── Enums/               # Enumهای دامنه
│   ├── Repositories/        # Interfaces مخزن‌ها (قراردادها)
│   ├── Services/            # سرویس‌های دامنه
│   └── Events/              # رویدادهای دامنه (Domain Events)
├── Application/
│   ├── Services/            # سرویس‌های برنامه (Application Services -协调 کننده)
│   ├── DTOs/                # Data Transfer Objects
│   └── Listeners/           #听众 برای رویدادهای دامنه
└── Infrastructure/
    ├── Eloquent/
    │   ├── Models/          # مدل‌های Eloquent (اکنون فقط جزئیات اجرایی هستند)
    │   └── Repositories/    # پیاده‌سازی مخزن‌ها با Eloquent
    └── Providers/
        └── RepositoryServiceProvider.php # وابستگی‌ها را به پیاده‌سازی Infrastructure متصل می‌کند

ارتباط بین لایه‌ها: Domain <- Application <- Infrastructure <- UI (Controllers, Commands)


۲. بلوک‌های سازنده تاکتیکی: پیاده‌سازی با PHP 8.4 و Laravel 12

بیایید یک محدوده مستقل (Bounded Context) به نام «ارزیابی» (Review) را پیاده‌سازی کنیم.

الف. شیء ارزش (Value Object): Rating
// app/Domain/ValueObjects/Rating.php
<?php

namespace App\Domain\ValueObjects;

use InvalidArgumentException;

readonly final class Rating
{
    public int $value;

    public function __construct(int $value)
    {
        // قوانین کسب‌وکار: ریتینگ باید بین ۱ تا ۵ باشد.
        if ($value < 1 || $value > 5) {
            throw new InvalidArgumentException('Rating must be between 1 and 5.');
        }

        $this->value = $value;
    }

    // اشیاء ارزش باید با مقایسه ویژگی‌هایشان سنجیده شوند.
    public function equals(Rating $other): bool
    {
        return $this->value === $other->value;
    }
}
ب. موجودیت (Entity): Review

این یک کلاس PHP ساده و خالص است که هیچ وابستگی به Laravel یا Eloquent ندارد.

// app/Domain/Models/Review.php
<?php

namespace App\Domain\Models;

use App\Domain\ValueObjects\Rating;
use DateTimeImmutable;

class Review
{
    // از ویژگی‌های تایپ شده PHP 8 بهره می‌برد
    public function __construct(
        public string $id,           // شناسه (هویت)
        public string $productId,
        public string $userId,
        public string $comment,
        public Rating $rating,       // استفاده از شیء ارزش
        public DateTimeImmutable $createdAt,
        public ?DateTimeImmutable $updatedAt = null,
    ) {}

    // رفتار دامنه: به روزرسانی نظر
    public function update(string $newComment, Rating $newRating): void
    {
        $this->comment = $newComment;
        $this->rating = $newRating;
        $this->updatedAt = new DateTimeImmutable();
    }

    // رفتار دامنه: آیا نظر توسط کاربر خاصی ایجاد شده است؟
    public function isWrittenBy(string $userId): bool
    {
        return $this->userId === $userId;
    }
}
ج. قرارداد مخزن (Repository Interface) در دامنه
// app/Domain/Repositories/ReviewRepository.php
<?php

namespace App\Domain\Repositories;

use App\Domain\Models\Review;

interface ReviewRepository
{
    public function findById(string $id): ?Review;
    public function findByProductId(string $productId): array;
    public function save(Review $review): void;
    public function delete(string $id): void;
}
د. پیاده‌سازی مخزن (Repository Implementation) در Infrastructure با Eloquent

اکنون از Eloquent به عنوان یک ابزار اجرایی استفاده می‌کنیم.

// app/Infrastructure/Eloquent/Repositories/EloquentReviewRepository.php
<?php

namespace App\Infrastructure\Eloquent\Repositories;

use App\Domain\Models\Review;
use App\Domain\Repositories\ReviewRepository;
use App\Infrastructure\Eloquent\Models\EloquentReview; // مدل Eloquent
use App\Domain\ValueObjects\Rating;

class EloquentReviewRepository implements ReviewRepository
{
    public function __construct(private EloquentReview $model) {}

    public function findById(string $id): ?Review
    {
        $eloquentReview = $this->model->find($id);

        if (!$eloquentReview) {
            return null;
        }

        return $this->toDomainEntity($eloquentReview);
    }

    public function save(Review $review): void
    {
        // پیدا کردن یا ایجاد یک رکورد Eloquent
        $eloquentReview = $this->model->findOrNew($review->id);

        // پر کردن داده از Entity دامنه به مدل Eloquent
        $eloquentReview->id = $review->id;
        $eloquentReview->product_id = $review->productId;
        $eloquentReview->user_id = $review->userId;
        $eloquentReview->comment = $review->comment;
        $eloquentReview->rating = $review->rating->value; // تبدیل شیء ارزش به مقدار اولیه
        $eloquentReview->created_at = $review->createdAt;
        $eloquentReview->updated_at = $review->updatedAt;

        $eloquentReview->save();
    }

    // ... سایر متدها (findByProductId, delete)

    // تابع کمک‌کننده برای تبدیل مدل Eloquent به Entity دامنه
    private function toDomainEntity(EloquentReview $eloquentReview): Review
    {
        return new Review(
            id: $eloquentReview->id,
            productId: $eloquentReview->product_id,
            userId: $eloquentReview->user_id,
            comment: $eloquentReview->comment,
            rating: new Rating($eloquentReview->rating), // تبدیل مقدار اولیه به شیء ارزش
            createdAt: $eloquentReview->created_at->toDateTimeImmutable(),
            updatedAt: $eloquentReview->updated_at?->toDateTimeImmutable(),
        );
    }
}

مدل Eloquent (در Infrastructure):

// app/Infrastructure/Eloquent/Models/EloquentReview.php
<?php

namespace App\Infrastructure\Eloquent\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class EloquentReview extends Model
{
    use HasFactory;

    protected $table = 'reviews';
    protected $keyType = 'string'; // برای UUID یا ULID
    public $incrementing = false;
    protected $guarded = []; // یا fillable را مشخص کنید

    // Casts برای تاریخ‌ها و سایر ویژگی‌ها
    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
        'rating' => 'integer',
    ];
}
ه. سرویس برنامه (Application Service) به عنوان هماهنگ‌کننده

این سرویس مورد استفاده کنترلرها قرار می‌گیرد و جریان برنامه را هماهنگ می‌کند.

// app/Application/Services/ReviewService.php
<?php

namespace App\Application\Services;

use App\Domain\Models\Review;
use App\Domain\Repositories\ReviewRepository;
use App\Domain\ValueObjects\Rating;
use DateTimeImmutable;

class ReviewService
{
    public function __construct(private ReviewRepository $reviewRepository) {}

    public function createReview(
        string $reviewId,
        string $productId,
        string $userId,
        string $comment,
        int $ratingValue
    ): void {
        // 1. ایجاد شیء ارزش و اعتبارسنجی آن
        $rating = new Rating($ratingValue);

        // 2. ایجاد Entity دامنه
        $review = new Review(
            id: $reviewId,
            productId: $productId,
            userId: $userId,
            comment: $comment,
            rating: $rating,
            createdAt: new DateTimeImmutable(),
        );

        // 3. ذخیره آن (مخزن به طور خودکار به Eloquent نگاشت می‌شود)
        $this->reviewRepository->save($review);

        // 4. می‌توان یک رویداد دامنه اینجا انتشار داد
        // event(new ReviewCreated($reviewId, $productId));
    }

    public function updateUserReview(string $reviewId, string $userId, string $newComment, int $newRatingValue): void
    {
        $review = $this->reviewRepository->findById($reviewId);

        if (!$review) {
            throw new \Exception("Review not found.");
        }

        // بررسی مجوز: آیا کاربر صاحب این نظر است؟
        if (!$review->isWrittenBy($userId)) {
            throw new \Exception("You can only update your own reviews.");
        }

        $newRating = new Rating($newRatingValue);
        $review->update($newComment, $newRating);

        $this->reviewRepository->save($review);
    }
}
و. کنترلر (Controller) به عنوان نقطه ورود UI

کنترلر بسیار سبک می‌شود و فقط درخواست‌ها را به سرویس برنامه منتقل می‌کند.

// app/Http/Controllers/API/ReviewController.php
<?php

namespace App\Http\Controllers\API;

use App\Application\Services\ReviewService;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreReviewRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;

class ReviewController extends Controller
{
    public function __construct(private ReviewService $reviewService) {}

    public function store(StoreReviewRequest $request): JsonResponse
    {
        // اعتبارسنجی Request توسط Form Request Laravel انجام می‌شود
        $validated = $request->validated();

        // استفاده از UUID یا ULID برای شناسه‌ها
        $reviewId = (string) Str::ulid();

        // هماهنگی توسط سرویس برنامه انجام می‌شود
        $this->reviewService->createReview(
            reviewId: $reviewId,
            productId: $validated['product_id'],
            userId: $request->user()->id, // کاربر احراز هویت شده
            comment: $validated['comment'],
            ratingValue: $validated['rating'],
        );

        return response()->json(['id' => $reviewId, 'message' => 'Review created successfully.'], 201);
    }
}
ز. ثبت وابستگی‌ها (Binding) در Service Provider
// app/Infrastructure/Providers/RepositoryServiceProvider.php
<?php

namespace App\Infrastructure\Providers;

use App\Domain\Repositories\ReviewRepository;
use App\Infrastructure\Eloquent\Repositories\EloquentReviewRepository;
use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // رابط دامنه را به پیاده‌سازی Infrastructure متصل می‌کند
        $this->app->bind(
            ReviewRepository::class,
            EloquentReviewRepository::class
        );

        // سایر اتصالات مخزن...
    }
}

این Provider باید در config/app.php ثبت شود.


خلاصه و مزایا

با پیروی از این ساختار در Laravel:

مزیتتوضیح
قابلیت تست‌پذیری عالیDomain و Application شما از Laravel جدا شده‌اند و می‌توان به راحتی Unit Test نوشت.
قابلیت نگهداریتغییر در منطق کسب‌وکار فقط روی لایه Domain تاثیر می‌گذارد. تغییر در database فقط روی Infrastructure تاثیر می‌گذارد.
انعطاف‌پذیریاگر روزی بخواهید Eloquent را با Doctrine یا یک SDK NoSQL عوض کنید، فقط کافی است یک پیاده‌سازی جدید از Repository در Infrastructure بنویسید. Domain و Application شما دست نخورده باقی می‌مانند.
شفافیت و وضوحقوانین کسب‌وکار در Domain متمرکز شده‌اند و خوانایی و درک کد را بسیار افزایش می‌دهند.

این روش ممکن است در ابتدا برای پروژه‌های کوچک پیچیده به نظر برسد، اما برای پروژه‌های با مقیاس متوسط به بالا که منطق کسب‌وکار پیچیده‌ای دارند، یک سرمایه‌گذاری ضروری برای حفظ سرعت توسعه در بلندمدت است. Laravel 12 با ویژگی‌هایی مانند PHP 8.4 و پشتیبانی پیشرفته از Artisan، اجرای این معماری را ساده‌تر از همیشه کرده است.

سبد خرید
پیمایش به بالا