Gate adalah bentuk otorisasi paling ringkas di Laravel: satu closure, satu nama ability, terdaftar sekali, bisa dipakai di mana saja. Ketika aturan akses kita masih sependek “hanya admin yang boleh membuka pengaturan”, mendirikan class khusus jelas berlebihan — cukup satu baris Gate::define di service provider. Pada chapter ini kita mendefinisikan gate sederhana dan gate berparameter model, lalu memanggilnya dari berbagai arah: boolean untuk branching, exception untuk menolak request, middleware untuk route, dan directive @can untuk Blade. Semua contoh dijalankan nyata sehingga kita bisa melihat angka 403 muncul persis di tempat yang diprediksi.

Mendefinisikan Gate di AppServiceProvider #

Kita daftarkan dua gate di method boot() milik AppServiceProvider:

php
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;

Gate::define('edit-settings', function (User $user): bool {
    return $user->role === 'admin';
});

Gate::define('touch-post', function (User $user, Post $post): bool {
    return $post->user_id === $user->id;
});

Kedua closure menerima User $user yang otomatis diisi pengguna yang sedang login. Gate pertama tidak butuh data lain — ia hanya membaca kolom role. Gate kedua menerima parameter tambahan $post, sehingga pertanyaannya berubah menjadi spesifik: “bolehkah pengguna ini menyentuh tulisan ini?”. Nama ability bebas kita pilih, tetapi gaya kebab-case seperti edit-settings adalah konvensi yang umum.

Memeriksa Gate: allows dan denies #

Method Gate::allows() mengembalikan true bila diizinkan, sedangkan Gate::denies() adalah kebalikannya. Karena contoh berikut dijalankan dari CLI tanpa pengguna yang login, kita memakai Gate::forUser($user) untuk mengikat pemeriksaan pada user tertentu:

php
$budi = User::create([
    'name' => 'Budi', 'email' => '[email protected]',
    'password' => 'password-rahasia', 'role' => 'penulis',
]);
$admin = User::create([
    'name' => 'Admin', 'email' => '[email protected]',
    'password' => 'password-rahasia', 'role' => 'admin',
]);
$post = Post::create([
    'user_id' => $budi->id,
    'judul' => 'Belajar Gate Laravel',
    'isi' => 'Isi tulisan tentang gate.',
]);

echo 'budi   edit-settings : allows='
    .var_export(Gate::forUser($budi)->allows('edit-settings'), true)
    .' denies='.var_export(Gate::forUser($budi)->denies('edit-settings'), true)."\n";
echo 'admin  edit-settings : allows='
    .var_export(Gate::forUser($admin)->allows('edit-settings'), true)."\n";
echo 'budi   touch-post    : allows='
    .var_export(Gate::forUser($budi)->allows('touch-post', $post), true)."\n";
echo 'admin  touch-post    : allows='
    .var_export(Gate::forUser($admin)->allows('touch-post', $post), true)."\n";

Output yang Dihasilkan:

text
budi   edit-settings : allows=false denies=true
admin  edit-settings : allows=true
budi   touch-post    : allows=true
admin  touch-post    : allows=false

Hasilnya membentuk salib yang rapi: Budi gagal di edit-settings karena rolenya penulis, tetapi lolos di touch-post karena tulisannya memang milik Budi. Admin berlaku sebaliknya. Dua boolean ini biasa dipakai untuk branching tampilan atau logika di controller.

Gate::authorize: Menolak dengan 403 #

Gate::authorize() mengambil jalan yang lebih tegas — ketika closure mengembalikan false, method ini melempar AuthorizationException yang oleh framework diterjemahkan menjadi respons 403. Kita buat controller kecil:

php
use Illuminate\Support\Facades\Gate;

class GateController extends Controller
{
    public function paksa(): JsonResponse
    {
        Gate::authorize('edit-settings');

        return response()->json(['status' => 'ok']);
    }
}

Baris Gate::authorize('edit-settings') menjadi gerbang: hanya admin yang mencapai baris return di bawahnya. Feature test berikut membuktikan efeknya pada dua peran:

