إطار Laravel

بنية مشروع Laravel والأنماط

20 دقيقة الدرس 45 من 45

بنية مشروع Laravel والأنماط

مع نمو تطبيقات Laravel، تصبح البنية المعمارية المناسبة أمراً بالغ الأهمية. يغطي هذا الدرس الأنماط المعمارية المتقدمة وأفضل الممارسات لبناء تطبيقات قابلة للتوسع والصيانة تتبع مبادئ SOLID وممارسات الكود النظيف.

فهم مبادئ SOLID في Laravel

مبادئ SOLID:
  • Single Responsibility Principle - يجب أن يكون لكل فئة سبب واحد للتغيير
  • Open/Closed Principle - مفتوح للتوسع، مغلق للتعديل
  • Liskov Substitution Principle - يجب أن تكون الفئات المشتقة قابلة للاستبدال بفئاتها الأساسية
  • Interface Segregation Principle - الواجهات المحددة العديدة أفضل من واجهة واحدة عامة الغرض
  • Dependency Inversion Principle - اعتمد على التجريدات، وليس على التنفيذات الملموسة

نمط المستودع (Repository Pattern)

يُجرّد نمط المستودع منطق الوصول إلى البيانات، مما يجعل كودك أكثر قابلية للاختبار والصيانة:

// app/Contracts/Repositories/UserRepositoryInterface.php namespace App\Contracts\Repositories; use App\Models\User; use Illuminate\Database\Eloquent\Collection; interface UserRepositoryInterface { public function all(): Collection; public function find(int $id): ?User; public function create(array $data): User; public function update(int $id, array $data): User; public function delete(int $id): bool; public function findByEmail(string $email): ?User; public function getActiveUsers(): Collection; } // app/Repositories/UserRepository.php namespace App\Repositories; use App\Contracts\Repositories\UserRepositoryInterface; use App\Models\User; use Illuminate\Database\Eloquent\Collection; class UserRepository implements UserRepositoryInterface { public function __construct(protected User $model) { } public function all(): Collection { return $this->model->all(); } public function find(int $id): ?User { return $this->model->find($id); } public function create(array $data): User { return $this->model->create($data); } public function update(int $id, array $data): User { $user = $this->find($id); $user->update($data); return $user->fresh(); } public function delete(int $id): bool { return $this->model->destroy($id) > 0; } public function findByEmail(string $email): ?User { return $this->model->where('email', $email)->first(); } public function getActiveUsers(): Collection { return $this->model->where('is_active', true) ->orderBy('created_at', 'desc') ->get(); } } // الربط في AppServiceProvider use App\Contracts\Repositories\UserRepositoryInterface; use App\Repositories\UserRepository; public function register(): void { $this->app->bind(UserRepositoryInterface::class, UserRepository::class); } // الاستخدام في وحدة التحكم public function __construct( protected UserRepositoryInterface $userRepository ) {} public function index() { $users = $this->userRepository->getActiveUsers(); return view('users.index', compact('users')); }
فوائد نمط المستودع:
  • يفصل منطق الأعمال عن الوصول إلى البيانات
  • يجعل الاختبار أسهل مع مستودعات وهمية
  • يركز استعلامات قاعدة البيانات
  • أسهل لتبديل مصادر البيانات
  • يتبع مبدأ انعكاس التبعية

نمط طبقة الخدمة (Service Layer)

تُغلّف الخدمات منطق الأعمال الذي لا ينتمي إلى وحدات التحكم أو النماذج:

