معمارية Laravel نظيفة: من المتحكّم السمين إلى Services وActions وForm Requests
دليل عملي بأمثلة كود لتحويل متحكّم Laravel السمين إلى بنية نظيفة: Form Requests للتحقّق، وActions وServices لمنطق الأعمال، وAPI Resources للاستجابة، مع قاعدة متى تستخدم كلًّا منها.
لنبدأ بمشهد يعرفه كل من عمل على مشروع Laravel كبَر مع الوقت. تفتح ملف المتحكّم (Controller) لتعدّل سطرًا بسيطًا، فتجد دالّة واحدة بطول مئة سطر: تحقّق من المدخلات، ومنطق أعمال، واستعلامات قاعدة بيانات، وإرسال بريد، وكتابة في السجلّ، كلّها متشابكة. أيّ تعديل صغير يصبح مغامرة، والاختبار شبه مستحيل، وإعادة استخدام المنطق في مكان آخر تعني نسخه ولصقه. هذا ما يسمّيه المطوّرون «المتحكّم السمين» (Fat Controller)، وهو أحد أكثر مصادر الفوضى شيوعًا في تطبيقات Laravel.
الحلّ ليس معقّدًا، لكنه يتطلّب انضباطًا في توزيع المسؤوليات. سنأخذ مثالًا واحدًا — تسجيل مستخدم جديد — ونرحل به من فوضى المتحكّم السمين إلى بنية نظيفة، طبقةً طبقة.
نقطة البداية: المتحكّم الذي يفعل كل شيء
إليك الشكل الذي نريد الهروب منه: متحكّم يتحقّق ويعالج ويحفظ ويُشعر، كلّه في مكان واحد.
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$user->assignRole('member');
Mail::to($user)->send(new WelcomeMail($user));
Log::info('New user registered: ' . $user->id);
return redirect()->route('users.index');
}يعمل هذا الكود، نعم. لكنه يخلط أربع مسؤوليات منفصلة: التحقّق، ومنطق الأعمال، والآثار الجانبية (البريد والسجلّ)، والاستجابة. لنفصلها.
الطبقة الأولى: Form Request للتحقّق
أوّل ما نخرجه من المتحكّم هو منطق التحقّق. يوفّر Laravel صنف «طلب النموذج» (Form Request) المخصّص لهذا الغرض تحديدًا. أنشئه بالأمر:
php artisan make:request StoreUserRequestثم ضع فيه قواعد التحقّق والصلاحية:
class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', User::class);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8'],
];
}
}الآن صار التحقّق والتفويض في مكان واحد مسؤول عنهما فقط، ويمكن اختبارهما بمعزل عن أيّ شيء آخر.
الطبقة الثانية: Action لعملية واحدة
هنا يأتي القرار المعماري الأهمّ: أين يذهب منطق إنشاء المستخدم؟ أمامك خياران شائعان، الأنماط Action وService، والفرق بينهما عملي لا عقائدي.
صنف «الإجراء» (Action) يغلّف عملية أعمال واحدة كاملة، بدالّة عامّة واحدة (عادةً handle أو execute). أنشئ مجلّد app/Actions وضع فيه:
namespace App\Actions\Users;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class CreateUserAction
{
public function handle(array $data): User
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
$user->assignRole('member');
return $user;
}
}ميزة الإجراء أنه «لا يعرف» من استدعاه: يعمل من المتحكّم، أو من أمر طرفية (Console Command)، أو من مهمّة في الطابور (Queued Job)، أو من اختبار، بالشيفرة نفسها تمامًا. عملية واحدة، مسؤولية واحدة، قابلة لإعادة الاستخدام في أيّ سياق.
متى Action ومتى Service؟
السؤال يربك كثيرين، والإجابة أبسط مما يبدو. استخدم Action حين يكون المنطق عملية مكتملة مدفوعة بحدث أو إجراء مستخدم: CreateOrderAction، أو PublishPostAction، أو CancelSubscriptionAction. أمّا Service فيناسب تجميع منطق مترابط حول كيان أو مجال معيّن في صنف واحد متعدّد الدوالّ، مثل خدمة تتعامل مع واجهة برمجية خارجية، أو حاسبة معقّدة، أو منطق يُعاد استخدامه في عدّة عمليات.
قاعدة عملية مفيدة: إن كان ما تكتبه «فعلًا» (أنشئ، احذف، انشر) فهو غالبًا Action؛ وإن كان «معالجة بيانات» أو «تكاملًا مع نظام خارجي» فهو غالبًا Service. ولا تثقل مشروعًا صغيرًا بطبقات لا يحتاجها؛ الأنماط أدوات لا فروض.
الشكل النهائي: متحكّم يكتفي بالتنسيق
بعد توزيع المسؤوليات، انظر كيف تقلّص المتحكّم إلى جوهره: يستقبل الطلب المُتحقَّق منه، يفوّض العمل، ثم يردّ الاستجابة.
public function store(StoreUserRequest $request, CreateUserAction $action)
{
$user = $action->handle($request->validated());
return redirect()->route('users.index');
}لاحظ أن Laravel يحقن CreateUserAction تلقائيًّا عبر حاوية الخدمات (Service Container) بمجرّد كتابة نوعه في توقيع الدالّة، فلا حاجة لإنشائه يدويًّا. صار المتحكّم ينسّق فقط، لا يحسب.
الطبقة الأخيرة: API Resource للاستجابة
إن كنت تبني واجهة برمجية (API)، فلا تُرجِع نموذج Eloquent خامًا، لأن ذلك يكشف بنية قاعدة بياناتك ويجعل استجابتك تتغيّر بتغيّرها. استخدم «مورد API» (API Resource) طبقةَ تحويل صريحة بين النموذج والاستجابة:
php artisan make:resource UserResourcepublic function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'joined_at' => $this->created_at->toDateString(),
];
}هكذا تتحكّم بدقّة في الحقول المكشوفة وصيغتها، وتبقى استجابتك ثابتة ومستقرّة مهما تغيّر المخطّط الداخلي.
وأين الآثار الجانبية؟ البريد والسجلّ
بقي في مثالنا الأصلي إرسال بريد ترحيب وكتابة في السجلّ. الأنظف ألّا يحملهما الإجراء مباشرةً، بل أن يُطلق حدثًا (Event) بعد إنشاء المستخدم، ويستمع إليه مستمعون (Listeners) منفصلون. بهذا يبقى إنشاء المستخدم نقيًّا، وتُضاف الآثار الجانبية أو تُزال دون لمس منطق الأعمال. هذا الفصل يجعل كلًّا من الإجراء والمستمع قابلًا للاختبار وحده.
البنية التي وصلنا إليها
انتقلنا من دالّة واحدة مكتظّة إلى مسؤوليات واضحة موزّعة: Form Request يتحقّق ويفوّض، وAction ينفّذ عملية الأعمال، وحدث يتولّى الآثار الجانبية، وAPI Resource يشكّل الاستجابة، ومتحكّم نحيف ينسّق بينها. الفائدة ليست جمالية فحسب؛ كل طبقة صارت قابلة للاختبار وحدها، وإعادة استخدام منطق إنشاء المستخدم في أمر طرفية أو استيراد جماعي صارت سطرًا واحدًا. ولمن يريد التوسّع أكثر، تأتي طبقات اختيارية كـ Policies للتفويض المركزي، وQuery Services للاستعلامات المعقّدة، تُضاف عند الحاجة لا قبلها.
القاعدة الذهبية التي تختصر كل ما سبق: المتحكّم ينسّق، ولا يحسب. متى تذكّرت هذا في كل مرّة تكتب فيها دالّة متحكّم، ستجد بنيتك تنتظم تلقائيًّا، وسيشكرك زميلك — أو أنت بعد ستّة أشهر — على كل سطر لم تحشره في المكان الخطأ.
هل وجدت هذا المقال مفيدًا؟