php
public function test_gate_authorize_melempar_403(): void
{
    $budi = User::factory()->create([
        'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
    ]);

    $response = $this->actingAs($budi)->get('/gate/paksa');

    echo 'penulis GET /gate/paksa status: '.$response->status()."\n";

    $response->assertForbidden();
}

Output yang Dihasilkan:

text
penulis GET /gate/paksa status: 403

Gate::any dan Gate::check #

Terkadang satu keputusan bergantung pada beberapa ability sekaligus. Gate::any() lolos bila salah satu ability terpenuhi, sedangkan Gate::check() menuntut semuanya terpenuhi:

php
$super = User::create([
    'name' => 'Super Admin', 'email' => '[email protected]',
    'password' => 'password-rahasia', 'role' => 'super-admin',
]);
$abilitas = ['edit-settings', 'touch-post'];

echo 'budi   any='.var_export(Gate::forUser($budi)->any($abilitas, $post), true)
    .' check='.var_export(Gate::forUser($budi)->check($abilitas, $post), true)."\n";
echo 'admin  any='.var_export(Gate::forUser($admin)->any($abilitas, $post), true)
    .' check='.var_export(Gate::forUser($admin)->check($abilitas, $post), true)."\n";
echo 'super  any='.var_export(Gate::forUser($super)->any($abilitas, $post), true)
    .' check='.var_export(Gate::forUser($super)->check($abilitas, $post), true)."\n";

Output yang Dihasilkan:

text
budi   any=true check=false
admin  any=true check=false
super  any=true check=true

Budi hanya menguasai touch-post dan admin hanya menguasai edit-settings, jadi any() mengembalikan true untuk keduanya sementara check() mengembalikan false. Hanya pengguna super yang menuntaskan kedua ability — dan itu bukan kebetulan, seperti kita lihat berikutnya.

Interception dengan Gate::before #

Gate::before() mendaftarkan callback yang berjalan sebelum semua pemeriksaan lain. Ini tempat sempurna untuk super admin:

php
Gate::before(function (User $user, string $ability): bool|null {
    return $user->role === 'super-admin' ? true : null;
});

Mengembalikan true berarti lolos apa pun ability-nya; mengembalikan null berarti “tidak berpendapat, lanjutkan pemeriksaan normal”. Itulah sebabnya pengguna super pada output sebelumnya menembus check() tanpa mendefinisikan ability apa pun — closure before sudah memutuskan lebih dulu.

Gate di Middleware dan Blade #

Karena middleware can: dan directive @can sama-sama bertanya ke Gate, satu definisi gate otomatis aktif di kedua tempat. Di route:

php
use Illuminate\Support\Facades\Route;

Route::get('/gate/rahasia', fn () => response()->json(['rahasia' => 'kunci-api-123']))
    ->middleware('can:edit-settings')
    ->name('gate.rahasia');

Feature test membandingkan dua peran pada route yang sama:

php
$budi = User::factory()->create([
    'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
]);
$admin = User::factory()->create([
    'name' => 'Admin', 'email' => '[email protected]', 'role' => 'admin',
]);

echo 'penulis GET /gate/rahasia status: '
    .$this->actingAs($budi)->get('/gate/rahasia')->status()."\n";
echo 'admin   GET /gate/rahasia status: '
    .$this->actingAs($admin)->get('/gate/rahasia')->status()."\n";
echo 'body: '.$this->actingAs($admin)->get('/gate/rahasia')->getContent()."\n";

Output yang Dihasilkan:

text
penulis GET /gate/rahasia status: 403
admin   GET /gate/rahasia status: 200
body: {"rahasia":"kunci-api-123"}

Di Blade, pengecekan yang sama ditulis @can('edit-settings') ... @endcan. Gate terasa ringan untuk satu-dua aturan, tetapi bayangkan dua belas ability yang semuanya berbicara tentang model Post — closure bertumpuk di satu provider mulai sulit dirawat. Untuk wilayah itu Laravel menyediakan class khusus per model, yaitu policy, yang menjadi fokus chapter berikutnya. Rujukan lengkapnya ada di dokumentasi Gates .