📍 الدليل المرجعي: Laravel 11 وPHP 8.4
API REST مُصَمَّمة جيّدًا أساس أيّ مشروع حديث: تطبيقات محمولة، SPAs، تكاملات خارجية، microservices — كلّها تستهلك endpoints HTTP مُهَيكَلَة. Laravel 11 يجعل إنشاء API نظيفًا بفضل routes opt-in، controllers ressource، Form Requests للتحقّق، وAPI Resources لتسلسل ردود JSON. يبني هذا الدليل API كاملة لإدارة مقالات مدوّنة، خطوة بخطوة.
المتطلّبات
- PHP 8.2 كحدّ أدنى (8.4 موصى به).
- Laravel 11 مثبَّت.
- قاعدة بيانات مُهَيَّأة (MySQL، PostgreSQL، أو SQLite).
- Postman، Insomnia، أو curl لاختبار endpoints.
- الوقت: 60 إلى 90 دقيقة.
الخطوة 1 — تفعيل routing API في Laravel 11
في Laravel 11، ملفّ routes/api.php لا يُنشَأ افتراضيًّا. لتفعيل routing API وتثبيت Sanctum:
php artisan install:api
هذا الأمر يُنشئ routes/api.php، يُثَبِّت Sanctum، ينشر migrations وينفّذها. كلّ routes في routes/api.php تستقبل تلقائيًّا بادئة /api وmiddleware api.
php artisan route:list
الخطوة 2 — إنشاء النموذج، migration، وcontroller
php artisan make:model Article -msc --api
هذا يُنشئ app/Models/Article.php، migration، seeder، وcontroller API بـ index/store/show/update/destroy. عَرِّف schema في migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->string('slug')->unique();
$table->enum('status', ['draft', 'published'])->default('draft');
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('articles');
}
};
php artisan migrate
الخطوة 3 — تهيئة نموذج Article
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class Article extends Model
{
protected $fillable = [
'title', 'content', 'slug', 'status', 'published_at'
];
protected $casts = [
'published_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
protected static function booted(): void
{
static::creating(function (Article $article) {
if (empty($article->slug)) {
$article->slug = Str::slug($article->title);
}
});
}
}
الـ hook booted() مع creating observer inline: مع كلّ إنشاء، إن لم يُوَفَّر slug، Laravel يُوَلِّده تلقائيًّا من العنوان.
مهمّ: لكي يعمل $user->articles()->create(...) في controller، أضف العلاقة العكسية في User:
<?php
// app/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Authenticatable
{
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
}
الخطوة 4 — التحقّق بـ Form Requests
التحقّق لا يتمّ مباشرة في controller — هذا دور Form Requests. الفصل يُبقي controller نظيفًا ويُمَركِز قواعد التحقّق.
php artisan make:request StoreArticleRequest
php artisan make:request UpdateArticleRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreArticleRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:5', 'max:255'],
'content' => ['required', 'string', 'min:50'],
'slug' => ['nullable', 'string', 'unique:articles,slug'],
'status' => ['in:draft,published'],
'published_at' => ['nullable', 'date', 'required_if:status,published'],
];
}
}
إن أخفق التحقّق، Laravel يُرجع 422 Unprocessable Content مع تفصيل الأخطاء — تمامًا ما ينتظره عميل API. authorize() يُقَيَّم مسبقًا: إن أرجع false، Laravel يردّ 403.
الخطوة 5 — تنفيذ controller API
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Http\Resources\ArticleResource;
use App\Models\Article;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class ArticleController extends Controller
{
// GET /api/articles
public function index(): AnonymousResourceCollection
{
$articles = Article::with('user')->latest()->paginate(15);
return ArticleResource::collection($articles);
}
// POST /api/articles
public function store(StoreArticleRequest $request): ArticleResource
{
$article = $request->user()->articles()->create($request->validated());
return new ArticleResource($article);
}
// GET /api/articles/{article}
public function show(Article $article): ArticleResource
{
return new ArticleResource($article->load('user'));
}
// PUT /api/articles/{article}
public function update(UpdateArticleRequest $request, Article $article): ArticleResource
{
$article->update($request->validated());
return new ArticleResource($article);
}
// DELETE /api/articles/{article}
public function destroy(Article $article): JsonResponse
{
$article->delete();
return response()->json(null, 204);
}
}
show(Article $article) يستعمل Route Model Binding: حين تستقبل route معامل {article}، Laravel يستعلم تلقائيًّا ويحقن المثيل. إن لم يوجد، 404 تلقائي.
الخطوة 6 — API Resource لتسلسل الردود
php artisan make:resource ArticleResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ArticleResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'content' => $this->content,
'status' => $this->status,
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'author' => [
'id' => $this->user?->id,
'name' => $this->user?->name,
],
];
}
}
الصياغة $this->published_at?->toIso8601String() تستعمل nullsafe في PHP: إن كان null، التعبير يُرجع null دون رفع استثناء.
الخطوة 7 — إعلان routes واختبار API
<?php
// routes/api.php
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
// Routes عامّة
Route::apiResource('articles', ArticleController::class)->only(['index', 'show']);
// Routes محمية
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('articles', ArticleController::class)->except(['index', 'show']);
});
php artisan route:list --path=api/articles
curl -s http://localhost:8000/api/articles | jq .
الردّ كائن JSON بمفتاح data (مصفوفة المقالات) وmeta بمعلومات pagination.
أخطاء شائعة
| الخطأ | السبب | الحلّ |
|---|---|---|
| 404 على /api/articles | routes/api.php غائب | php artisan install:api |
| 422 على POST مع بيانات صحيحة | authorize() يُرجع false |
أرجِع true للـ routes العامّة أو تحقّق من التوثيق |
| user_id لا يُملأ تلقائيًّا | استعمال Article::create() |
أنشئ دائمًا عبر $request->user()->articles()->create() |
| JSON يحوي حقولًا حسّاسة | إرجاع النموذج مباشرة | غَلِّف الكلّ في ArticleResource |
FAQ
هل نستعمل دائمًا API Resources؟ نعم في الإنتاج. إرجاع النموذج مباشرة يكشف كلّ الخصائص.
كيف ننسخ API؟ ببادئة routes: Route::prefix('v1')->group(...).
الفرق بين apiResource وresource؟ resource يُولِّد 7 routes (مع create وedit). apiResource يُولِّد 5 فقط — مناسبة لـ API REST.