بناء أنظمة سير عمل الموافقة القوية في Laravel #
تعتبر سير عمل الموافقة (Approval Workflows) ميزة حاسمة في العديد من تطبيقات الويب، حيث تتيح الإشراف على المحتوى، والتحقق من صحة المهام، أو عمليات متعددة المراحل قبل أن تصبح بيانات معينة عامة أو نشطة. سواء كنت تقوم ببناء مدونة تتطلب منشوراتها موافقة المشرف، أو منصة للتجارة الإلكترونية تتطلب قوائم المنتجات فحصًا، أو موقعًا لمحتوى من إنشاء المستخدمين، فإن نظام الموافقة القوي لا غنى عنه.
سيرشدك هذا البرنامج التعليمي خلال بناء سير عمل موافقة مرن في Laravel، يغطي تصميم قاعدة البيانات، وسياسات التخويل، ومنطق المتحكمات (Controllers)، وإشعارات المستخدمين.
1. تصميم قاعدة البيانات والترحيلات (Migrations) #
أولاً، دعنا نصمم قاعدة البيانات الخاصة بنا لاستيعاب حالة الموافقة. سنضيف عمود status إلى جدول النموذج المستهدف (مثل posts). سنقوم أيضًا بتضمين approved_by_id و approved_at لأغراض التدقيق.
لنفترض أن لديك جدول posts. إذا لم يكن كذلك، فقم بإنشاء نموذج Post وترحيل أولاً.
php artisan make:model Post -m
عدّل ترحيل create_posts_table الخاص بك لتضمين الأعمدة الجديدة:
// database/migrations/YYYY_MM_DD_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(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('content');
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
$table->foreignId('approved_by_id')->nullable()->constrained('users')->onDelete('set null');
$table->timestamp('approved_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
قم بتشغيل الترحيلات الخاصة بك:
php artisan migrate
2. إعداد النموذج (Model) #
قم بتحديث نموذج Post الخاص بك لتضمين السمات القابلة للتعبئة (fillable)، وعمليات التحويل (casts)، والعلاقات.
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'title',
'content',
'status',
'approved_by_id',
'approved_at',
];
protected $casts = [
'approved_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function approver()
{
return $this->belongsTo(User::class, 'approved_by_id');
}
// نطاقات لسهولة الاستعلام
public function scopeApproved(Builder $query): void
{
$query->where('status', 'approved');
}
public function scopePending(Builder $query): void
{
$query->where('status', 'pending');
}
public function scopeRejected(Builder $query): void
{
$query->where('status', 'rejected');
}
public function isPending(): bool
{
return $this->status === 'pending';
}
public function isApproved(): bool
{
return $this->status === 'approved';
}
public function isRejected(): bool
{
return $this->status === 'rejected';
}
public function approve(int $approverId)
{
$this->status = 'approved';
$this->approved_by_id = $approverId;
$this->approved_at = now();
$this->save();
}
public function reject(int $approverId)
{
$this->status = 'rejected';
$this->approved_by_id = $approverId;
$this->approved_at = now(); // أو تعيينها إلى null إذا لم يعتبر الرفض حدث موافقة
$this->save();
}
}
3. التخويل باستخدام السياسات (Policies) #
نحتاج إلى تحديد من يمكنه الموافقة أو رفض منشور. تعتبر سياسات Laravel مثالية لذلك. دعنا ننشئ PostPolicy.
php artisan make:policy PostPolicy --model=Post
سجل السياسة في AuthServiceProvider:
// app/Providers/AuthServiceProvider.php
protected $policies = [
Post::class => PostPolicy::class,
];
طبق طريقة approve في PostPolicy. سنفترض أن المستخدمين الذين لديهم دور محدد فقط (مثل 'moderator' أو 'admin') يمكنهم الموافقة.
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\User;
use App\Models\Post;
class PostPolicy
{
/**
* تحديد ما إذا كان المستخدم يمكنه الموافقة على أي منشورات.
*/
public function approveAny(User $user): bool
{
// مثال: يمكن فقط للمستخدمين الذين لديهم دور 'admin' أو 'moderator' الموافقة.
// قد يكون لديك طريقة `hasRole` على نموذج المستخدم الخاص بك.
return $user->isAdmin() || $user->isModerator();
}
/**
* تحديد ما إذا كان المستخدم يمكنه الموافقة على المنشور المحدد.
*/
public function approve(User $user, Post $post): bool
{
// يمكن للمستخدم الموافقة على منشور إذا كان لديه إذن `approveAny`
// والمنشور في حالة انتظار حاليًا.
return ($user->isAdmin() || $user->isModerator()) && $post->isPending();
}
/**
* تحديد ما إذا كان المستخدم يمكنه رفض المنشور المحدد.
*/
public function reject(User $user, Post $post): bool
{
// نفس المنطق للرفض كما هو للموافقة.
return ($user->isAdmin() || $user->isModerator()) && $post->isPending();
}
}
ملاحظة: ستحتاج إلى طرق isAdmin() و isModerator() على نموذج User الخاص بك، أو حزمة أدوار/أذونات قوية مثل laravel-permission من Spatie.
4. منطق المتحكم (Controller Logic) #
الآن، دعنا ننشئ متحكمًا للتعامل مع عملية الموافقة. قد يكون لديك متحكمات منفصلة للمستخدمين الذين يقدمون المنشورات والمشرفين الذين يديرونها.
php artisan make:controller Moderator/PostApprovalController
// app/Http/Controllers/Moderator/PostApprovalController.php
namespace App\Http\Controllers\Moderator;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PostApprovalController extends Controller
{
public function __construct()
{
$this->middleware(['auth']); // التأكد من أن المستخدم مصادق عليه
}
public function pending(Request $request): View
{
$this->authorize('approveAny', Post::class); // التحقق مما إذا كان المستخدم يمكنه الموافقة على أي منشور
$pendingPosts = Post::pending()->latest()->paginate(10);
return view('moderator.posts.pending', compact('pendingPosts'));
}
public function approve(Post $post): RedirectResponse
{
$this->authorize('approve', $post);
$post->approve(auth()->id());
// اختياريًا إرسال إشعار إلى مؤلف المنشور
// $post->user->notify(new PostApproved($post));
return redirect()->route('moderator.posts.pending')->with('success', 'تمت الموافقة على المنشور بنجاح.');
}
public function reject(Post $post): RedirectResponse
{
$this->authorize('reject', $post);
$post->reject(auth()->id());
// اختياريًا إرسال إشعار إلى مؤلف المنشور
// $post->user->notify(new PostRejected($post));
return redirect()->route('moderator.posts.pending')->with('success', 'تم رفض المنشور.');
}
}
5. المسارات (Routes) #
حدد المسارات لعملية الموافقة. من الممارسات الجيدة تجميع مسارات المشرفين.
// routes/web.php
use App\Http\Controllers\Moderator\PostApprovalController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth'])->prefix('moderator')->name('moderator.')->group(function () {
Route::get('posts/pending', [PostApprovalController::class, 'pending'])->name('posts.pending');
Route::post('posts/{post}/approve', [PostApprovalController::class, 'approve'])->name('posts.approve');
Route::post('posts/{post}/reject', [PostApprovalController::class, 'reject'])->name('posts.reject');
});
// مثال على مسار لمستخدم يقدم منشورًا (إنشاء مبدئي)
Route::post('/posts', function (Request $request) {
$request->user()->posts()->create($request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]));
return back()->with('success', 'تم تقديم المنشور للموافقة!');
})->middleware('auth');
// مثال على مسار لعرض المنشورات الموافق عليها
Route::get('/posts', function () {
$approvedPosts = App\Models\Post::approved()->latest()->paginate(10);
return view('posts.index', compact('approvedPosts'));
});
6. الإشعارات (Notifications) #
إبلاغ المؤلف عند الموافقة على منشوره أو رفضه يعزز تجربة المستخدم. دعنا ننشئ فئتي إشعارات.
php artisan make:notification PostApproved
php artisan make:notification PostRejected
// app/Notifications/PostApproved.php
namespace App\Notifications;
use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class PostApproved extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Post $post) { }
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('تمت الموافقة على منشورك!')
->line('أخبار رائعة! تمت الموافقة على منشورك بعنوان "' . $this->post->title . '" وهو الآن مباشر.')
->action('عرض منشورك', url('/posts/' . $this->post->id))
->line('شكراً لك على مساهمتك!');
}
}
// app/Notifications/PostRejected.php
namespace App\Notifications;
use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class PostRejected extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Post $post) { }
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('تحديث بشأن تقديم منشورك')
->line('نأسف لإبلاغك بأنه تم رفض منشورك بعنوان "' . $this->post->title . '".')
->line('يرجى مراجعة إرشادات المحتوى الخاصة بنا إذا كنت ترغب في إعادة التقديم أو لديك أي أسئلة.')
->action('الذهاب إلى لوحة التحكم', url('/dashboard')) // أو صفحة لتعديل المنشور المرفوض
->line('شكراً لتفهمك.');
}
}
تذكر إلغاء التعليق عن استدعاءات notify في PostApprovalController.
7. العروض الأساسية (Basic Views) #
إليك مثال بسيط للعرض pending للمشرف ونموذج تقديم المنشور للمؤلف.
resources/views/moderator/posts/pending.blade.php:
<!-- resources/views/moderator/posts/pending.blade.php -->
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Pending Posts for Approval') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4" role="alert">
{{ session('success') }}
</div>
@endif
@forelse($pendingPosts as $post)
<div class="mb-6 p-4 border rounded">
<h3 class="text-lg font-bold">{{ $post->title }}</h3>
<p class="text-sm text-gray-600">بواسطة: {{ $post->user->name }} في {{ $post->created_at->format('M d, Y') }}</p>
<p class="mt-2">{{ Str::limit($post->content, 200) }}</p>
<div class="mt-4">
<form action="{{ route('moderator.posts.approve', $post) }}" method="POST" class="inline-block">
@csrf
<button type="submit" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">موافقة</button>
</form>
<form action="{{ route('moderator.posts.reject', $post) }}" method="POST" class="inline-block ml-2">
@csrf
<button type="submit" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">رفض</button>
</form>
</div>
</div>
@empty
<p>لا توجد منشورات معلقة للموافقة.</p>
@endforelse
<div class="mt-4">
{{ $pendingPosts->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
الخلاصة #
لقد قمت بنجاح بتطبيق سير عمل موافقة أساسي ولكنه قوي في تطبيق Laravel الخاص بك. يمكن توسيع هذا النظام بشكل أكبر بميزات مثل الموافقات متعددة المراحل، وسجلات التدقيق، وتعليقات المستخدمين على العناصر المعلقة، أو أنواع مختلفة من المحتوى التي تتطلب عمليات موافقة مختلفة. من خلال الاستفادة من ميزات Laravel المضمنة مثل Eloquent و Policies و Notifications، يمكنك بناء أنظمة معقدة وآمنة بكفاءة.