متحكمات Laravel والتعامل مع الطلبات
المتحكم هو "قائد الأعمال" في Laravel — يستقبل الطلبات وينسق النماذج والعروض ويعيد الاستجابات؛ جميع المنطق التجاري يُدار من هنا.
1. ما ستتعلمه
- المتحكمات الأساسية والمتحكمات ذات الإجراء الواحد
__invoke - متحكم الموارد: تعيين علم
--resourceلدوال CRUD - متحكم موارد API: علم
--apiمرتبط بمسار - حقن التبعية: حقن على مستوى المنشئ وعلى مستوى الدالة
- تخصيص برمجية المتحكم الوسيطة
2. قصة حقيقية لمطور واجهة خلفية
(1) المشكلة: متحكم "إلهي" من 2000 سطر
في البداية، حشر Bob كل منطق ShopMetrics في متحكم ShopController واحد — إدارة المنتجات ومعالجة الطلبات ومصادقة المستخدمين وتوليد التقارير كانت كلها مجمعة. عندما تضخم الكود إلى 2000 سطر، كان تغيير ميزة واحدة قد يعطل أخرى. عندما تولى Charlie الأمر، استغرق ثلاثة أيام فقط لفك هيكل الكود، واضطرت Alice للانتظار أسبوعين لطلب ميزة بسيطة.
(2) حل متحكم الموارد
يحلل متحكم موارد Laravel عمليات CRUD إلى سبع دوال منفصلة، حيث تؤدي كل دالة مهمة واحدة. وعند دمجه مع ربط المسارات، يتم تعيين عناوين URL والدوال تلقائيًا.
# أمر واحد يولّد متحكم CRUD كامل
php artisan make:controller ShopController --resource
# ينشئ: index()، create()، store()، show()، edit()، update()، destroy()
(3) العائد
بعد أن أعاد Bob هيكلة الكود باستخدام متحكم الموارد، اقتصرت كل دالة على 30 سطرًا أو أقل؛ ميزة Alice الصغيرة أُنجزت في يومين بدلاً من أسبوعين؛ ولم يعد Charlie يقلق من تعطيل الوظائف الحالية عند تولي ميزات جديدة.
3. المتحكم الأساسي
(1) الإنشاء والهيكل
php artisan make:controller HomeController
# ينشئ: app/Http/Controllers/HomeController.php
// app/Http/Controllers/HomeController.php
class HomeController extends Controller
{
public function index(): View
{
return view('home.index');
}
public function about(): View
{
return view('home.about');
}
}
(2) متحكم الإجراء الواحد __invoke
عندما يحتاج المتحكم إلى دالة واحدة فقط، استخدم __invoke بدلاً من دالة مسماة.
php artisan make:controller GenerateReportController --invokable
// app/Http/Controllers/GenerateReportController.php
class GenerateReportController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$report = ReportGenerator::create($request->all());
return redirect()->route('reports.show', $report->id);
}
}
// تسجيل المسار
Route::post('/reports/generate', GenerateReportController::class);
| البُعد | متحكم قياسي | متحكم إجراء واحد |
|---|---|---|
| عدد الدوال | متعددة | 1 __invoke |
| تسجيل المسار | [Ctrl::class, 'method'] |
Ctrl::class |
| حالات الاستخدام | عمليات مرتبطة | عمليات مسؤولية واحدة |
| مثال | ShopController | GenerateReportController |
(1) ▶ مثال: متحكم إجراء واحد لـ ShopMetrics
// app/Http/Controllers/ExportOrdersController.php
class ExportOrdersController extends Controller
{
public function __invoke(Request $request): StreamedResponse
{
$shop = Shop::findOrFail($request->shop_id);
$csv = OrderExporter::toCsv($shop->orders);
return response()->streamDownload(
callback: fn () => print($csv),
name: "orders-{$shop->slug}.csv",
headers: ['Content-Type' => 'text/csv'],
);
}
}
// routes/web.php
Route::post('/shops/{shop}/export', ExportOrdersController::class)
->name('shops.export');
الناتج:
// تم التنفيذ بنجاح
4. متحكم الموارد
(1) إنشاء متحكم موارد
php artisan make:controller ShopController --resource
يولّد تلقائيًا 7 دوال CRUD:
| فعل HTTP | URI | الدالة | الغرض |
|---|---|---|---|
| GET | /shops | index | قائمة |
| GET | /shops/create | create | نموذج إنشاء |
| POST | /shops | store | حفظ سجل جديد |
| GET | /shops/{shop} | show | تفاصيل |
| GET | /shops/{shop}/edit | edit | نموذج تعديل |
| PUT/PATCH | /shops/{shop} | update | تحديث |
| DELETE | /shops/{shop} | destroy | حذف |
(2) تسجيل المسار
// سطر واحد يسجل جميع المسارات السبعة
Route::resource('shops', ShopController::class);
// تقييد بدوال محددة فقط
Route::resource('shops', ShopController::class)->only([
'index', 'show', 'store', 'update', 'destroy',
]);
// استبعاد دوال محددة
Route::resource('shops', ShopController::class)->except([
'create', 'edit',
]);
(1) ▶ مثال: متحكم موارد متجر ShopMetrics
// app/Http/Controllers/ShopController.php
class ShopController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('tenant.resolve')->except('index', 'show');
}
public function index(): View
{
$shops = Shop::with('tenant')->paginate(15);
return view('shops.index', compact('shops'));
}
public function create(): View
{
return view('shops.create');
}
public function store(StoreShopRequest $request): RedirectResponse
{
$shop = Shop::create($request->validated());
return redirect()->route('shops.show', $shop)
->with('success', 'تم إنشاء المتجر بنجاح.');
}
public function show(Shop $shop): View
{
$shop->load('products', 'orders');
return view('shops.show', compact('shop'));
}
public function edit(Shop $shop): View
{
return view('shops.edit', compact('shop'));
}
public function update(UpdateShopRequest $request, Shop $shop): RedirectResponse
{
$shop->update($request->validated());
return redirect()->route('shops.show', $shop)
->with('success', 'تم تحديث المتجر بنجاح.');
}
public function destroy(Shop $shop): RedirectResponse
{
$shop->delete();
return redirect()->route('shops.index')
->with('success', 'تم حذف المتجر بنجاح.');
}
}
الناتج:
// تم التنفيذ بنجاح
5. متحكم موارد API
(1) إنشاء متحكم API
php artisan make:controller Api/ShopController --api
--api مكافئ لـ --resource --except=create,edit، لأن API لا يتطلب صفحة نموذج.
| الدالة | موارد الويب | موارد API |
|---|---|---|
| index | ✅ | ✅ |
| create | ✅ | ❌ |
| store | ✅ | ✅ |
| show | ✅ | ✅ |
| edit | ✅ | ❌ |
| update | ✅ | ✅ |
| destroy | ✅ | ✅ |
(1) ▶ مثال: متحكم API لـ ShopMetrics
// app/Http/Controllers/Api/ShopController.php
class ShopController extends Controller
{
public function __construct()
{
$this->middleware('auth:sanctum');
}
public function index(Request $request): JsonResponse
{
$shops = Shop::query()
->when($request->search, fn($q, $search) => $q->where('name', 'like', "%{$search}%"))
->paginate($request->per_page ?? 15);
return ShopResource::collection($shops);
}
public function store(StoreShopRequest $request): JsonResponse
{
$shop = Shop::create($request->validated());
return new ShopResource($shop);
}
public function show(Shop $shop): JsonResponse
{
return new ShopResource($shop->load('products', 'orders'));
}
public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
{
$shop->update($request->validated());
return new ShopResource($shop);
}
public function destroy(Shop $shop): Response
{
$shop->delete();
return response()->noContent();
}
}
الناتج:
// تم التنفيذ بنجاح
6. حقن التبعية
(1) حقن المنشئ
class OrderController extends Controller
{
public function __construct(
private OrderService $orderService,
private PaymentGateway $payment,
) {}
public function store(StoreOrderRequest $request): RedirectResponse
{
$order = $this->orderService->create($request->validated());
$this->payment->charge($order);
return redirect()->route('orders.show', $order);
}
}
(2) حقن على مستوى الدالة
class ReportController extends Controller
{
public function show(Request $request, Report $report): View
{
// $request يُحقن بواسطة الحاوية
// $report يُحل عبر ربط نموذج المسار
return view('reports.show', compact('report'));
}
}
(3) ربط نموذج المسار
// ربط ضمني — تلميح النوع في دالة المتحكم
Route::get('/shops/{shop}', [ShopController::class, 'show']);
public function show(Shop $shop): View
{
// $shop يُسترجع تلقائيًا من DB بـ {shop}
// مكافئ لـ: Shop::findOrFail($shop)
return view('shops.show', compact('shop'));
}
// مفتاح مخصص — ربط بـ slug بدلاً من id
Route::get('/shops/{shop:slug}', [ShopController::class, 'show']);
// الآن: /shops/alice-store → Shop حيث slug = 'alice-store'
| طريقة الحقن | حالات الاستخدام | دورة الحياة |
|---|---|---|
| المنشئ | مطلوب لجميع دوال المتحكم | الطلب بالكامل |
| مستوى الدالة | مطلوب لدوال محددة فقط | دالة واحدة |
| ربط نموذج المسار | استرجاع النماذج تلقائيًا من URL | دالة واحدة |
(1) ▶ مثال: متحكم طلبات ShopMetrics مع حقن التبعية
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
public function __construct(
private OrderService $orderService,
) {
$this->middleware('auth');
}
public function index(Request $request): View
{
$orders = $request->user()->orders()
->with('shop', 'products')
->latest()
->paginate(15);
return view('orders.index', compact('orders'));
}
public function show(Order $order): View
{
$this->authorize('view', $order);
$order->load('items.product', 'shop', 'payment');
return view('orders.show', compact('order'));
}
}
الناتج:
// تم التنفيذ بنجاح
7. برمجية المتحكم الوسيطة
(1) التعيين في المنشئ
class ShopController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('tenant.resolve')->except('index');
$this->middleware('can:update,shop')->only('update', 'edit');
}
}
(2) التخصيص على مستوى المسار
Route::middleware(['auth', 'admin'])->group(function () {
Route::resource('plans', PlanController::class);
});
| موقع التخصيص | الدقة | حالات الاستخدام |
|---|---|---|
| المنشئ | مستوى الدالة | دوال مختلفة في نفس المتحكم تتطلب برمجيات وسيطة مختلفة |
| تعريف المسار | مجموعة مسارات | مجموعة من المسارات تشترك في برمجية وسيطة |
| Kernel العام | عام | يجب تنفيذه لجميع الطلبات |
(1) ▶ مثال: متحكم لوحة إدارة ShopMetrics
// app/Http/Controllers/Admin/PlanController.php
class PlanController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'role:admin']);
}
public function index(): View
{
$plans = Plan::withCount('subscriptions')->get();
return view('admin.plans.index', compact('plans'));
}
public function store(StorePlanRequest $request): RedirectResponse
{
Plan::create($request->validated());
return redirect()->route('admin.plans.index')
->with('success', 'تم إنشاء الخطة.');
}
}
الناتج:
// تم التنفيذ بنجاح
8. سلسلة الطلب ← المتحكم ← النموذج ← العرض ← الاستجابة
sequenceDiagram
participant C as العميل
participant R as الموجه
participant M as البرمجية الوسيطة
participant Ctrl as المتحكم
participant Model as النموذج
participant V as العرض
C->>R: طلب HTTP
R->>M: تشغيل خط أنابيب البرمجية الوسيطة
M->>Ctrl: استدعاء دالة المتحكم
Ctrl->>Model: استعلام البيانات
Model-->>Ctrl: إرجاع النتائج
Ctrl->>V: تمرير البيانات إلى العرض
V-->>Ctrl: HTML المُعرض
Ctrl-->>C: استجابة HTTP
9. مثال شامل: متحكم CRUD منتجات ShopMetrics
// ============================================
// شامل: ProductController لـ ShopMetrics
// يغطي: متحكم موارد، حقن تبعية، برمجية وسيطة، ربط نموذج
// ============================================
// app/Http/Controllers/ProductController.php
class ProductController extends Controller
{
public function __construct(
private ProductService $productService,
) {
$this->middleware('auth');
$this->middleware('tenant.resolve');
}
public function index(Request $request): View
{
$products = Product::query()
->where('tenant_id', tenant()->id)
->when($request->search, fn($q, $s) => $q->where('name', 'like', "%{$s}%"))
->when($request->category, fn($q, $c) => $q->where('category_id', $c))
->with('category')
->orderBy($request->sort ?? 'created_at', $request->direction ?? 'desc')
->paginate(20);
return view('products.index', compact('products'));
}
public function create(): View
{
$categories = Category::forTenant(tenant()->id)->get();
return view('products.create', compact('categories'));
}
public function store(StoreProductRequest $request): RedirectResponse
{
$product = $this->productService->create(
tenant()->id,
$request->validated(),
);
return redirect()->route('products.show', $product)
->with('success', 'تم إنشاء المنتج بنجاح.');
}
public function show(Product $product): View
{
$this->authorize('view', $product);
$product->load('category', 'orderItems.order');
return view('products.show', compact('product'));
}
public function edit(Product $product): View
{
$this->authorize('update', $product);
$categories = Category::forTenant(tenant()->id)->get();
return view('products.edit', compact('product', 'categories'));
}
public function update(UpdateProductRequest $request, Product $product): RedirectResponse
{
$this->authorize('update', $product);
$product->update($request->validated());
return redirect()->route('products.show', $product)
->with('success', 'تم تحديث المنتج بنجاح.');
}
public function destroy(Product $product): RedirectResponse
{
$this->authorize('delete', $product);
$product->delete();
return redirect()->route('products.index')
->with('success', 'تم حذف المنتج بنجاح.');
}
}
❓ أسئلة شائعة
Route::bind() في RouteServiceProvider لتعريف منطق تحليل مخصص، أو تجاوز دالة resolveRouteBindingQuery() على النموذج.new؟new ينشئ كائنات يدويًا، مما يتطلب منك إدارة سلسلة التبعيات بنفسك؛ مع حقن التبعية، تحل حاوية Laravel التبعيات وتضخها تلقائيًا، مع دعم ربط الواجهات وإدارة Singleton والاختبار الوهمي.Resource بدلاً من JSON مباشرة؟Resource يوحّد تنسيق إخراج JSON، مما يسمح لك بإخفاء الحقول الحساسة وإعادة تسمية الحقول وتداخل البيانات المرتبطة. إرجاع النموذج مباشرة سيكشف جميع الحقات وينتج تنسيقًا غير متسق.DB::raw لكتابة SQL في المتحكم؟DB::raw يتجاوز طبقة أمان Eloquent، مما يجعله عرضة لحقن SQL.📖 ملخص
- المتحكم مسؤول عن استقبال الطلبات وتنسيق النموذج والعرض وإرجاع الاستجابات
- متحكم الإجراء الواحد يستخدم
__invokeللتعامل مع عمليات فردية ليست عمليات CRUD - متحكم الموارد يعيّن تلقائيًا دوال CRUD السبع إلى أفعال HTTP
- متحكم API يحذف
createوeditويُستخدم معapiResource - حقن التبعية يلغي الحاجة لاستخدام
newعند إنشاء كائنات في المتحكمات؛ الحاوية تحل التبعيات تلقائيًا - ربط نموذج المسار يحلل معلمات URL تلقائيًا إلى نسخ نماذج
📝 تمارين
-
تمرين أساسي (⭐): استخدم
make:controller --resourceلإنشاء ProductController لـ ShopMetrics، وسجل مسارات الموارد، ونفذ دالتيindexوshowلإرجاع عروض بسيطة. -
تمرين متقدم (⭐⭐): أنشئ متحكم إجراء واحد
ExportOrdersControllerينفذ وظيفة تصدير CSV، مع حقن OrderService للتعامل مع تحويل البيانات، واستخدامstreamDownloadلإرجاع الملف. -
تحدٍ (⭐⭐⭐): صمم
TenantProductControllerيستخدم ربط نموذج المسار لتحليل{tenant}و{product}، ونفذ عمليات CRUD للمنتجات مع عزل المستأجر للتأكد من أن المستخدمين يمكنهم فقط إدارة المنتجات ضمن مستأجرهم.



