موضع نص الإشعار اتصل بنا تفضل الآن!

ليه توقفت عن استخدام Middleware للصلاحيات بعد ما جربت Laravel Policies

George Bahgat

ليه توقفت عن استخدام Middleware للصلاحيات بعد ما جربت Laravel Policies

في رحلتي مع تطوير تطبيقات الويب باستخدام إطار عمل Laravel، مررت بعدة مراحل في التعامل مع نظام الصلاحيات والأذونات. في البداية، كنت أستخدم Middleware كأداة رئيسية للتحكم في من يستطيع الوصول إلى أي جزء من التطبيق. بدت الفكرة منطقية في البداية: نضع middleware على route معين، ونتحقق من دور المستخدم أو صلاحيته، وإذا لم يكن مؤهلاً، نعيده إلى الصفحة الرئيسية أو نعرض له رسالة خطأ. لكن مع تطور المشروع وازدياد تعقيداته، بدأت أشعر أن هذا الأسلوب يفتقر إلى المرونة والدقة التي أحتاجها. وصلت إلى نقطة شعرت فيها أنني أُعيد اختراع العجلة، وأُثقل الكود بمنطق لا ينتمي لمكانه الحقيقي.

التحول الحقيقي حدث حين جربت Laravel Policies. في البداية، اعتبرتها مجرد خيار إضافي، لكن سرعان ما اكتشفت أنها ليست مجرد بديل، بل هي الحل الأمثل لتنظيم منطق الصلاحيات بطريقة نظيفة، قابلة للاختبار، ومترابطة مع نموذج البيانات نفسه. في هذا المقال، سأشرح بالتفصيل لماذا توقفت عن استخدام Middleware للصلاحيات، وكيف غيرت Laravel Policies طريقة تفكيري في بناء أنظمة أمان مرنة وقابلة للتوسع. سنغطي كل شيء: من المشاكل العملية التي واجهتني، إلى الأمثلة البرمجية الكاملة، وصولاً إلى أفضل الممارسات التي تعلمتها من التجربة.

ملاحظة: إذا كنت تعمل على مشروع Laravel يتطلب نظام صلاحيات متقدم (مثل منصات إدارة محتوى، أنظمة ERP، أو تطبيقات SaaS متعددة المستخدمين)، ففهم الفرق بين Middleware وPolicies ليس خياراً تقنياً فقط، بل هو قرار معماري يؤثر على صيانة الكود على المدى الطويل.

المشكلة الحقيقية مع Middleware في إدارة الصلاحيات

عندما بدأت استخدام Middleware للصلاحيات، كنت أتبع نمطاً بسيطاً: أنشئ middleware باسم مثل CheckRole أو CheckPermission، ثم أستخدمه في routes كالتالي:

Route::get('/admin/users', [UserController::class, 'index'])
    ->middleware('role:admin');

في البداية، هذا النمط كان كافياً. لكن مع تطور المشروع، بدأت الأمور تتعقد. مثلاً، ماذا لو أردت السماح للمستخدم بتعديل منشوره الخاص فقط، وليس جميع المنشورات؟ أو ماذا لو كان هناك أكثر من شرط: "المستخدم يجب أن يكون مالك المنشور أو مشرفاً أو محرراً"؟ هنا بدأت middleware تتحول إلى كوابيس صيانة.

في أحد المشاريع، وجدت نفسي أكتب middleware معقدة تحتوي على عشرات الشروط المنطقية داخل دالة handle، وأحياناً أستدعي نماذج (Models) مباشرة من داخل middleware، مما يخلّ بمبدأ الفصل بين الطبقات (Separation of Concerns). أصبح الكود غير قابل للاختبار بسهولة، لأن middleware يعتمد على حالة HTTP الحالية (مثل معرف المنشور من الـ route parameter)، ولا يمكن فصل منطق الصلاحية لاختباره بشكل منفصل.

