تفاوت Gate و Policy در Laravel به طور خلاصه این است: Gate برای مجوزهای ساده و سراسری (بر اساس یک نام توانایی) استفاده می‌شود، و Policy برای قوانین سطح مدل با متدهای ساخت‌یافته. اگر مجوز شما به یک مدل خاص وابسته است یا احتمال رشد و پیچیدگی دارد، Policy انتخاب بهتری است؛ اگر قانون سبک، نقش محور یا سراسری است (مثل مشاهده داشبورد ادمین یا یک Feature toggle)، Gate کفایت می‌کند.

تفاوت Gate و Policy در لاراول چیست؟

Gate مجموعه‌ای از قوانین سبک و مبتنی بر نام توانایی (ability) است که معمولا با کلوزر تعریف می‌شوند؛ مثلا view-admin یا access-report. Policy یک کلاس اختصاصی برای هر مدل دامنه (مثل PostPolicy برای Post) است که متدهای استانداردی مانند view، create، update و delete دارد. نتیجه این تفاوت:

  • اگر سناریو به یک مدل گره خورده است (مالکیت رکورد، وضعیت انتشار، چند-سازمانی)، Policy ساختار تمیزتر و تست‌پذیرتری می‌دهد.
  • اگر سناریو مدل‌محور نیست (نقش‌های کلی، دسترسی به بخش‌های سراسری، قفل موقت یک قابلیت)، Gate سریع‌تر و سرراست‌تر است.

معیار انتخاب سریع

  • قانون به یک مدل وابسته است؟ Policy.
  • CRUD روی یک مدل (view/update/delete و…) دارید؟ Policy.
  • قانون سراسری یا نقش‌محور است (بدون مدل خاص)؟ Gate.
  • می‌خواهید قوانین را سازمان‌دهی و مستند کنید؟ Policy.
  • قانون موقتی/کوچک است یا فقط یک «پرچم ویژگی» دارید؟ Gate.
  • می‌خواهید ادمین ارشد همه چیز را دور بزند؟ Gate::before مناسب است، چه با Gate چه با Policy.

Gate در عمل: تعریف، استفاده و نکات

هدف: تعریف یک دسترسی ساده برای مشاهده پنل ادمین و یک دورزن عمومی برای ادمین ارشد.

<?php

namespace App\Providers;

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', function (User $user): bool {
            return (bool) $user->is_admin;
        });

        // دورزن جهانی؛ اگر true برگردد، سایر قوانین بررسی نمی‌شوند
        Gate::before(function (User $user, string $ability) {
            return $user->is_super_admin ? true : null;
        });
    }
}

استفاده در کنترلر:

<?php

use Illuminate\Support\Facades\Gate;

public function adminDashboard()
{
    Gate::authorize('view-admin'); // در صورت عدم مجوز، 403 پرتاب می‌کند
    // ...
}

استفاده در Blade:

@can('view-admin')
    <!-- محتوای مخصوص ادمین -->
@endcan

نکته‌ها:

  • برای بررسی دستی می‌توانید از Gate::allows و Gate::denies استفاده کنید.
  • Gate::before و Gate::after برای قوانین فراگیر مفیدند؛ اما مراقب باشید ناخواسته همه چیز را باز نکنید.

Policy در عمل: ساخت، ثبت و فراخوانی

هدف: فقط نویسنده یا ادمین بتواند یک پست را ویرایش کند؛ نمایش پست عمومی برای همه آزاد باشد.

# ساخت Policy (نمونه دستور Artisan)
php artisan make:policy PostPolicy --model=Post

نمونه کلاس Policy:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    // مهم: اگر می‌خواهید مهمان هم بررسی شود، از ?User استفاده کنید
    public function view(?User $user, Post $post): bool
    {
        return $post->is_public || ($user && $user->id === $post->user_id);
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || (bool) $user->is_admin;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

ثبت Policy (اگر کشف خودکار فعال نیست):

<?php

namespace App\Providers;

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    public function boot(): void
    {
        // ...
    }
}

استفاده در کنترلر:

<?php

public function update(Post $post)
{
    $this->authorize('update', $post); // 403 در صورت عدم مجوز
    // ...
}

استفاده در Route Middleware:

<?php

use Illuminate\Support\Facades\Route;

Route::patch('/posts/{post}', [PostController::class, 'update'])
     ->middleware('can:update,post');

استفاده در Blade:

@can('update', $post)
    <button>ویرایش</button>
@endcan

