404 Not Found

404 Not Found


nginx

طلبات واستجابات Laravel

الطلبات والاستجابات هي "المدخلات والمخرجات" في Laravel—يُستخرج البيانات عند دخول طلب، وتُغلف عند خروج استجابة؛ وكلا العمليتين تتطلبان تحكماً دقيقاً.

1. ما ستتعلمه


2. قصة حقيقية من مطوّر API

(1) نقطة الألم: كل نقطة نهاية تُرجع البيانات بتنسيق مختلف

كتب Bob عشر نقاط نهاية لواجهة ShopMetrics API—بعضها يُرجع {data: [...]}، وبعضها يُرجع {shops: [...]}، وأخرى تُرجع مصفوفات مباشرة. عند دمج API في الواجهة الأمامية، كان على Alice كتابة منطق تحليل مختلف لكل نقطة نهاية. ولأسوأ من ذلك، كانت البيانات المُقسَّمة تُرجع إمّا total_pages أو last_page، مما جعل من المستحيل توحيد مكون التقسيم في الواجهة الأمامية.

(2) طرق حل توحيد الاستجابات

مورد API في Laravel وماكرو الاستجابة يوحّدان تنسيق المخرجات—جميع نقاط النهاية تُرجع نفس بنية JSON، والبيانات المُقسَّمة تتبع تنسيقاً متسقاً.

PHP
// استجابة موحَّدة — كل نقطة نهاية تتبع هذا التنسيق
return ShopResource::collection($shops);
// {
//   "data": [...],
//   "meta": {"current_page": 1, "total": 50},
//   "links": {"next": "...", "prev": "..."}
// }

(3) العائد

بعد أن اعتمدت Alice تنسيقاً موحّداً، انخفض عدد مجموعات منطق التحليل في الواجهة الأمامية من 10 إلى 1، وبلغ معدل إعادة استخدام مكون التقسيم 100%.


3. كائن الطلب

(1) جلب المدخلات

PHP
// جلب مدخل واحد
$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

PHP
// 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);
}

الناتج:

TEXT
// تم التنفيذ بنجاح

4. رفع الملفات

(1) معالجة الرفع الأساسية

PHP
// التحقق من رفع الملف
$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

PHP
// 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.');
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

5. بناء الاستجابة

(1) أنواع الاستجابة

PHP
// استجابة 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

PHP
// 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',
        ]);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

6. تقسيم API

(1) مقارنة طرق التقسيم

الدالة عدد الاستعلامات المعلومات المُرجعة حالات الاستخدام
paginate() 2 (COUNT+SELECT) total/last_page/links يُطلب إجمالي عدد الصفحات
simplePaginate() 1 (SELECT) روابط next/prev لا يُطلب إجمالي الصفحات
cursorPaginate() 1 (SELECT+WHERE) مؤشر next/prev مجموعة بيانات كبيرة

(2) التقسيم بتنسيق JSON

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) التقسيم بالمؤشر

PHP
// أكثر كفاءة لمجموعات البيانات الكبيرة
$shops = Shop::cursorPaginate(15);
// يستخدم معامل "cursor" بدلاً من "page"
// URL: /api/shops?cursor=eyJpZCI6MTV9

// رابط الصفحة التالية
$shops->nextPageUrl();
// /api/shops?cursor=eyJpZCI6MzB9

(1) ▶ مثال: تقسيم وتصفية ShopMetrics API

PHP
// 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);
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. ماكرو الاستجابة وتوحيد التنسيق

(1) تعريف ماكرو استجابة

PHP
// 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) استخدام ماكرو الاستجابة

PHP
// في المتحكم
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

PHP
// 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(),
            ],
        ]);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: العملية الكاملة لمعالجة طلبات ShopMetrics API

PHP
// ============================================
// شامل: طلب-استجابة 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.
س كيف يجب توحيد تنسيق استجابة API؟
ج أنشئ سمة ApiResponse أو ماكرو استجابة لتغليف الدوال success و error و paginated. يجب على جميع المتحكمات استخدام دوال الاستجابة الموحّدة هذه لضمان الاتساق في أسماء الحقول ورموز الحالة وتنسيقات التقسيم.
س هل يجب استخدام download() أم streamDownload() لتنزيل الملفات الكبيرة؟
ج للملفات الصغيرة، استخدم download() لقراءتها مباشرة في الذاكرة؛ للملفات الكبيرة، استخدم streamDownload() للبث، مما يحافظ على استهلاك الذاكرة ثابتاً. يجب استخدام streamDownload() لسيناريوهات مثل تصدير CSV.
س كيف تتعامل مع اختلافات تنسيقات الاستجابة بين إصدارات API؟
ج كل إصدار من API يستخدم فئة Resource منفصلة (V1/ShopResource مقابل V2/ShopResource)؛ يتم التحكم في حقول المخرجات في دالة toArray() ضمن Resource، وتحدد طبقة التوجيه الإصدار المناسب.
س ما الفرق بين تخزين الملفات المرفوعة محلياً وعلى S3؟
ج التخزين المحلي يكون على قرص الخادم (مجاني لكن غير قابل للتوسع)، بينما S3 في السحابة (الدفع حسب الاستخدام، تسريع CDN، توسع غير محدود). يُنصح بـ S3 لبيئات الإنتاج.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): نفِّذ نقطة النهاية GET /api/v1/shops لواجهة ShopMetrics API، مع دعم معاملات فلتر البحث search والحالة status والفرز sort والاتجاه direction، واستخدم paginate لإرجاع JSON مقسّم.

  2. تمرين متقدم (⭐⭐): أنشئ سمة ApiResponse تغلّف الدوال success و error و paginated، واستخدمها في جميع متحكمات API لضمان تنسيق استجابة متسق.

  3. تحدي (⭐⭐⭐): نفِّذ واجهة لقائمة طلبات مقسّمة بالمؤشر (GET /api/v1/orders?cursor=xxx)، وقارن فروق أداء الاستعلام بين paginate و cursorPaginate عند معالجة 100,000 سجل.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%