بالإضافة إلى ذلك، كان هناك تكرار كبير. مثلاً، نفس منطق "هل المستخدم مالك هذا المنشور؟" كان يُكتب في middleware، وفي الـ controller، وأحياناً حتى في الـ view. هذا التكرار لم يكن فقط مضيعة للوقت، بل كان مصدر أخطاء أمنية محتملة. لو نسيت تحديث أحد الأماكن عند تغيير شرط الصلاحية، قد يصبح جزء من التطبيق عرضة للاختراق.

لماذا Middleware ليست المكان المناسب لمنطق الصلاحيات؟

Middleware في Laravel مصممة أساساً لتصفية طلبات HTTP قبل وصولها إلى الـ controller. وظيفتها مناسبة للتحقق من أشياء عامة مثل: هل المستخدم مسجل دخوله؟ هل لديه دور معين؟ هل الطلب يأتي من مصدر موثوق؟ لكنها ليست مصممة لاتخاذ قرارات تعتمد على علاقة بين المستخدم ونموذج معين.

مثلاً، التحقق مما إذا كان المستخدم يستطيع حذف تعليق معين يتطلب معرفة:

  • هل المستخدم هو صاحب التعليق؟
  • هل المستخدم مشرف؟
  • هل التعليق مرتبط بمنشور لا يزال نشطاً؟

كل هذه الأسئلة تعتمد على حالة الكيان (Entity) المحدد، وليس على حالة المستخدم العامة. وضع هذا المنطق في middleware يعني أنك تخلط بين مسؤوليات الطبقات: middleware يصبح مسؤولاً عن منطق أعمال (Business Logic) لا علاقة له بتصفية الطلبات.

تجربتي العملية: في مشروع لإدارة المهام، استخدمت middleware للتحقق من صلاحية تعديل مهمة. لكن عندما أردت إضافة شرط جديد: "لا يمكن تعديل المهمة إذا كانت مغلقة"، اضطررت لتعديل middleware وإضافة استعلام جديد للـ database. هذا جعل middleware بطيئاً وغير فعال، لأن كل طلب يمر عليه ينفذ استعلاماً حتى لو لم يكن المستخدم مؤهلاً أصلاً.

اكتشاف Laravel Policies: بداية التغيير

في لحظة يأس من تعقيد middleware، قرأت عن Laravel Policies في الوثائق الرسمية. في البداية، بدت لي مجرد طبقة إضافية بدون فائدة حقيقية. لكن عندما جربتها في مشروع صغير، فهمت فوراً لماذا يُوصى بها بشدة.

Policies في Laravel هي فئات (Classes) مخصصة لتنظيم منطق الصلاحيات المتعلقة بنموذج معين. كل Policy يرتبط بنموذج (Model)، ويحتوي على دوال تمثل الإجراءات الممكنة: view، create، update، delete، وغيرها. الفكرة بسيطة لكنها قوية: بدلاً من نشر منطق الصلاحيات في أماكن متفرقة، نجمعه في مكان واحد منظم، يرتبط مباشرة بالكيان الذي نتحكم في الوصول إليه.

الأهم من ذلك أن Laravel يوفر طرقاً سلسة لاستخدام Policies في الـ controllers، الـ views، وحتى في الـ routes، مما يجعل الكود أكثر وضوحاً وقابلية للقراءة.

كيف تُنشئ Policy في Laravel؟

إنشاء Policy في Laravel سهل جداً. لنأخذ مثالاً على نموذج اسمه Post. نبدأ بإنشاء Policy خاصة به باستخدام Artisan:

php artisan make:policy PostPolicy --model=Post

هذا الأمر يولّد ملفاً في app/Policies/PostPolicy.php يحتوي على هيكل أساسي:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    public function viewAny(User $user)
    {
        // هل يمكن للمستخدم عرض قائمة المنشورات؟
    }

    public function view(User $user, Post $post)
    {
        // هل يمكن للمستخدم عرض منشور معين؟
    }

    public function create(User $user)
    {
        // هل يمكن للمستخدم إنشاء منشور جديد؟
    }

    public function update(User $user, Post $post)
    {
        // هل يمكن للمستخدم تعديل هذا المنشور؟
    }

    public function delete(User $user, Post $post)
    {
        // هل يمكن للمستخدم حذف هذا المنشور؟
    }
}

