السياسات والبوابات في Laravel
التفويض هو جانب حاسم في أي تطبيق. يوفر Laravel طريقتين أساسيتين لتفويض الإجراءات: البوابات (Gates) والسياسات (Policies). البوابات هي callbacks بسيطة تعتمد على الإغلاق، بينما السياسات هي فئات تنظم منطق التفويض حول نموذج أو مورد معين. دعنا نستكشف كلا النهجين بعمق.
فهم التفويض
يحدد التفويض ما إذا كان يُسمح للمستخدم بتنفيذ إجراء معين. تسهل ميزات التفويض في Laravel تنظيم منطق التفويض والحفاظ على المتحكمات نظيفة:
المصادقة مقابل التفويض:
- المصادقة: من أنت؟ (تسجيل الدخول، التحقق من الهوية)
- التفويض: ماذا يمكنك أن تفعل؟ (الأذونات، التحكم في الوصول)
- يتعامل Laravel مع كليهما بأناقة مع الأدوات المدمجة
تعريف البوابات
البوابات هي إغلاقات بسيطة تحدد ما إذا كان المستخدم مصرحاً له بتنفيذ إجراء معين. عرّف البوابات في طريقة boot في AuthServiceProvider الخاص بك:
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* تسجيل أي خدمات مصادقة / تفويض.
*/
public function boot(): void
{
// بوابة بسيطة
Gate::define('view-admin-dashboard', function (User $user) {
return $user->isAdmin();
});
// بوابة مع وسائط إضافية
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
// بوابة تسمح للمستخدمين الضيوف (User nullable)
Gate::define('view-public-content', function (?User $user) {
return true; // يمكن لأي شخص العرض
});
// منطق تفويض معقد
Gate::define('delete-post', function (User $user, Post $post) {
// المالك يمكنه حذف منشوراته الخاصة
if ($user->id === $post->user_id) {
return true;
}
// المسؤولون يمكنهم حذف أي منشور
if ($user->isAdmin()) {
return true;
}
// المشرفون يمكنهم حذف المنشورات في فئاتهم
if ($user->isModerator() && $user->moderatesCategory($post->category_id)) {
return true;
}
return false;
});
// استجابات البوابة مع الرسائل
Gate::define('purchase-course', function (User $user, $course) {
if ($user->hasActiveMembership()) {
return true;
}
return Gate::deny('تحتاج إلى عضوية نشطة لشراء الدورات.');
});
// خطافات Before - تعمل قبل جميع البوابات
Gate::before(function (User $user, string $ability) {
// المسؤولون الفائقون يمكنهم فعل كل شيء
if ($user->isSuperAdmin()) {
return true;
}
// لا تُرجع شيئاً لمتابعة التفويض العادي
});
// خطافات After - تعمل بعد جميع البوابات
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
// تسجيل فحوصات التفويض
if ($result === false) {
logger()->warning("محاولة وصول غير مصرح بها", [
'user' => $user->id,
'ability' => $ability
]);
}
});
}
}
استخدام البوابات
بمجرد تعريف البوابات، يمكنك فحصها بطرق مختلفة في جميع أنحاء تطبيقك:
// في المتحكمات
use Illuminate\Support\Facades\Gate;
public function index()
{
// تحقق مما إذا كان المستخدم يمكنه تنفيذ الإجراء
if (Gate::allows('view-admin-dashboard')) {
// المستخدم يمكنه عرض لوحة تحكم المسؤول
}
if (Gate::denies('view-admin-dashboard')) {
// المستخدم لا يمكنه عرض لوحة تحكم المسؤول
}
// التحقق مع وسائط إضافية
$post = Post::find(1);
if (Gate::allows('update-post', $post)) {
// المستخدم يمكنه تحديث هذا المنشور
}
// التفويض أو رمي استثناء
Gate::authorize('update-post', $post); // يرمي AuthorizationException إذا تم الرفض
// التحقق من قدرات متعددة (أي)
if (Gate::any(['update-post', 'delete-post'], $post)) {
// المستخدم يمكنه التحديث أو الحذف
}
// التحقق من قدرات متعددة (الكل)
if (Gate::none(['update-post', 'delete-post'], $post)) {
// المستخدم لا يمكنه التحديث ولا الحذف
}
// التحقق للمستخدم الحالي
if (auth()->user()->can('update-post', $post)) {
// نفس Gate::allows()
}
if (auth()->user()->cannot('update-post', $post)) {
// نفس Gate::denies()
}
}
// في المسارات
Route::get('/admin', [AdminController::class, 'index'])
->middleware('can:view-admin-dashboard');
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update-post,post');
// في قوالب Blade
@can('view-admin-dashboard')
<a href="/admin">لوحة تحكم المسؤول</a>
@endcan
@cannot('update-post', $post)
<p>لا يمكنك تحرير هذا المنشور.</p>
@endcannot
@canany(['update-post', 'delete-post'], $post)
<button>تحرير أو حذف</button>
@endcanany
البوابة مقابل البرنامج الوسيط: استخدم Gate::authorize() في المتحكمات للحصول على رسائل خطأ أفضل وتحكم دقيق. استخدم البرنامج الوسيط للحماية على مستوى المسار. كلا النهجين يعملان معاً بسلاسة.
إنشاء السياسات
السياسات هي فئات تنظم منطق التفويض حول نموذج معين. إنها مثالية لسيناريوهات التفويض المعقدة:
// إنشاء سياسة
php artisan make:policy PostPolicy --model=Post
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* تحديد ما إذا كان المستخدم يمكنه عرض أي منشورات.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* تحديد ما إذا كان المستخدم يمكنه عرض المنشور.
*/
public function view(?User $user, Post $post): bool
{
// يمكن لأي شخص عرض المنشورات المنشورة
if ($post->isPublished()) {
return true;
}
// فقط المالك يمكنه عرض المنشورات المسودة
return $user && $user->id === $post->user_id;
}
/**
* تحديد ما إذا كان المستخدم يمكنه إنشاء منشورات.
*/
public function create(User $user): bool
{
// فقط المستخدمون المحققون يمكنهم إنشاء منشورات
return $user->hasVerifiedEmail();
}
/**
* تحديد ما إذا كان المستخدم يمكنه تحديث المنشور.
*/
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
/**
* تحديد ما إذا كان المستخدم يمكنه حذف المنشور.
*/
public function delete(User $user, Post $post): bool
{
// المالكون يمكنهم حذف منشوراتهم
if ($user->id === $post->user_id) {
return true;
}
// المسؤولون يمكنهم حذف أي منشور
return $user->isAdmin();
}
/**
* تحديد ما إذا كان المستخدم يمكنه استعادة المنشور.
*/
public function restore(User $user, Post $post): bool
{
return $user->isAdmin();
}
/**
* تحديد ما إذا كان المستخدم يمكنه حذف المنشور نهائياً.
*/
public function forceDelete(User $user, Post $post): bool
{
return $user->isAdmin();
}
/**
* تحديد ما إذا كان المستخدم يمكنه نشر المنشور.
*/
public function publish(User $user, Post $post): bool
{
// المالك أو المحرر يمكنه النشر
return $user->id === $post->user_id || $user->isEditor();
}
/**
* خطاف Before - يعمل قبل جميع طرق السياسة.
*/
public function before(User $user, string $ability): ?bool
{
// المسؤولون الفائقون يمكنهم فعل كل شيء
if ($user->isSuperAdmin()) {
return true;
}
// أرجع null لمتابعة التفويض العادي
return null;
}
}
تسجيل السياسات
يمكن لـ Laravel اكتشاف السياسات تلقائياً، ولكن يمكنك أيضاً تسجيلها يدوياً:
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use App\Models\Post;
use App\Models\Comment;
use App\Models\User;
use App\Policies\PostPolicy;
use App\Policies\CommentPolicy;
use App\Policies\UserPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* تعيينات السياسة للتطبيق.
*/
protected $policies = [
Post::class => PostPolicy::class,
Comment::class => CommentPolicy::class,
User::class => UserPolicy::class,
];
public function boot(): void
{
// سيتم اكتشاف السياسات تلقائياً أو تسجيلها يدوياً
}
}
// اصطلاحات الاكتشاف التلقائي للسياسة:
// App\Models\Post -> App\Policies\PostPolicy
// App\Models\User -> App\Policies\UserPolicy
// App\Models\BlogPost -> App\Policies\BlogPostPolicy
اكتشاف السياسة: يكتشف Laravel السياسات تلقائياً إذا اتبعت اصطلاحات التسمية. ضع السياسات في app/Policies/ مع اسم النموذج + لاحقة "Policy". التسجيل اليدوي ضروري فقط للمواقع أو الأسماء غير القياسية.
استخدام السياسات
يمكن فحص السياسات بنفس الطرق التي تُفحص بها البوابات، ولكن بصيغة أنظف:
// في المتحكمات
use App\Models\Post;
public function update(Request $request, Post $post)
{
// التفويض باستخدام السياسة
$this->authorize('update', $post);
// إذا تم التفويض، قم بتحديث المنشور
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
public function destroy(Post $post)
{
// التحقق من التفويض
if ($request->user()->cannot('delete', $post)) {
abort(403, 'غير مصرح لك بحذف هذا المنشور.');
}
$post->delete();
return redirect()->route('posts.index');
}
// صيغة بديلة
public function edit(Post $post)
{
// استخدام واجهة Gate
Gate::authorize('update', $post);
return view('posts.edit', compact('post'));
}
// التحقق بدون استثناء
public function show(Post $post)
{
if (auth()->user()->can('view', $post)) {
// عرض المنشور الكامل
} else {
// عرض المعاينة فقط
}
}
// في المسارات - ربط النموذج التلقائي
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::delete('/posts/{post}', [PostController::class, 'destroy'])
->middleware('can:delete,post');
// بدون ربط النموذج
Route::post('/posts', [PostController::class, 'store'])
->middleware('can:create,App\Models\Post');
توجيهات تفويض Blade
استخدم توجيهات Blade لإظهار/إخفاء المحتوى بناءً على التفويض:
<!-- التحقق من قدرة واحدة -->
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">تحرير المنشور</a>
@endcan
@cannot('delete', $post)
<p class="text-muted">لا يمكنك حذف هذا المنشور</p>
@endcannot
<!-- التحقق من قدرات متعددة (أي) -->
@canany(['update', 'delete'], $post)
<div class="post-actions">
@can('update', $post)
<button class="btn-edit">تحرير</button>
@endcan
@can('delete', $post)
<button class="btn-delete">حذف</button>
@endcan
</div>
@endcanany
<!-- التحقق من القدرة بدون مثيل النموذج -->
@can('create', App\Models\Post::class)
<a href="{{ route('posts.create') }}">إنشاء منشور جديد</a>
@endcan
<!-- جمل Else -->
@can('update', $post)
<button>تحرير</button>
@else
<span>للقراءة فقط</span>
@endcan
<!-- التحقق مما إذا كان المستخدم مصادقاً عليه -->
@auth
<p>مرحباً، {{ auth()->user()->name }}!</p>
@endauth
@guest
<a href="{{ route('login') }}">تسجيل الدخول</a>
@endguest
طرق السياسة بدون نماذج
بعض طرق السياسة لا تتطلب مثيل نموذج (مثل "create"):
// في السياسة
public function create(User $user): bool
{
// تحقق مما إذا كان المستخدم يمكنه إنشاء منشورات
return $user->hasVerifiedEmail() && !$user->isBanned();
}
// في المتحكم
public function create()
{
// مرر اسم الفئة
$this->authorize('create', Post::class);
return view('posts.create');
}
// في Blade
@can('create', App\Models\Post::class)
<a href="{{ route('posts.create') }}">إنشاء منشور</a>
@endcan
// في المسارات
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
استجابات التفويض
قدم ملاحظات مفصلة عندما يفشل التفويض:
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function update(User $user, Post $post): Response
{
if ($user->id === $post->user_id) {
return Response::allow();
}
if ($post->isLocked()) {
return Response::deny('هذا المنشور مقفل ولا يمكن تحريره.');
}
return Response::deny('أنت لا تملك هذا المنشور.');
}
public function delete(User $user, Post $post): Response
{
if ($user->isBanned()) {
return Response::denyWithStatus(403, 'حسابك محظور.');
}
if ($post->hasComments()) {
return Response::denyAsNotFound(); // أرجع 404 بدلاً من 403
}
return $user->id === $post->user_id
? Response::allow()
: Response::deny('أنت لا تملك هذا المنشور.');
}
}
// معالجة استثناءات التفويض
// app/Exceptions/Handler.php
use Illuminate\Auth\Access\AuthorizationException;
public function render($request, Throwable $exception)
{
if ($exception instanceof AuthorizationException) {
return response()->json([
'message' => $exception->getMessage() ?: 'إجراء غير مصرح به.',
], 403);
}
return parent::render($request, $exception);
}
رسائل مخصصة: استخدم Response::deny() مع رسائل مخصصة لتوفير تجربة مستخدم أفضل. سيرى المستخدمون السبب الدقيق لعدم تفويضهم، بدلاً من خطأ "403 Forbidden" عام.
تقنيات السياسة المتقدمة
// التحقق من القدرات في النماذج
class Post extends Model
{
public function canBeEditedBy(User $user): bool
{
return Gate::forUser($user)->allows('update', $this);
}
public function canBeDeletedBy(User $user): bool
{
return Gate::forUser($user)->allows('delete', $this);
}
}
// الاستخدام في الكود
if ($post->canBeEditedBy(auth()->user())) {
// تحرير المنشور
}
// مرشحات السياسة (تجاوز جميع الفحوصات)
class PostPolicy
{
public function before(User $user, string $ability): ?bool
{
// المسؤولون الفائقون يتجاوزون جميع الفحوصات
if ($user->isSuperAdmin()) {
return true;
}
// المستخدمون المحظورون يفشلون في جميع الفحوصات
if ($user->isBanned()) {
return false;
}
return null; // متابعة طرق السياسة العادية
}
}
// التفويض لإجراءات متعددة دفعة واحدة
public function bulkDelete(Request $request)
{
$posts = Post::findMany($request->post_ids);
// التفويض للجميع دفعة واحدة
foreach ($posts as $post) {
$this->authorize('delete', $post);
}
Post::destroy($posts->pluck('id'));
}
// سياسات المستخدم الضيف (User nullable)
public function view(?User $user, Post $post): bool
{
if ($post->isPublished()) {
return true; // يمكن للجميع عرض المنشورات المنشورة
}
return $user && $user->id === $post->user_id; // فقط المالك يمكنه عرض المسودات
}
التمرين 1: إنشاء سياسة تعليق
بناء CommentPolicy شاملة بالقواعد التالية:
- يمكن لأي شخص عرض التعليقات
- يمكن للمستخدمين المصادق عليهم إنشاء تعليقات
- يمكن لمؤلفي التعليقات تحديث تعليقاتهم الخاصة في غضون 15 دقيقة
- يمكن لمؤلفي التعليقات حذف تعليقاتهم الخاصة
- يمكن لمؤلفي المنشورات حذف أي تعليق على منشوراتهم
- يمكن للمسؤولين فعل كل شيء
- أرجع رسائل خطأ مخصصة لكل رفض
التمرين 2: تنفيذ التفويض القائم على الأدوار
أنشئ نظام تفويض قائم على البوابة لمدونة:
- عرّف الأدوار: ضيف، مستخدم، مؤلف، محرر، مسؤول
- أنشئ البوابات: view-dashboard, create-post, publish-post, edit-any-post, delete-any-post
- نفذ خطافات before/after للتسجيل
- أضف البرنامج الوسيط لحماية مسارات المسؤول
- أنشئ مكونات Blade تظهر/تخفي بناءً على الأدوار
التمرين 3: سياسة متقدمة مع استجابات مخصصة
أنشئ CoursePolicy لمنصة تعلم عبر الإنترنت:
- يمكن للطلاب عرض الدورات المسجلين فيها
- يمكن للطلاب التسجيل إذا كان لديهم أرصدة متاحة
- يمكن للمدرسين تحديث دوراتهم الخاصة
- أرجع رسائل مخصصة: "أرصدة غير كافية"، "الدورة ممتلئة"، "غير مسجل"
- استخدم Response::denyAsNotFound() للدورات الحساسة
- نفذ طريقة before() للحسابات الموقوفة
الخلاصة
في هذا الدرس، أتقنت نظام التفويض في Laravel باستخدام البوابات والسياسات. تعلمت كيفية تعريف البوابات البسيطة للفحوصات السريعة، وإنشاء سياسات شاملة للتفويض القائم على النموذج، وتسجيل واكتشاف السياسات تلقائياً، واستخدام التفويض في المتحكمات والمسارات وعروض Blade، وتوفير استجابات تفويض مفصلة. تمكنك هذه الأدوات من بناء تطبيقات آمنة مع تحكم دقيق في الوصول.
في الدرس التالي، سنستكشف ميزات الطابور المتقدمة بما في ذلك تجميع الوظائف، والتسلسل، ومعالجة الإخفاقات.