مقدمة إلى ميزات استعلام الـ API
يجب أن توفر واجهات برمجة التطبيقات الحديثة طرقًا مرنة للعملاء للاستعلام والتصفية والترتيب والبحث عبر مجموعات البيانات الكبيرة. بدون هذه القدرات، سيحتاج العملاء إلى جلب جميع البيانات وتصفيتها محليًا—نهج غير فعال وغير عملي. في هذا الدرس الشامل، سنستكشف أنماط Laravel لتنفيذ قدرات قوية للتصفية والترتيب والبحث في واجهات REST API الخاصة بك.
لماذا تهم ميزات الاستعلام
تعد قدرات الاستعلام الفعالة ضرورية لإنشاء واجهات برمجة تطبيقات قوية وسهلة الاستخدام:
فوائد ميزات الاستعلام المتقدمة:
- الأداء: يقلل من نقل البيانات عن طريق إرجاع السجلات المطلوبة فقط
- المرونة: يمكّن العملاء من جلب ما يحتاجونه بالضبط
- تجربة المستخدم: يمكّن البحث في الوقت الفعلي والفلاتر الديناميكية والترتيب المخصص
- كفاءة النطاق الترددي: يقلل من حجم الحمولة عن طريق التصفية على مستوى قاعدة البيانات
- قابلية التوسع: يتعامل مع مجموعات البيانات الكبيرة دون إرباك العملاء
- تجربة المطور: يوفر واجهات API بديهية لمطوري الواجهة الأمامية
التصفية الأساسية باستخدام معلمات الاستعلام
النهج الأكثر شيوعًا للتصفية هو استخدام معلمات استعلام URL:
<?php
// app/Http/Controllers/Api/PostController.php
namespace App\Http\Controllers\Api;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Post::query();
// التصفية حسب الحالة
if ($request->has('status')) {
$query->where('status', $request->status);
}
// التصفية حسب المؤلف
if ($request->has('author_id')) {
$query->where('author_id', $request->author_id);
}
// التصفية حسب نطاق التاريخ
if ($request->has('from_date')) {
$query->where('created_at', '>=', $request->from_date);
}
if ($request->has('to_date')) {
$query->where('created_at', '<=', $request->to_date);
}
$posts = $query->with('author:id,name')
->paginate(15);
return response()->json($posts);
}
}
// أمثلة الاستخدام:
// GET /api/posts?status=published
// GET /api/posts?author_id=5&status=published
// GET /api/posts?from_date=2024-01-01&to_date=2024-12-31
</pre>
أنماط التصفية المتقدمة
1. قيم متعددة لحقل واحد
السماح بالتصفية حسب قيم متعددة باستخدام قوائم مفصولة بفواصل أو صيغة المصفوفة:
<?php
public function index(Request $request): JsonResponse
{
$query = Post::query();
// التصفية حسب حالات متعددة
if ($request->has('status')) {
$statuses = explode(',', $request->status);
$query->whereIn('status', $statuses);
}
// بديل: صيغة المصفوفة
if ($request->has('categories')) {
$categories = $request->input('categories', []);
$query->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('id', $categories);
});
}
$posts = $query->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?status=published,draft
// GET /api/posts?categories[]=1&categories[]=2&categories[]=3
</pre>
2. عوامل المقارنة
دعم عوامل المقارنة المختلفة للحقول الرقمية وحقول التاريخ:
<?php
public function index(Request $request): JsonResponse
{
$query = Post::query();
// التصفية حسب المشاهدات مع دعم العوامل
if ($request->has('views')) {
$operator = $request->input('views_operator', '=');
$value = $request->input('views');
$allowedOperators = ['=', '!=', '>', '>=', '<', '<='];
if (in_array($operator, $allowedOperators)) {
$query->where('views', $operator, $value);
}
}
// بديل: صيغة عامل مضمنة
if ($request->has('rating')) {
$this->applyComparison($query, 'rating', $request->rating);
}
$posts = $query->paginate(15);
return response()->json($posts);
}
protected function applyComparison($query, $field, $value)
{
// تحليل صيغة مثل "gt:4.5"، "lte:100"، "eq:50"
if (preg_match('/^(gt|gte|lt|lte|eq|ne):(.+)$/', $value, $matches)) {
$operator = match ($matches[1]) {
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'eq' => '=',
'ne' => '!=',
};
$query->where($field, $operator, $matches[2]);
} else {
$query->where($field, $value);
}
}
// الاستخدام:
// GET /api/posts?views=1000&views_operator=>=
// GET /api/posts?rating=gt:4.5
// GET /api/posts?price=lte:99.99
</pre>
3. تصفية العلاقات
التصفية بناءً على سمات النموذج ذات الصلة:
<?php
public function index(Request $request): JsonResponse
{
$query = Post::with('author', 'tags');
// التصفية حسب اسم المؤلف
if ($request->filled('author_name')) {
$query->whereHas('author', function ($q) use ($request) {
$q->where('name', 'like', '%' . $request->author_name . '%');
});
}
// التصفية حسب أسماء الوسوم
if ($request->filled('tags')) {
$tags = explode(',', $request->tags);
$query->whereHas('tags', function ($q) use ($tags) {
$q->whereIn('name', $tags);
});
}
// التصفية حسب الحد الأدنى لعدد التعليقات
if ($request->has('min_comments')) {
$query->withCount('comments')
->having('comments_count', '>=', $request->min_comments);
}
$posts = $query->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?author_name=أحمد
// GET /api/posts?tags=laravel,php
// GET /api/posts?min_comments=10
</pre>
نصيحة احترافية: قم دائمًا بالتحقق من صحة معلمات التصفية وتعقيمها لمنع حقن SQL والتأكد من أنه يمكن تصفية الحقول المسموح بها فقط.
تنفيذ الترتيب
تمكين الترتيب المرن باستخدام معلمات الاستعلام:
<?php
public function index(Request $request): JsonResponse
{
$request->validate([
'sort_by' => 'string|in:created_at,title,views,rating',
'sort_direction' => 'string|in:asc,desc',
]);
$query = Post::query();
// تطبيق الترتيب
$sortBy = $request->input('sort_by', 'created_at');
$sortDirection = $request->input('sort_direction', 'desc');
$query->orderBy($sortBy, $sortDirection);
$posts = $query->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?sort_by=views&sort_direction=desc
// GET /api/posts?sort_by=title&sort_direction=asc
</pre>
الترتيب متعدد الأعمدة
دعم الترتيب حسب أعمدة متعددة:
<?php
public function index(Request $request): JsonResponse
{
$query = Post::query();
// صيغة الترتيب: "sort=-views,created_at" (علامة الطرح للتنازلي)
if ($request->has('sort')) {
$sortFields = explode(',', $request->sort);
foreach ($sortFields as $field) {
$direction = 'asc';
if (str_starts_with($field, '-')) {
$direction = 'desc';
$field = substr($field, 1);
}
$allowedFields = ['id', 'created_at', 'title', 'views', 'rating'];
if (in_array($field, $allowedFields)) {
$query->orderBy($field, $direction);
}
}
} else {
// الترتيب الافتراضي
$query->latest();
}
$posts = $query->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?sort=-views,created_at (المشاهدات تنازلي، created_at تصاعدي)
// GET /api/posts?sort=-rating,-created_at (كلاهما تنازلي)
</pre>
ترتيب العلاقات
الترتيب حسب حقول النموذج ذات الصلة:
<?php
public function index(Request $request): JsonResponse
{
$query = Post::query();
// الترتيب حسب اسم المؤلف
if ($request->input('sort_by') === 'author_name') {
$query->join('users', 'posts.author_id', '=', 'users.id')
->orderBy('users.name', $request->input('sort_direction', 'asc'))
->select('posts.*');
} else {
$query->orderBy(
$request->input('sort_by', 'created_at'),
$request->input('sort_direction', 'desc')
);
}
$posts = $query->with('author')->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?sort_by=author_name&sort_direction=asc
</pre>
تنفيذ البحث
البحث الأساسي
تنفيذ بحث نصي بسيط عبر حقول متعددة:
<?php
public function index(Request $request): JsonResponse
{
$request->validate([
'search' => 'string|max:255',
]);
$query = Post::query();
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%")
->orWhere('excerpt', 'like', "%{$search}%");
});
}
$posts = $query->with('author:id,name')
->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?search=Laravel
</pre>
البحث مع العلاقات
توسيع البحث إلى النماذج ذات الصلة:
<?php
public function index(Request $request): JsonResponse
{
$query = Post::query();
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
// البحث في حقول المنشور
$q->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%")
// البحث في اسم المؤلف
->orWhereHas('author', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
})
// البحث في الوسوم
->orWhereHas('tags', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
});
}
$posts = $query->with('author', 'tags')
->distinct()
->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?search=أحمد (يجد منشورات بواسطة المؤلف أحمد أو تحتوي على "أحمد")
</pre>
تحذير: لا يمكن لعامل LIKE مع أحرف البدل البادئة (%search%) استخدام الفهارس بكفاءة. بالنسبة لمجموعات البيانات الكبيرة، ضع في اعتبارك تنفيذ البحث بالنص الكامل باستخدام فهارس MySQL FULLTEXT أو محركات البحث المخصصة مثل Elasticsearch أو Meilisearch أو Algolia.
البحث بالنص الكامل (MySQL)
لأداء أفضل على مجموعات البيانات الكبيرة، استخدم البحث بالنص الكامل في MySQL:
<?php
// Migration
Schema::table('posts', function (Blueprint $table) {
$table->fullText(['title', 'content', 'excerpt']);
});
// Controller
public function index(Request $request): JsonResponse
{
$query = Post::query();
if ($request->filled('search')) {
$search = $request->search;
// البحث بالنص الكامل (أسرع بكثير لمجموعات البيانات الكبيرة)
$query->whereRaw(
'MATCH(title, content, excerpt) AGAINST(? IN BOOLEAN MODE)',
[$search]
);
}
$posts = $query->with('author:id,name')
->paginate(15);
return response()->json($posts);
}
// الاستخدام:
// GET /api/posts?search=Laravel+tutorial
</pre>
Laravel Scout للبحث
للحصول على قدرات بحث متقدمة، استخدم Laravel Scout مع Meilisearch أو Algolia:
<?php
// تثبيت Scout والسائق
// composer require laravel/scout
// composer require meilisearch/meilisearch-php
// جعل النموذج قابلاً للبحث
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'excerpt' => $this->excerpt,
'author_name' => $this->author->name,
'tags' => $this->tags->pluck('name'),
];
}
}
// Controller مع Scout
public function index(Request $request): JsonResponse
{
if ($request->filled('search')) {
// بحث Scout (متسامح مع الأخطاء المطبعية، سريع، ذو صلة)
$posts = Post::search($request->search)
->query(fn ($query) => $query->with('author'))
->paginate(15);
} else {
$posts = Post::with('author')->paginate(15);
}
return response()->json($posts);
}
</pre>
حزمة Spatie Query Builder
توفر حزمة Spatie Laravel Query Builder نهجًا نظيفًا ومعياريًا للتصفية والترتيب وتضمين العلاقات:
<?php
// تثبيت الحزمة
// composer require spatie/laravel-query-builder
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedSort;
class PostController extends Controller
{
public function index(Request $request): JsonResponse
{
$posts = QueryBuilder::for(Post::class)
// الفلاتر المسموح بها
->allowedFilters([
'status',
'author_id',
AllowedFilter::exact('id'),
AllowedFilter::partial('title'),
AllowedFilter::scope('published_after'),
])
// أنواع الترتيب المسموح بها
->allowedSorts([
'created_at',
'title',
'views',
AllowedSort::field('author', 'author_id'),
])
// التضمينات المسموح بها (التحميل المسبق)
->allowedIncludes(['author', 'tags', 'comments'])
// الترتيب الافتراضي
->defaultSort('-created_at')
->paginate(15);
return response()->json($posts);
}
}
// الاستخدام:
// GET /api/posts?filter[status]=published&filter[title]=Laravel
// GET /api/posts?sort=-views,created_at
// GET /api/posts?include=author,tags
// GET /api/posts?filter[status]=published&sort=-created_at&include=author
</pre>
فلاتر مخصصة مع Spatie
إنشاء منطق تصفية مخصص:
<?php
// app/Filters/PopularFilter.php
namespace App\Filters;
use Illuminate\Database\Eloquent\Builder;
use Spatie\QueryBuilder\Filters\Filter;
class PopularFilter implements Filter
{
public function __invoke(Builder $query, $value, string $property): void
{
$query->where('views', '>=', 1000)
->where('rating', '>=', 4.5);
}
}
// Controller
QueryBuilder::for(Post::class)
->allowedFilters([
'status',
AllowedFilter::custom('popular', new PopularFilter()),
])
->paginate(15);
// الاستخدام:
// GET /api/posts?filter[popular]=true
</pre>
التحقق من صحة الطلب لمعلمات الاستعلام
قم دائمًا بالتحقق من صحة معلمات الاستعلام لضمان سلامة البيانات:
<?php
// app/Http/Requests/PostIndexRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PostIndexRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
// التصفية
'status' => 'string|in:draft,published,archived',
'author_id' => 'integer|exists:users,id',
'from_date' => 'date',
'to_date' => 'date|after_or_equal:from_date',
'tags' => 'string',
// الترتيب
'sort_by' => 'string|in:created_at,title,views,rating',
'sort_direction' => 'string|in:asc,desc',
// البحث
'search' => 'string|max:255',
// ترقيم الصفحات
'per_page' => 'integer|min:1|max:100',
];
}
}
// Controller
public function index(PostIndexRequest $request): JsonResponse
{
// جميع المعلمات الآن تم التحقق من صحتها
$query = Post::query();
// منطق التصفية والترتيب والبحث الخاص بك هنا...
return response()->json($query->paginate($request->input('per_page', 15)));
}
</pre>
تحسين الأداء
1. فهارس قاعدة البيانات
<?php
// Migration
Schema::table('posts', function (Blueprint $table) {
// فهارس للفلاتر الشائعة
$table->index('status');
$table->index('author_id');
$table->index('created_at');
// فهارس مركبة للفلاتر المجمعة
$table->index(['status', 'created_at']);
$table->index(['author_id', 'status']);
// فهرس النص الكامل للبحث
$table->fullText(['title', 'content']);
});
</pre>
2. حدد فقط الأعمدة المطلوبة
<?php
// غير فعال: يحدد جميع الأعمدة
$posts = Post::where('status', 'published')->get();
// فعال: يحدد فقط الأعمدة المطلوبة
$posts = Post::select(['id', 'title', 'excerpt', 'author_id', 'created_at'])
->where('status', 'published')
->get();
</pre>
3. تحميل العلاقات مسبقًا
<?php
// مشكلة N+1
$posts = Post::where('status', 'published')->get();
// كل منشور يطلق استعلام مؤلف منفصل
// الحل: التحميل المسبق
$posts = Post::with('author:id,name', 'tags:id,name')
->where('status', 'published')
->get();
</pre>
تمرين عملي:
- نفذ نظام تصفية شامل لـ Product API يدعم نطاقات الأسعار والفئات والعلامات التجارية والتقييمات وحالة التوفر
- قم ببناء نظام ترتيب متعدد الأعمدة يسمح بالترتيب حسب ما يصل إلى 3 حقول مختلفة في وقت واحد
- أنشئ ميزة بحث تبحث عبر المنتجات والفئات والعلامات التجارية مع تسجيل الصلة
- قم بدمج Laravel Scout مع Meilisearch للبحث السريع المتسامح مع الأخطاء المطبعية على blog API
- نفذ حزمة Spatie Query Builder لـ User API مع فلاتر مخصصة لعمر الحساب ومستوى النشاط وحالة الاشتراك
أفضل الممارسات
أفضل ممارسات ميزات الاستعلام:
- قائمة بيضاء بالحقول المسموح بها: اسمح فقط بالتصفية/الترتيب على أعمدة محددة وآمنة
- تحقق من جميع المدخلات: استخدم Form Requests للتحقق من معلمات الاستعلام
- أضف فهارس قاعدة البيانات: فهرس جميع الأعمدة القابلة للتصفية والترتيب
- استخدم التحميل المسبق: تجنب استعلامات N+1 باستخدام with()
- حدد نطاق البحث: لا تبحث عبر عدد كبير جدًا من الحقول أو استخدم البحث بالنص الكامل
- وثق واجهة برمجة التطبيقات الخاصة بك: وثق بوضوح الفلاتر والترتيبات وقدرات البحث المتاحة
- حدد الحدود المعقولة: حدد طول استعلام البحث وعدد النتائج
- ضع في اعتبارك التخزين المؤقت: قم بتخزين مجموعات الفلاتر المستخدمة بشكل متكرر مؤقتًا
الملخص
في هذا الدرس، أتقنت تنفيذ قدرات التصفية والترتيب والبحث في Laravel APIs. أنت الآن تفهم كيفية بناء أنظمة تصفية مرنة بعوامل متعددة، وتنفيذ الترتيب الفردي ومتعدد الأعمدة، وإنشاء ميزات بحث قوية باستعلامات LIKE وفهارس النص الكامل، ودمج Laravel Scout للبحث المتقدم، واستخدام حزمة Spatie Query Builder للاستعلام الموحد. هذه ميزات الاستعلام ضرورية لإنشاء واجهات برمجة تطبيقات مرنة وفعالة تمكن العملاء من الوصول بكفاءة إلى البيانات التي يحتاجونها بالضبط.