الآن، نسجل Policy في AuthServiceProvider:

protected $policies = [
    Post::class => PostPolicy::class,
];

بعد هذا، يمكننا استخدام Policy في أي مكان في التطبيق.

الفرق الجوهري: منطق مركّز مقابل منطق متناثر

الفرق الأهم الذي لاحظته بعد الانتقال إلى Policies هو التركيز. كل Policy يحتوي على كل ما يتعلق بصلاحية نموذج معين، في مكان واحد. هذا يعني أن أي مطور جديد ينضم للفريق يستطيع فهم قواعد الوصول إلى كيان معين بمجرد فتح ملف Policy الخاص به.

لنأخذ مثالاً عملياً. في مشروع سابق، كان لدينا نموذج Invoice (فاتورة)، وشروط صلاحيات معقدة:

  • يمكن للمستخدم عرض الفواتير الخاصة به فقط.
  • يمكن للمحاسبين عرض جميع الفواتير.
  • لا يمكن حذف فاتورة تم دفعها.
  • يمكن للمديرين تعديل أي فاتورة في أي وقت.

مع Middleware، كنت سأكتب middleware منفصلة لكل شرط، أو أدمجها في واحدة معقدة. مع Policies، كل هذا يصبح:

public function view(User $user, Invoice $invoice)
{
    return $user->hasRole('accountant') || 
           $user->hasRole('admin') || 
           $invoice->user_id === $user->id;
}

public function delete(User $user, Invoice $invoice)
{
    if ($user->hasRole('admin')) {
        return true;
    }

    if ($invoice->is_paid) {
        return false; // لا يمكن حذف فاتورة مدفوعة
    }

    return $invoice->user_id === $user->id;
}

المنطق واضح، منظم، وقابل للاختبار. يمكنني كتابة اختبار وحدة (Unit Test) لكل دالة في Policy دون الحاجة لمحاكاة طلب HTTP كامل.

نصيحة: استخدم دوال Policy كـ "Single Source of Truth" لصلاحيات النموذج. لا تكرر نفس المنطق في الـ controller أو الـ view. إذا احتجت لعرض زر "حذف" في الواجهة، استخدم @can('delete', $invoice) مباشرة.

استخدام Policies في الـ Controllers

في الـ controllers، يمكننا استخدام Policies بعدة طرق. الطريقة الأكثر شيوعاً هي استخدام الدالة authorize الموروثة من Controller الأساسي:

public function edit(Post $post)
{
    $this->authorize('update', $post);
    
    return view('posts.edit', compact('post'));
}

public function destroy(Post $post)
{
    $this->authorize('delete', $post);
    
    $post->delete();
    return redirect()->route('posts.index');
}

إذا لم يكن المستخدم مخولاً، سيتم رمي استثناء AuthorizationException تلقائياً، والذي يتعامل معه Laravel بإعادة توجيه المستخدم أو عرض صفحة خطأ 403.

هناك طريقة أخرى أكثر مرونة باستخدام Gate::allows أو Gate::denies إذا أردت التحكم في السلوك يدوياً:

public function update(Request $request, Post $post)
{
    if (Gate::denies('update', $post)) {
        return response()->json(['error' => 'غير مسموح'], 403);
    }

    // تحديث المنشور...
}

لكن في رأيي، استخدام authorize هو الأفضل لأنه يحافظ على نظافة الكود ويتوافق مع مبادئ Laravel.

ماذا عن الـ Resource Controllers؟

حتى مع الـ Resource Controllers، Policies تعمل بسلاسة. يمكنك تمرير Policy مباشرة في تعريف الـ route:

Route::resource('posts', PostController::class)
    ->middleware('auth')
    ->can('viewAny', Post::class);