// app/Services/UserService.php namespace App\Services; use App\Contracts\Repositories\UserRepositoryInterface; use App\Events\UserRegistered; use App\Mail\WelcomeEmail; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Mail; class UserService { public function __construct( protected UserRepositoryInterface $userRepository, protected RoleService $roleService ) {} public function registerUser(array $data): User { return DB::transaction(function () use ($data) { // إنشاء مستخدم $user = $this->userRepository->create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); // تعيين دور افتراضي $defaultRole = $this->roleService->getDefaultRole(); $user->roles()->attach($defaultRole); // إرسال بريد الترحيب Mail::to($user)->queue(new WelcomeEmail($user)); // إرسال الحدث event(new UserRegistered($user)); return $user; }); } public function updateProfile(int $userId, array $data): User { $user = $this->userRepository->find($userId); if (isset($data['avatar'])) { $data['avatar'] = $this->uploadAvatar($data['avatar']); } return $this->userRepository->update($userId, $data); } public function deactivateUser(int $userId): bool { return DB::transaction(function () use ($userId) { $user = $this->userRepository->find($userId); // تسجيل الخروج من جميع الجلسات $user->tokens()->delete(); // وضع علامة كغير نشط $this->userRepository->update($userId, [ 'is_active' => false, 'deactivated_at' => now(), ]); return true; }); } protected function uploadAvatar($file): string { // منطق رفع الصورة الرمزية return $file->store('avatars', 'public'); } } // الاستخدام في وحدة التحكم public function __construct( protected UserService $userService ) {} public function register(RegisterRequest $request) { $user = $this->userService->registerUser($request->validated()); return redirect()->route('dashboard') ->with('success', 'مرحباً بك في منصتنا!'); }

فئات الإجراء (Action Classes)

تتبع فئات الإجراء مبدأ المسؤولية الواحدة - فئة واحدة، إجراء واحد:

// app/Actions/User/CreateUserAction.php namespace App\Actions\User; use App\Models\User; use Illuminate\Support\Facades\Hash; class CreateUserAction { public function execute(array $data): User { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); } } // app/Actions/User/SendWelcomeEmailAction.php namespace App\Actions\User; use App\Mail\WelcomeEmail; use App\Models\User; use Illuminate\Support\Facades\Mail; class SendWelcomeEmailAction { public function execute(User $user): void { Mail::to($user)->queue(new WelcomeEmail($user)); } } // app/Actions/User/RegisterUserAction.php namespace App\Actions\User; use App\Models\User; use Illuminate\Support\Facades\DB; class RegisterUserAction { public function __construct( protected CreateUserAction $createUser, protected AssignDefaultRoleAction $assignRole, protected SendWelcomeEmailAction $sendEmail ) {} public function execute(array $data): User { return DB::transaction(function () use ($data) { $user = $this->createUser->execute($data); $this->assignRole->execute($user); $this->sendEmail->execute($user); return $user; }); } } // الاستخدام في وحدة التحكم public function register( RegisterRequest $request, RegisterUserAction $registerUser ) { $user = $registerUser->execute($request->validated()); auth()->login($user); return redirect()->route('dashboard'); }
متى نستخدم فئات الإجراء:
  • استخدمها للعمليات المعقدة ذات الخطوات المتعددة
  • عندما يُستخدم نفس المنطق في أماكن متعددة
  • لقابلية اختبار أفضل للخطوات الفردية
  • تجنب التجريد المفرط للعمليات البسيطة
  • لا تُنشئ إجراءات للعمليات التافهة ذات السطر الواحد

كائنات نقل البيانات (DTOs)

DTOs هي كائنات بسيطة تحمل البيانات بين العمليات:

// app/DataTransferObjects/UserData.php namespace App\DataTransferObjects; readonly class UserData { public function __construct( public string $name, public string $email, public string $password, public ?string $phone = null, public bool $isActive = true, ) {} public static function fromRequest(array $data): self { return new self( name: $data['name'], email: $data['email'], password: $data['password'], phone: $data['phone'] ?? null, isActive: $data['is_active'] ?? true, ); } public static function fromModel(User $user): self { return new self( name: $user->name, email: $user->email, password: $user->password, phone: $user->phone, isActive: $user->is_active, ); } public function toArray(): array { return [ 'name' => $this->name, 'email' => $this->email, 'password' => $this->password, 'phone' => $this->phone, 'is_active' => $this->isActive, ]; } } // الاستخدام public function store(RegisterRequest $request, CreateUserAction $createUser) { $userData = UserData::fromRequest($request->validated()); $user = $createUser->execute($userData->toArray()); return redirect()->route('users.show', $user); } // مع حزمة Spatie Laravel Data (موصى به) composer require spatie/laravel-data // app/Data/UserData.php namespace App\Data; use Spatie\LaravelData\Data; class UserData extends Data { public function __construct( public string $name, public string $email, public string $password, public ?string $phone, ) {} } // التحقق والتحويل التلقائي public function store(UserData $data) { $user = User::create($data->toArray()); return UserData::from($user); }

التصميم المدفوع بالمجال في Laravel

نظم الكود حسب مجالات الأعمال بدلاً من الطبقات التقنية:

// هيكل قائم على المجال app/ ├── Domain/ │ ├── User/ │ │ ├── Models/ │ │ │ └── User.php │ │ ├── Actions/ │ │ │ ├── CreateUserAction.php │ │ │ └── UpdateUserAction.php │ │ ├── DataTransferObjects/ │ │ │ └── UserData.php │ │ ├── Events/ │ │ │ └── UserRegistered.php │ │ ├── Exceptions/ │ │ │ └── UserNotFoundException.php │ │ ├── Policies/ │ │ │ └── UserPolicy.php │ │ └── QueryBuilders/ │ │ └── UserQueryBuilder.php │ │ │ ├── Order/ │ │ ├── Models/ │ │ │ ├── Order.php │ │ │ └── OrderItem.php │ │ ├── Actions/ │ │ │ ├── CreateOrderAction.php │ │ │ ├── ProcessPaymentAction.php │ │ │ └── SendOrderConfirmationAction.php │ │ ├── States/ │ │ │ ├── OrderState.php │ │ │ ├── PendingState.php │ │ │ ├── ProcessingState.php │ │ │ └── CompletedState.php │ │ └── ValueObjects/ │ │ ├── Money.php │ │ └── OrderNumber.php │ │ │ └── Billing/ │ ├── Models/ │ ├── Actions/ │ └── Services/ │ └── Application/ ├── Http/ │ └── Controllers/ │ ├── UserController.php │ └── OrderController.php ├── Console/ └── Providers/ // مثال كائن القيمة namespace App\Domain\Order\ValueObjects; readonly class Money { public function __construct( public float $amount, public string $currency = 'USD' ) { if ($amount < 0) { throw new \InvalidArgumentException('المبلغ لا يمكن أن يكون سالباً'); } } public function add(Money $other): self { $this->ensureSameCurrency($other); return new self($this->amount + $other->amount, $this->currency); } public function format(): string { return match ($this->currency) { 'USD' => '$' . number_format($this->amount, 2), 'EUR' => '€' . number_format($this->amount, 2), default => $this->currency . ' ' . number_format($this->amount, 2), }; } protected function ensureSameCurrency(Money $other): void { if ($this->currency !== $other->currency) { throw new \InvalidArgumentException('يجب أن تتطابق العملات'); } } }

بنية Monolith المعيارية

هيكل تطبيقك كوحدات مستقلة داخل monolith:

// هيكل معياري باستخدام حزمة Laravel Modules composer require nwidart/laravel-modules php artisan module:make Blog php artisan module:make Shop php artisan module:make Forum // الهيكل Modules/ ├── Blog/ │ ├── Config/ │ ├── Console/ │ ├── Database/ │ │ ├── Migrations/ │ │ └── Seeders/ │ ├── Entities/ │ │ └── Post.php │ ├── Http/ │ │ ├── Controllers/ │ │ ├── Middleware/ │ │ └── Requests/ │ ├── Providers/ │ │ └── BlogServiceProvider.php │ ├── Resources/ │ │ ├── assets/ │ │ ├── lang/ │ │ └── views/ │ ├── Routes/ │ │ ├── api.php │ │ └── web.php │ ├── Tests/ │ ├── composer.json │ └── module.json │ └── Shop/ └── (هيكل مشابه) // مزود خدمة الوحدة namespace Modules\Blog\Providers; use Illuminate\Support\ServiceProvider; class BlogServiceProvider extends ServiceProvider { protected string $moduleName = 'Blog'; public function boot(): void { $this->registerTranslations(); $this->registerConfig(); $this->registerViews(); $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations')); } public function register(): void { $this->app->register(RouteServiceProvider::class); } }
اختيار البنية المعمارية المناسبة: التطبيقات الصغيرة (<10 نماذج):
  • هيكل Laravel الافتراضي مع الخدمات كافٍ
  • لا تبالغ في هندسة التطبيقات البسيطة
التطبيقات المتوسطة (10-50 نموذج):
  • نمط المستودع للاستعلامات المعقدة
  • طبقة الخدمة لمنطق الأعمال
  • فئات الإجراء للعمليات القابلة لإعادة الاستخدام
التطبيقات الكبيرة (>50 نموذج):
  • التصميم المدفوع بالمجال
  • Monolith معياري
  • فكر في microservices لمجالات محددة

نمط كائن الاستعلام (Query Object)

// app/Queries/UserQueries/ActiveUsersQuery.php namespace App\Queries\UserQueries; use App\Models\User; use Illuminate\Database\Eloquent\Builder; class ActiveUsersQuery { public function __construct( protected ?string $search = null, protected ?string $role = null, protected ?string $sortBy = 'created_at', protected ?string $sortDirection = 'desc' ) {} public function get() { return $this->query()->get(); } public function paginate(int $perPage = 15) { return $this->query()->paginate($perPage); } protected function query(): Builder { return User::query() ->where('is_active', true) ->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%\")) ->when($this->role, fn($q) => $q->whereHas('roles', fn($q) => $q->where('name', $this->role))) ->orderBy($this->sortBy, $this->sortDirection); } } // الاستخدام في وحدة التحكم public function index(Request $request) { $users = (new ActiveUsersQuery( search: $request->input('search'), role: $request->input('role'), sortBy: $request->input('sort_by', 'created_at'), sortDirection: $request->input('sort_direction', 'desc') ))->paginate(); return view('users.index', compact('users')); }
تمرين 1: أعد هيكلة وحدة تحكم باستخدام الأنماط المعمارية:
  1. خذ وحدة تحكم موجودة مع 5+ دوال
  2. استخرج منطق الأعمال إلى فئات الخدمة
  3. أنشئ واجهات المستودع والتنفيذات
  4. استخدم DTOs لنقل البيانات
  5. أنشئ فئات إجراء للعمليات المعقدة
  6. اكتب اختبارات لجميع الطبقات
تمرين 2: نفذ هيكل التصميم المدفوع بالمجال:
  1. اختر مجال أعمال (مثل الطلب، المخزون، المستخدم)
  2. أنشئ هيكل مجلد المجال
  3. نفذ كائنات القيمة (المال، البريد الإلكتروني، العنوان)
  4. أنشئ أحداث المجال والمستمعين
  5. ابن جذور التجميع مع قواعد الأعمال
  6. افصل طبقة التطبيق عن طبقة المجال
تمرين 3: ابن نظام مدونة معياري:
  1. ثبت حزمة Laravel Modules
  2. أنشئ وحدة Blog مع المنشورات والفئات والتعليقات
  3. نفذ مزودي الخدمة للوحدة
  4. أضف المسارات ووحدات التحكم الخاصة بالوحدة
  5. أنشئ اختبارات الوحدة
  6. ابن وحدة أخرى (مثل Media) وادمجهما

اكتمل الدرس!

تهانينا! لقد أكملت جميع الدروس في هذا البرنامج التعليمي.