مقایسه بر اساس ابعاد کاربردی

معیارGatePolicy
دامنهسراسری/ویژگی‌محورمدل‌محور (Post, Order, …)
ساختارکلوزرهای پراکنده با نام تواناییکلاس‌های منسجم با متدهای استاندارد
مقیاس‌پذیریبرای قوانین کم و سادهبرای قوانین زیاد و در حال رشد
استفاده در Middlewarecan:abilitycan:ability,model
تست‌پذیریخوب، ولی پراکندگی بیشتربسیار خوب، منطبق با مدل
دورزن جهانیGate::before/afterقابل استفاده همراه با Policy

سناریوهای واقعی و انتخاب مناسب

  • نمایش منوی ادمین یا ورود به داشبورد: Gate با توانایی view-admin.
  • ویرایش پست توسط صاحب پست یا ادمین: Policy روی مدل Post با متد update.
  • قفل‌کردن یک قابلیت در بازه زمانی نگهداری: Gate (مثلا feature-enabled).
  • سیستم چندسازمانی (tenant) که هر کاربر فقط داده‌های سازمان خودش را ببیند: Policy روی همه مدل‌های مرتبط تا منطق به هر مدل نزدیک بماند.

اشتباهات رایج و نکات امنیتی

  • ثبت‌نکردن Policy در AuthServiceProvider (وقتی کشف خودکار فعال نیست) که باعث بی‌اثر شدن آن می‌شود.
  • ناهماهنگی نام توانایی‌ها: در Policy از نام‌های استاندارد (view، update…) استفاده کنید تا با authorize و can سازگار بماند.
  • اتکا به فرانت‌اند: دکمه را در Blade پنهان می‌کنید ولی بررسی سمت سرور در کنترلر را فراموش می‌کنید. همیشه در سرور هم authorize کنید.
  • استفاده نادرست از Gate::before: اگر بی‌محابا true برگردانید، عملا همه چیز باز می‌شود. فقط برای نقش‌های مشخص و مطمئن استفاده کنید.
  • عدم توجه به کاربر مهمان: اگر دسترسی برای مهمان هم باید ارزیابی شود، امضای متد Policy را ?User بگذارید و null را مدیریت کنید.
  • اجرا پس از بازیابی داده حساس: در عملیات مهم، قبل از کار با داده، authorize را صدا بزنید تا از افشای ناخواسته جلوگیری شود.
  • نادیده‌گرفتن پاسخ‌های قابل توضیح: اگر نیاز به پیام خطای دقیق دارید، به جای bool از \Illuminate\Auth\Access\Response در Policy برگردانید.

روش بررسی نتیجه و تست

دو مسیر برای اطمینان از صحت پیاده‌سازی وجود دارد: تست رفتار برنامه و تست مستقیم مجوز.

تست رفتار با HTTP تست‌ها

<?php

public function test_non_owner_cannot_update_post(): void
{
    $user = \App\Models\User::factory()->create();
    $post = \App\Models\Post::factory()->create(); // owner متفاوت است

    $this->actingAs($user)
         ->patch(route('posts.update', $post), ['title' => 'X'])
         ->assertForbidden();
}

public function test_owner_can_update_post(): void
{
    $owner = \App\Models\User::factory()->create();
    $post = \App\Models\Post::factory()->for($owner)->create();

    $this->actingAs($owner)
         ->patch(route('posts.update', $post), ['title' => 'New'])
         ->assertOk();
}

تست مستقیم Gate/Policy

<?php

use Illuminate\Support\Facades\Gate;

public function test_gate_view_admin_is_blocked_for_regular_user(): void
{
    $user = \App\Models\User::factory()->create(['is_admin' => false]);

    $this->assertFalse(Gate::forUser($user)->allows('view-admin'));
}

کدام را کجا استفاده کنیم؟ نتیجه نهایی

برای هر چیزی که به یک مدل مربوط است، با Policy شروع کنید؛ نام‌گذاری استاندارد و ساختار کلاس‌محور، نگهداری و تست را ساده می‌کند. برای قوانین سراسری، نقش‌های کلی و پرچم‌های ویژگی، Gate انتخاب بهینه است. در پروژه‌های واقعی ترکیبی از هر دو لازم می‌شود: نقش‌های پایه با Gate، و مجوزهای جزئی CRUD با Policy. مستند کنید که هر توانایی کجا تعریف شده و قبل از هر اقدام حساس، authorize را فراخوانی کنید.