وهذا يضمن أن المستخدم لا يمكنه حتى الوصول إلى قائمة المنشورات إذا لم يكن مخولاً. لكن احذر: هذا يناسب فقط الصلاحيات العامة (مثل viewAny)، وليس الصلاحيات الخاصة بكائن معين.

استخدام Policies في الواجهات (Views)

من أقوى ميزات Policies أنها تعمل مباشرة في الـ Blade templates. هذا يعني أنك تستطيع إخفاء أو إظهار عناصر الواجهة بناءً على صلاحيات المستخدم دون الحاجة لكتابة منطق معقد في الـ controller.

مثال بسيط:

<div class="post-actions">
    @can('update', $post)
        <a href="{{ route('posts.edit', $post) }}">تعديل</a>
    @endcan

    @can('delete', $post)
        <form action="{{ route('posts.destroy', $post) }}" method="POST">
            @csrf @method('DELETE')
            <button type="submit">حذف</button>
        </form>
    @endcan
</div>

الجميل هنا أن نفس منطق Policy المستخدم في الـ controller يُستخدم في الـ view. لا تكرار، لا تناقض. إذا غيّرت شرط الحذف في Policy، سيتغير السلوك في كل مكان تلقائياً.

حتى في الحلقات (Loops)، Policies فعالة:

@foreach($posts as $post)
    <div class="post">
        <h3>{{ $post->title }}</h3>
        @can('update', $post)
            <span class="badge">يمكنك تعديله</span>
        @endcan
    </div>
@endforeach

قد تتساءل: أليس هذا سيؤدي إلى استعلامات كثيرة (N+1)؟ الجواب: نعم، إذا لم تُحمّل العلاقات المطلوبة مسبقاً. لكن هذا ليس عيباً في Policies، بل في طريقة الاستعلام. الحل هو تحميل العلاقة بين المستخدم والمنشورات في الـ controller:

public function index()
{
    $posts = Post::with('user')->get();
    return view('posts.index', compact('posts'));
}

أو إذا كان المنطق يعتمد على أدوار المستخدم، تأكد من تحميل الأدوار مسبقاً:

$posts = Post::with('user.roles')->get();
تنبيه أداء: عند استخدام Policies في الحلقات، تأكد من تحميل العلاقات المطلوبة (Eager Loading) لتجنب مشكلة N+1. Policies نفسها لا تسبب بطئاً، لكن سوء استخدام العلاقات في النماذج قد يفعل ذلك.

التعامل مع الصلاحيات المعقدة: عندما لا يكفي الدور (Role)

في كثير من المشاريع، يبدأ المطورون بنظام أدوار بسيط: admin، editor، user. لكن مع تطور التطبيق، يصبح هذا النموذج غير كافٍ. مثلاً، في منصة تعليمية، قد يحتاج مدرس أن يتحكم في طلاب فصله فقط، وليس جميع الطلاب. أو في نظام إدارة مشاريع، قد يحتاج عضو الفريق إلى صلاحيات مختلفة حسب المشروع الذي يعمل عليه.

هنا، تظهر قوة Policies. لأن كل Policy يتعامل مع كيان محدد، يمكنك كتابة منطق يعتمد على العلاقات بين الكيانات.

مثال: في نظام إدارة مشاريع، نموذج Task مرتبط بـ Project، وProject مرتبط بفريق من المستخدمين.

public function update(User $user, Task $task)
{
    // هل المستخدم مالك المشروع؟
    if ($task->project->user_id === $user->id) {
        return true;
    }

    // هل المستخدم عضو في فريق المشروع؟
    return $task->project->members->contains($user);
}

هذا النوع من المنطق سيكون كابوساً في middleware، لأنه يتطلب تحميل العلاقات وتحليلها في كل طلب. أما في Policy، فهو طبيعي وواضح.

دمج Policies مع أنظمة الأذونات (Permissions)

إذا كنت تستخدم حزمة مثل spatie/laravel-permission، يمكنك دمجها بسلاسة مع Policies. مثلاً:

