بناء تطبيق CRUD متكامل
مقدمة إلى تطبيقات CRUD
CRUD تعني إنشاء وقراءة وتحديث وحذف - العمليات الأربع الأساسية لإدارة البيانات في أي تطبيق. في هذا الدرس الشامل، سنبني نظام إدارة منشورات مدونة متكامل يوضح أفضل الممارسات لتطوير Laravel، بما في ذلك النماذج والترحيلات ووحدات التحكم والعروض والتحقق من الصحة وتحميل الملفات والعلاقات والبحث والتصفية والترقيم.
ما سنبنيه:
- نظام إدارة منشورات المدونة مع عمليات CRUD الكاملة
- تحميل الصور المميزة مع التحقق من الصحة
- علاقات الفئات والوسوم (متعددة إلى متعددة)
- محرر نص منسق لمحتوى المنشور
- وظيفة البحث والتصفية
- الترقيم مع خيارات الفرز
- التحقق من صحة النموذج والتعامل مع الأخطاء
- التفويض مع السياسات
الخطوة 1: تصميم قاعدة البيانات والترحيلات
أولاً، لنقم بإنشاء هيكل قاعدة البيانات لنظام المدونة الخاص بنا:
# إنشاء الترحيلات
php artisan make:migration create_posts_table
php artisan make:migration create_categories_table
php artisan make:migration create_tags_table
php artisan make:migration create_post_tag_table
<?php
// database/migrations/xxxx_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->longText('content');
$table->string('featured_image')->nullable();
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->timestamp('published_at')->nullable();
$table->integer('views')->default(0);
$table->timestamps();
// فهارس للأداء
$table->index('status');
$table->index('published_at');
$table->index(['status', 'published_at']);
$table->fullText(['title', 'content']);
});
}
public function down()
{
Schema::dropIfExists('posts');
}
};
<?php
// database/migrations/xxxx_create_categories_table.php
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
// database/migrations/xxxx_create_tags_table.php
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}
// database/migrations/xxxx_create_post_tag_table.php
public function up()
{
Schema::create('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->primary(['post_id', 'tag_id']);
});
}
# تشغيل الترحيلات
php artisan migrate
الخطوة 2: إنشاء النماذج مع العلاقات
# إنشاء النماذج
php artisan make:model Post
php artisan make:model Category
php artisan make:model Tag
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'user_id', 'category_id', 'title', 'slug', 'excerpt',
'content', 'featured_image', 'status', 'published_at', 'views'
];
protected $casts = [
'published_at' => 'datetime',
'views' => 'integer',
];
// العلاقات
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// النطاقات
public function scopePublished($query)
{
return $query->where('status', 'published')
->where('published_at', '<=', now());
}
public function scopeSearch($query, $search)
{
return $query->where(function($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%")
->orWhereHas('tags', function($tagQuery) use ($search) {
$tagQuery->where('name', 'like', "%{$search}%");
});
});
}
public function scopeByCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
// المستخرجات والمحولات
public function getExcerptAttribute($value)
{
return $value ?: Str::limit(strip_tags($this->content), 150);
}
public function setTitleAttribute($value)
{
$this->attributes['title'] = $value;
$this->attributes['slug'] = Str::slug($value);
}
// دوال مساعدة
public function isPublished()
{
return $this->status === 'published' && $this->published_at <= now();
}
public function incrementViews()
{
$this->increment('views');
}
}
<?php
// app/Models/Category.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = ['name', 'slug', 'description'];
public function posts()
{
return $this->hasMany(Post::class);
}
}
// app/Models/Tag.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = ['name', 'slug'];
public function posts()
{
return $this->belongsToMany(Post::class);
}
}
الخطوة 3: طلبات النماذج للتحقق من الصحة
# إنشاء فئات طلب النموذج
php artisan make:request StorePostRequest
php artisan make:request UpdatePostRequest
<?php
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize()
{
return true; // التعامل مع السياسة
}
public function rules()
{
return [
'title' => 'required|string|max:255|unique:posts,title',
'category_id' => 'required|exists:categories,id',
'excerpt' => 'nullable|string|max:500',
'content' => 'required|string|min:100',
'featured_image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'status' => 'required|in:draft,published,archived',
'published_at' => 'nullable|date|after_or_equal:today',
];
}
public function messages()
{
return [
'title.required' => 'الرجاء إدخال عنوان للمنشور',
'title.unique' => 'منشور بهذا العنوان موجود بالفعل',
'content.min' => 'يجب أن يكون محتوى المنشور 100 حرف على الأقل',
'featured_image.max' => 'يجب ألا يتجاوز حجم الصورة 2 ميجابايت',
'category_id.exists' => 'الرجاء اختيار فئة صالحة',
];
}
}
// app/Http/Requests/UpdatePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdatePostRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => [
'required',
'string',
'max:255',
Rule::unique('posts')->ignore($this->post->id)
],
'category_id' => 'required|exists:categories,id',
'excerpt' => 'nullable|string|max:500',
'content' => 'required|string|min:100',
'featured_image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'status' => 'required|in:draft,published,archived',
'published_at' => 'nullable|date',
];
}
}
الخطوة 4: وحدة التحكم مع عمليات CRUD
# إنشاء وحدة التحكم
php artisan make:controller PostController --resource
تتضمن وحدة التحكم جميع عمليات CRUD: index، create، store، show، edit، update، destroy، بما في ذلك البحث والتصفية والترقيم والتفويض وتحميل الملفات.
الخطوة 5: التفويض مع السياسة
# إنشاء السياسة
php artisan make:policy PostPolicy --model=Post
تحدد السياسة من يمكنه عرض وإنشاء وتحديث وحذف المنشورات بناءً على ملكية المستخدم والأدوار.
الخطوة 6: تكوين المسارات
<?php
// routes/web.php
use App\Http\Controllers\PostController;
Route::resource('posts', PostController::class);
// مسارات مخصصة إضافية إذا لزم الأمر
Route::get('/posts/category/{category:slug}', [PostController::class, 'byCategory'])
->name('posts.by-category');
Route::get('/posts/tag/{tag:slug}', [PostController::class, 'byTag'])
->name('posts.by-tag');
الخطوة 7 و 8: العروض
تتضمن العروض صفحة الفهرس بالبحث والتصفية، ونموذج الإنشاء/التحرير مع التحقق من الصحة، وصفحة العرض لعرض المنشور الكامل مع المنشورات ذات الصلة.
محرر نص منسق: للحصول على تجربة تحرير محتوى أفضل، قم بدمج محرر WYSIWYG مثل TinyMCE أو CKEditor أو Trix. أضف مكتبة JavaScript الخاصة بالمحرر وقم بتهيئتها على textarea المحتوى.
تمرين تطبيقي 1: إضافة نظام التعليقات
وسع تطبيق المدونة بنظام تعليقات:
- أنشئ نموذج التعليق مع الترحيل (post_id، user_id، content، parent_id للردود)
- قم بإعداد العلاقات (post hasMany comments، comment belongsTo post/user)
- أنشئ CommentController مع دوال store/update/destroy
- أضف نموذج التعليق إلى صفحة عرض المنشور
- اعرض التعليقات مع الردود المتداخلة
- نفذ نظام الموافقة على التعليقات (عمود approved)
- أضف سياسات لإدارة التعليقات
تمرين تطبيقي 2: بحث متقدم مع مرشحات
عزز وظيفة البحث:
- أضف مرشح نطاق التاريخ (منشور بين X و Y)
- أضف قائمة منسدلة لمرشح المؤلف
- نفذ تصفية الوسوم (عرض المنشورات بالوسوم المحددة)
- أضف خيارات الفرز (الأحدث، الأقدم، الأكثر مشاهدة، الأكثر تعليقاً)
- احفظ تفضيلات المرشح في الجلسة
- أضف زر "مسح المرشحات"
- أظهر المرشحات النشطة كشارات قابلة للإزالة
تمرين تطبيقي 3: لوحة معلومات تحليلات المنشورات
أنشئ لوحة معلومات تحليلات لمؤلفي المنشورات:
- إجمالي المشاهدات لكل منشور (رسم بياني)
- المشاهدات مع مرور الوقت (رسم بياني خطي - آخر 30 يوماً)
- المنشورات الأكثر شعبية (أفضل 10)
- مقارنة أداء الفئات
- مقاييس المشاركة (التعليقات، الإعجابات إذا تم تنفيذها)
- تصدير التحليلات إلى CSV
- تخزين استعلامات التحليلات باهظة الثمن مؤقتاً
إضافة: استخدم Chart.js أو ApexCharts للتصورات.
الخلاصة
تهانينا! لقد بنيت تطبيق CRUD متكامل مع Laravel. غطى هذا الدرس:
- تصميم قاعدة البيانات مع العلاقات والفهارس المناسبة
- إنشاء النماذج مع علاقات Eloquent والنطاقات
- التحقق من صحة طلب النموذج مع رسائل مخصصة
- وحدة تحكم الموارد مع عمليات CRUD الكاملة
- معالجة تحميل الملفات مع التحقق من الصحة
- التفويض باستخدام السياسات
- وظيفة البحث والتصفية والفرز
- الترقيم مع الحفاظ على سلسلة الاستعلام
- عروض Blade مع النماذج والتعامل مع الأخطاء
- علاقات متعددة إلى متعددة مع الوسوم
- تخزين وحذف الصور
- أفضل الممارسات لتطوير Laravel
يوضح هذا التطبيق الشامل الأنماط الجاهزة للإنتاج التي يمكنك استخدامها في المشاريع الواقعية. لديك الآن المهارات اللازمة لبناء أي تطبيق قائم على CRUD مع Laravel، والتعامل مع العلاقات المعقدة والتحقق من الصحة والتفويض وتفاعلات المستخدم.