طلبات واستجابات Laravel
الطلبات والاستجابات هي "المدخلات والمخرجات" في Laravel—يُستخرج البيانات عند دخول طلب، وتُغلف عند خروج استجابة؛ وكلا العمليتين تتطلبان تحكماً دقيقاً.
1. ما ستتعلمه
- كائن الطلب: input/query/has/only/except والتحويل بين الأنواع
- معالجة رفع الملفات: store/storedAs/التكامل مع S3
- بناء الاستجابة: json/view/download/redirect/stream
- تقسيم موارد API: LengthAwarePaginator والتقسيم بالمؤشر
- توحيد وحدات ماكرو الاستجابة وتنسيقات الاستجابة العامة
2. قصة حقيقية من مطوّر API
(1) نقطة الألم: كل نقطة نهاية تُرجع البيانات بتنسيق مختلف
كتب Bob عشر نقاط نهاية لواجهة ShopMetrics API—بعضها يُرجع {data: [...]}، وبعضها يُرجع {shops: [...]}، وأخرى تُرجع مصفوفات مباشرة. عند دمج API في الواجهة الأمامية، كان على Alice كتابة منطق تحليل مختلف لكل نقطة نهاية. ولأسوأ من ذلك، كانت البيانات المُقسَّمة تُرجع إمّا total_pages أو last_page، مما جعل من المستحيل توحيد مكون التقسيم في الواجهة الأمامية.
(2) طرق حل توحيد الاستجابات
مورد API في Laravel وماكرو الاستجابة يوحّدان تنسيق المخرجات—جميع نقاط النهاية تُرجع نفس بنية JSON، والبيانات المُقسَّمة تتبع تنسيقاً متسقاً.
// استجابة موحَّدة — كل نقطة نهاية تتبع هذا التنسيق
return ShopResource::collection($shops);
// {
// "data": [...],
// "meta": {"current_page": 1, "total": 50},
// "links": {"next": "...", "prev": "..."}
// }
(3) العائد
بعد أن اعتمدت Alice تنسيقاً موحّداً، انخفض عدد مجموعات منطق التحليل في الواجهة الأمامية من 10 إلى 1، وبلغ معدل إعادة استخدام مكون التقسيم 100%.
3. كائن الطلب
(1) جلب المدخلات
// جلب مدخل واحد
$name = $request->input('name');
$name = $request->input('name', 'default value');
// جلب من سلسلة الاستعلام فقط
$sort = $request->query('sort', 'created_at');
// جلب مدخلات متعددة
$filtered = $request->only(['name', 'email', 'status']);
$filtered = $request->except(['password', '_token']);
// التحقق من الوجود
if ($request->has('search')) { ... }
if ($request->filled('search')) { ... } // موجود + ليس فارغاً
if ($request->missing('search')) { ... }
// تحويل الأنواع
$page = $request->integer('page', 1);
$active = $request->boolean('active', false);
$date = $request->date('published_at', 'Y-m-d');
| الدالة | الوصف | الفرق |
|---|---|---|
input() |
جميع المدخلات (استعلام+جسم) | الأكثر شيوعاً |
query() |
سلسلة الاستعلام فقط | معاملات GET |
post() |
جسم POST فقط | بيانات النموذج |
only() |
استخراج الحقول المحددة | القائمة البيضاء |
except() |
استبعاد الحقول المحددة | القائمة السوداء |
has() |
الحقل موجود | يتضمن القيم الفارغة |
filled() |
الحقل موجود وليس فارغاً | يستبعد القيم الفارغة |
(1) ▶ مثال: معالجة طلبات ShopMetrics API
// app/Http/Controllers/Api/ShopController.php
public function index(Request $request): JsonResponse
{
$query = Shop::where('tenant_id', tenant()->id);
// فلتر البحث
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
// فلتر الحالة
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// الفرز
$sortField = $request->input('sort', 'created_at');
$sortDir = $request->input('direction', 'desc');
$query->orderBy($sortField, $sortDir);
// التقسيم
$perPage = $request->integer('per_page', 15);
$shops = $query->paginate($perPage);
return ShopResource::collection($shops);
}
الناتج:
// تم التنفيذ بنجاح
4. رفع الملفات
(1) معالجة الرفع الأساسية
// التحقق من رفع الملف
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// تخزين الملف
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123.jpg"
// تخزين باسم مخصص
$path = $request->file('logo')->storeAs(
'shops/logos',
$shop->slug . '.' . $request->file('logo')->extension(),
'public',
);
// جلب معلومات الملف
$file = $request->file('logo');
$file->getClientOriginalName();
$file->getClientOriginalExtension();
$file->getSize(); // بالبايت
$file->getMimeType();
| الدالة | الوصف |
|---|---|
store() |
الحفظ في دليل محدد باسم ملف عشوائي |
storeAs() |
الحفظ في دليل محدد باسم ملف مخصص |
storePublicly() |
الحفظ في الدليل العام |
isValid() |
التحقق من نجاح الرفع |
(1) ▶ مثال: رفع شعار متجر ShopMetrics
// app/Http/Controllers/ShopLogoController.php
class ShopLogoController extends Controller
{
public function update(Request $request, Shop $shop): RedirectResponse
{
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// حذف الشعار القديم إن وُجد
if ($shop->logo_path) {
Storage::disk('public')->delete($shop->logo_path);
}
// تخزين الشعار الجديد
$path = $request->file('logo')->store("shops/{$shop->id}/logos", 'public');
$shop->update(['logo_path' => $path]);
return back()->with('success', 'Logo updated.');
}
}
الناتج:
// تم التنفيذ بنجاح
5. بناء الاستجابة
(1) أنواع الاستجابة
// استجابة JSON
return response()->json(['message' => 'Created', 'data' => $shop], 201);
// استجابة عرض
return response()->view('shops.show', compact('shop'), 200);
// إعادة توجيه
return redirect()->route('shops.show', $shop);
return back()->withInput()->withErrors($errors);
return redirect()->away('https://external-site.com');
// تنزيل ملف
return response()->download(storage_path('app/reports/report.pdf'));
return response()->streamDownload(function () {
echo generateCsv();
}, 'orders.csv', ['Content-Type' => 'text/csv']);
// بدون محتوى
return response()->noContent(); // 204
| نوع الاستجابة | الدالة | رمز حالة HTTP |
|---|---|---|
| JSON | response()->json() |
200/201/422 |
| عرض | response()->view() |
200 |
| إعادة توجيه | redirect() |
302 |
| تنزيل | response()->download() |
200 |
| تنزيل تيار | response()->streamDownload() |
200 |
| بدون محتوى | response()->noContent() |
204 |
(1) ▶ مثال: استجابة تيار تصدير CSV في ShopMetrics
// app/Http/Controllers/ExportController.php
class ExportController extends Controller
{
public function exportOrders(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
$csv = fopen('php://output', 'w');
fputcsv($csv, ['Order ID', 'Customer', 'Total', 'Status', 'Date']);
$shop->orders()
->with('user')
->orderBy('created_at', 'desc')
->chunk(500, function ($orders) use ($csv) {
foreach ($orders as $order) {
fputcsv($csv, [
$order->order_number,
$order->user->name,
$order->total,
$order->status,
$order->created_at->format('Y-m-d'),
]);
}
});
fclose($csv);
}, "orders-{$shop->slug}-" . now()->format('Y-m-d') . '.csv', [
'Content-Type' => 'text/csv',
]);
}
}
الناتج:
// تم التنفيذ بنجاح
6. تقسيم API
(1) مقارنة طرق التقسيم
| الدالة | عدد الاستعلامات | المعلومات المُرجعة | حالات الاستخدام |
|---|---|---|---|
paginate() |
2 (COUNT+SELECT) | total/last_page/links | يُطلب إجمالي عدد الصفحات |
simplePaginate() |
1 (SELECT) | روابط next/prev | لا يُطلب إجمالي الصفحات |
cursorPaginate() |
1 (SELECT+WHERE) | مؤشر next/prev | مجموعة بيانات كبيرة |
(2) التقسيم بتنسيق JSON
{
"data": [
{"id": 1, "name": "Alice Store"},
{"id": 2, "name": "Bob Electronics"}
],
"current_page": 1,
"per_page": 15,
"total": 50,
"last_page": 4,
"from": 1,
"to": 15,
"links": {
"first": "/api/v1/shops?page=1",
"last": "/api/v1/shops?page=4",
"next": "/api/v1/shops?page=2",
"prev": null
}
}
(3) التقسيم بالمؤشر
// أكثر كفاءة لمجموعات البيانات الكبيرة
$shops = Shop::cursorPaginate(15);
// يستخدم معامل "cursor" بدلاً من "page"
// URL: /api/shops?cursor=eyJpZCI6MTV9
// رابط الصفحة التالية
$shops->nextPageUrl();
// /api/shops?cursor=eyJpZCI6MzB9
(1) ▶ مثال: تقسيم وتصفية ShopMetrics API
// app/Http/Controllers/Api/OrderController.php
public function index(Request $request): JsonResponse
{
$query = Order::where('tenant_id', tenant()->id)
->with(['shop', 'user', 'items.product']);
// الفلاتر
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('shop_id')) {
$query->where('shop_id', $request->shop_id);
}
if ($request->filled('date_from')) {
$query->where('created_at', '>=', $request->date('date_from'));
}
if ($request->filled('date_to')) {
$query->where('created_at', '<=', $request->date('date_to'));
}
// الفرز
$query->orderBy(
$request->input('sort_by', 'created_at'),
$request->input('sort_dir', 'desc'),
);
// اختيار استراتيجية التقسيم
$perPage = $request->integer('per_page', 15);
$orders = $request->boolean('cursor', false)
? $query->cursorPaginate($perPage)
: $query->paginate($perPage);
return OrderResource::collection($orders);
}
الناتج:
// تم التنفيذ بنجاح
7. ماكرو الاستجابة وتوحيد التنسيق
(1) تعريف ماكرو استجابة
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Response::macro('apiSuccess', function (mixed $data, string $message = 'Success', int $status = 200) {
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $status);
});
Response::macro('apiError', function (string $message, int $status = 400, array $errors = []) {
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $status);
});
}
(2) استخدام ماكرو الاستجابة
// في المتحكم
return response()->apiSuccess($shop, 'Shop created', 201);
return response()->apiError('Shop not found', 404);
return response()->apiError('Validation failed', 422, $validator->errors()->toArray());
(1) ▶ مثال: تنسيق الاستجابة المعياري لـ ShopMetrics API
// app/Traits/ApiResponse.php
trait ApiResponse
{
protected function success(mixed $data, string $message = 'Success', int $status = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
'timestamp' => now()->toISOString(),
], $status);
}
protected function error(string $message, int $status = 400, array $errors = []): JsonResponse
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
'timestamp' => now()->toISOString(),
], $status);
}
protected function paginated($resource, string $message = 'Success'): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $resource->resolve(),
'meta' => [
'current_page' => $resource->resource->currentPage(),
'per_page' => $resource->resource->perPage(),
'total' => $resource->resource->total(),
'last_page' => $resource->resource->lastPage(),
],
'links' => [
'first' => $resource->resource->url(1),
'last' => $resource->resource->url($resource->resource->lastPage()),
'next' => $resource->resource->nextPageUrl(),
'prev' => $resource->resource->previousPageUrl(),
],
]);
}
}
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: العملية الكاملة لمعالجة طلبات ShopMetrics API
// ============================================
// شامل: طلب-استجابة ShopMetrics API
// يغطي: معالجة المدخلات، رفع الملفات، التقسيم، تنسيق الاستجابة
// ============================================
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$query = Product::where('shop_id', $request->shop_id)
->with('categories');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
if ($request->filled('category')) {
$query->whereHas('categories', fn ($q) => $q->where('slug', $request->category));
}
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->float('min_price'));
}
if ($request->filled('max_price')) {
$query->where('price', '<=', $request->float('max_price'));
}
if ($request->filled('in_stock') && $request->boolean('in_stock')) {
$query->where('stock', '>', 0);
}
$query->orderBy(
$request->input('sort', 'created_at'),
$request->input('direction', 'desc'),
);
$products = $query->paginate($request->integer('per_page', 15));
return $this->paginated(ProductResource::collection($products));
}
public function store(StoreProductRequest $request): JsonResponse
{
$product = Product::create($request->validated());
if ($request->hasFile('image')) {
$path = $request->file('image')->store("products/{$product->id}", 's3');
$product->update(['image_path' => $path]);
}
return $this->success(new ProductResource($product), 'Product created', 201);
}
public function export(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
echo ProductExporter::toCsv($shop);
}, "products-{$shop->slug}.csv", ['Content-Type' => 'text/csv']);
}
}
❓ أسئلة شائعة
input() و query()؟input() يسترجع البيانات من جميع المصادر (سلسلة الاستعلام + جسم الطلب)، بينما query() يسترجع البيانات فقط من سلسلة استعلام URL. من الأوضح استخدام input() لبيانات نماذج POST و query() لمعاملات GET.cursorPaginate و paginate؟paginate لمجموعات البيانات الأصغر من 100,000 سجل (يتطلب عرض إجمالي عدد الصفحات)؛ استخدم cursorPaginate لمجموعات البيانات الأكبر من 100,000 سجل (لا يتطلب استعلام COUNT، أداء أفضل). واجهة التمرير اللانهائي هي الأنسب لـ cursor.ApiResponse أو ماكرو استجابة لتغليف الدوال success و error و paginated. يجب على جميع المتحكمات استخدام دوال الاستجابة الموحّدة هذه لضمان الاتساق في أسماء الحقول ورموز الحالة وتنسيقات التقسيم.download() أم streamDownload() لتنزيل الملفات الكبيرة؟download() لقراءتها مباشرة في الذاكرة؛ للملفات الكبيرة، استخدم streamDownload() للبث، مما يحافظ على استهلاك الذاكرة ثابتاً. يجب استخدام streamDownload() لسيناريوهات مثل تصدير CSV.📖 ملخص
- يوفر كائن الطلب دوال مثل input و query و only و except لجلب المدخلات
- استخدم
store()أوstoreAs()لحفظ الملفات المرفوعة في موقع محدد على القرص - الاستجابة تدعم أنواعاً متعددة: json/view/redirect/download/stream
- هناك ثلاث استراتيجيات تقسيم لـ API: paginate و simplePaginate و cursorPaginate
- التزم بتنسيق مخرجات API موحّد عبر وحدات الماكرو لتجنب عدم اتساق تنسيق كل نقطة نهاية
- استخدم
streamDownloadلبث تصدير الملفات الكبيرة لمنع فيضان الذاكرة
📝 تمارين
-
تمرين أساسي (⭐): نفِّذ نقطة النهاية GET /api/v1/shops لواجهة ShopMetrics API، مع دعم معاملات فلتر البحث search والحالة status والفرز sort والاتجاه direction، واستخدم paginate لإرجاع JSON مقسّم.
-
تمرين متقدم (⭐⭐): أنشئ سمة
ApiResponseتغلّف الدوالsuccessوerrorوpaginated، واستخدمها في جميع متحكمات API لضمان تنسيق استجابة متسق. -
تحدي (⭐⭐⭐): نفِّذ واجهة لقائمة طلبات مقسّمة بالمؤشر (GET /api/v1/orders?cursor=xxx)، وقارن فروق أداء الاستعلام بين
paginateوcursorPaginateعند معالجة 100,000 سجل.