public function delete(User $user, Post $post)
{
    // إذا كان المستخدم لديه إذن عام بالحذف
    if ($user->can('delete posts')) {
        return true;
    }

    // أو إذا كان مالك المنشور
    return $post->user_id === $user->id;
}

بهذه الطريقة، تحصل على أفضل ما في العالمين: مرونة الأذونات القائمة على الأدوار، ودقة الصلاحيات القائمة على الكيانات.

اختبار Policies: سهولة لا تُقدّر بثمن

واحدة من أكبر المكاسب التي لاحظتها بعد الانتقال إلى Policies هي سهولة الاختبار. لأن Policy هو فئة مستقلة، يمكنني كتابة اختبارات وحدة (Unit Tests) لها دون الحاجة لمحاكاة طلب HTTP كامل.

مثال على اختبار Policy:

public function test_admin_can_delete_any_post()
{
    $admin = User::factory()->create();
    $admin->assignRole('admin');

    $post = Post::factory()->create();

    $policy = new PostPolicy();
    $this->assertTrue($policy->delete($admin, $post));
}

public function test_user_can_delete_own_post()
{
    $user = User::factory()->create();
    $post = Post::factory()->create(['user_id' => $user->id]);

    $policy = new PostPolicy();
    $this->assertTrue($policy->delete($user, $post));
}

public function test_user_cannot_delete_others_post()
{
    $user1 = User::factory()->create();
    $user2 = User::factory()->create();
    $post = Post::factory()->create(['user_id' => $user1->id]);

    $policy = new PostPolicy();
    $this->assertFalse($policy->delete($user2, $post));
}

هذه الاختبارات سريعة، واضحة، وتغطي كل سيناريو ممكن. لو كنت أستخدم middleware، لاضطررت لكتابة اختبارات تكامل (Integration Tests) معقدة تتطلب إعداد قاعدة بيانات كاملة ومحاكاة طلبات HTTP.

تجربتي: بعد تحويل نظام الصلاحيات إلى Policies، انخفض وقت تشغيل اختباراتي بنسبة 40%، لأنني استبدلت عشرات اختبارات التكامل باختبارات وحدة أسرع بكثير.

حالات نادرة: متى قد تظل بحاجة لـ Middleware؟

رغم كل مزايا Policies، هناك حالات نادرة قد تحتاج فيها لاستخدام Middleware جنباً إلى جنب معها. مثلاً:

  • التحقق من صلاحية عامة قبل حتى تحميل الكيان: إذا كنت تريد منع المستخدم من الوصول إلى أي صفحة في لوحة التحكم إلا إذا كان لديه دور معين، فMiddleware مناسب هنا.
  • التحكم في الوصول على مستوى التطبيق ككل: مثل منع الوصول من دول معينة، أو التحقق من اشتراك نشط.

لكن حتى في هذه الحالات، يجب أن يكون Middleware مسؤولاً فقط عن التحقق العام، وليس عن منطق الصلاحيات الخاص بالكيانات. الفصل بين الاثنين يحافظ على نظافة الكود.

مثال جيد:

// Middleware عام للتحقق من الاشتراك
class CheckSubscription
{
    public function handle($request, Closure $next)
    {
        if (!auth()->user()->hasActiveSubscription()) {
            return redirect()->route('billing');
        }
        return $next($request);
    }
}

// Policy خاص بالمنشورات
class PostPolicy
{
    public function create(User $user)
    {
        // حتى لو كان لديه اشتراك، قد لا يسمح له بإنشاء منشورات
        return $user->canCreatePosts();
    }
}

هنا، كل طبقة تؤدي وظيفتها دون تداخل.

أفضل الممارسات عند استخدام Laravel Policies

بعد تجربة طويلة مع Policies، توصلت إلى مجموعة من الممارسات التي تجعل العمل معها أكثر كفاءة:

1. اسم الدوال يجب أن تعكس الإجراء بدقة

استخدم أسماء واضحة مثل publish، archive، restore بدلاً من أسماء عامة مثل check أو allow. هذا يجعل الكود ذاتي التوثيق.

2. لا تضع منطق أعمال معقد داخل Policy

Policy يجب أن تكون "بوابة" للصلاحية، وليس مكاناً لتنفيذ منطق الأعمال. إذا احتجت لحسابات معقدة، انقلها إلى Service Class أو إلى النموذج نفسه.

// سيء
public function update(User $user, Post $post)
{
    $post->calculateComplexMetric();
    return $post->metric > 100;
}

// جيد
public function update(User $user, Post $post)
{
    return $post->isEligibleForUpdate();
}

3. استخدم الـ Constructor Injection إذا احتجت لخدمات خارجية

إذا احتجت لاستخدام خدمة مثل PaymentService داخل Policy، يمكنك حقنها عبر الـ constructor:

class InvoicePolicy
{
    protected $paymentService;

    public function __construct(PaymentService $paymentService)
    {
        $this->paymentService = $paymentService;
    }

    public function delete(User $user, Invoice $invoice)
    {
        if ($this->paymentService->isPaid($invoice)) {
            return false;
        }
        return $invoice->user_id === $user->id;
    }
}

Laravel يتعامل مع Policies كخدمات، لذا الـ dependency injection يعمل تلقائياً.

4. لا تنسَ صلاحيات "viewAny" و"create"

الكثير من المطورين يركزون على update وdelete، لكن viewAny وcreate مهمان بنفس القدر. viewAny يحدد من يستطيع رؤية القائمة، وcreate يحدد من يستطيع إنشاء كيان جديد.

الخلاصة: لماذا Policies هي الخيار الأذكى

بعد كل هذه التجارب، أصبحت مقتنعاً أن استخدام Middleware للصلاحيات الخاصة بالكيانات هو قرار معماري خاطئ على المدى الطويل. Policies ليست مجرد ميزة في Laravel، بل هي تجسيد لمبدأ مهم في هندسة البرمجيات: اجعل المنطق قريباً من البيانات التي يتحكم فيها.

الانتقال إلى Policies جعل كودي:

  • أنظف: لا تكرار، لا منطق متناثر.
  • أكثر أماناً: مصدر واحد للحقيقة في الصلاحيات.
  • أسهل في الصيانة: تغيير شرط صلاحية يتم في مكان واحد.
  • أفضل في الاختبار: اختبارات وحدة سريعة ودقيقة.

بالنسبة لي، القرار لم يكن صعباً. بمجرد أن جربت Policies، لم أنظر إلى الوراء أبداً.

خطوتك التالية: إذا كان مشروعك الحالي يستخدم Middleware للصلاحيات الخاصة بالكيانات، جرب تحويل نموذج واحد إلى Policy. ابدأ بمنشورات أو تعليقات، وسترى الفرق بنفسك. لا تحتاج لتحويل المشروع كله دفعة واحدة، بل يمكنك فعل ذلك تدريجياً مع تطوير ميزات جديدة.

في النهاية، الأدوات لا تصنع المطور، بل الطريقة التي يستخدم بها الأدوات. Laravel قدم لنا Policies كهدية، فلماذا لا نستفيد منها؟

إرسال تعليق

الموافقة على ملفات تعريف الارتباط
”نحن نقدم ملفات تعريف الارتباط على هذا الموقع لتحليل حركة المرور وتذكر تفضيلاتك وتحسين تجربتك.“
لا يتوفر اتصال بالإنترنت!
”يبدو أن هناك خطأ ما في اتصالك بالإنترنت ، يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.“
تم الكشف عن مانع الإعلانات!
”لقد اكتشفنا أنك تستخدم مكونًا إضافيًا لحظر الإعلانات في متصفحك.
تُستخدم العائدات التي نحققها من الإعلانات لإدارة موقع الويب هذا ، ونطلب منك إدراج موقعنا في القائمة البيضاء في المكون الإضافي لحظر الإعلانات.“
Site is Blocked
Sorry! This site is not available in your country.