Gate, policy, dan permission sudah terpasang — sekarang soalnya adalah di mana kita memanggilnya. Controller adalah tempat pemeriksaan paling eksplisit karena kode otorisasi berdiri persis di atas aksi yang dijaganya, mudah dibaca, dan mudah diuji. Laravel menyediakan tiga gaya pemanggilan: helper $this->authorize() yang paling ringkas, facade Gate::authorize() yang setara, dan middleware can: yang memindahkan pemeriksaan ke definisi route. Pada chapter ini kita memakai ketiganya untuk mengamankan satu resource yang sama, lalu membandingkan kapan masing-masing terasa paling pas. Semua angka 200 dan 403 yang muncul adalah hasil feature test sungguhan.

Trait AuthorizesRequests #

Sebelum memakai $this->authorize(), ada satu langkah kecil yang sering terlewat. Base Controller bawaan Laravel 13 adalah class kosong — helper otorisasi tidak disertakan secara otomatis. Kita aktifkan lewat trait AuthorizesRequests:

php
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

class PostController extends Controller
{
    use AuthorizesRequests;
}

Trait inilah yang menyediakan $this->authorize() sekaligus $this->authorizeForUser(). Tanpa trait tersebut, PHP melempar error Call to undefined method — pesan yang masih sering membingungkan pemula yang mengikuti tutorial era Laravel lama.

$this->authorize: Otorisasi di Baris Pertama #

Bentuk paling lazim: satu baris di awal method, sebelum aksi apapun berjalan:

php
use App\Models\Post;

public function edit(Post $post): JsonResponse
{
    $this->authorize('update', $post);

    return response()->json(['judul' => $post->judul]);
}

Argumen pertama adalah nama ability — update — yang oleh auto-discovery policy dipetakan ke PostPolicy::update(). Argumen kedua adalah model yang dijaga; route model binding {post} sudah mengambilnya untuk kita. Feature test memperlihatkan dua sisi medannya:

php
$budi = User::factory()->create([
    'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
]);
$post = Post::create([
    'user_id' => $budi->id,
    'judul' => 'Otorisasi di Controller',
    'isi' => 'Isi tulisan Budi.',
]);
$siti = User::factory()->create([
    'name' => 'Siti', 'email' => '[email protected]', 'role' => 'penulis',
]);

$editBudi = $this->actingAs($budi)->get("/posts/{$post->id}/edit");
$editSiti = $this->actingAs($siti)->get("/posts/{$post->id}/edit");

echo 'pemilik  GET  /posts/{post}/edit    status: '.$editBudi->status()."\n";
echo 'siti     GET  /posts/{post}/edit    status: '.$editSiti->status()."\n";

Output yang Dihasilkan:

text
pemilik  GET  /posts/{post}/edit    status: 200
siti     GET  /posts/{post}/edit    status: 403

Saat authorize() gagal, AuthorizationException berhenti di situ — baris return tidak pernah dicapai dan framework yang mengubah exception menjadi respons 403. Letak pemeriksaan di baris pertama juga bukan gaya-gayaan: semua operasi di bawahnya boleh berasumsi bahwa request sudah sah, sehingga logika method tetap ramping tanpa cabang if penjaga di tengah kode.

Gate::authorize: Versi Facade #

Panggilan yang sama tersedia lewat facade, berguna di class yang bukan controller — job, command, listener:

php
use Illuminate\Support\Facades\Gate;

public function update(Request $request, Post $post): JsonResponse
{
    Gate::authorize('update', $post);

    $post->update(['judul' => (string) $request->input('judul', $post->judul)]);

    return response()->json(['status' => 'diperbarui', 'judul' => $post->judul]);
}

Tingkah lakunya identik dengan $this->authorize(), termasuk melempar exception yang sama. Feature test-nya:

php
$ubahBudi = $this->actingAs($budi)
    ->putJson("/posts/{$post->id}", ['judul' => 'Lewat Gate::authorize']);
$ubahSiti = $this->actingAs($siti)
    ->putJson("/posts/{$post->id}", ['judul' => 'Serobot tulisan Budi']);

echo 'pemilik  PUT  /posts/{post}        status: '.$ubahBudi->status()."\n";
echo 'siti     PUT  /posts/{post}        status: '.$ubahSiti->status()."\n";

Output yang Dihasilkan:

text
pemilik  PUT  /posts/{post}        status: 200
siti     PUT  /posts/{post}        status: 403

Gate::allows untuk Branching #

Kedua helper di atas bersifat “tolak atau lanjut”. Bila kita justru butuh dua jalur — menu berbeda, tombol berbeda — Gate::allows() mengembalikan boolean untuk percabangan:

php
use Illuminate\Support\Facades\Gate;

public function dasbor(): JsonResponse
{
    if (Gate::allows('edit-settings')) {
        return response()->json(['menu' => ['Beranda', 'Pengaturan']]);
    }

    return response()->json(['menu' => ['Beranda']]);
}

Output yang Dihasilkan (GET /dasbor untuk dua peran):

text
admin   GET /dasbor body: {"menu":["Beranda","Pengaturan"]}
penulis GET /dasbor body: {"menu":["Beranda"]}

Admin mendapat menu tambahan Pengaturan karena gate edit-settings membaca role-nya; penulis hanya melihat Beranda. Perhatikan bahwa di sini 403 tidak muncul — penolakan bukan tujuan, melainkan penyesuaian tampilan.

Middleware can: Pemeriksaan di Route #

Gaya ketiga memindahkan otorisasi ke definisi route. Middleware can: menerima nama ability plus parameter route yang memuat model:

php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

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

Segment kedua can:update,post berarti “periksa ability update, ambil modelnya dari parameter route post”. Controller-nya menjadi polos tanpa satu baris cek. Hal ini membuat policy tertulis hanya sekali di route list, mudah diaudit dalam sekali baca — siapa pun yang meninjau routes/web.php langsung tahu endpoint mana yang dijaga tanpa membuka isi controller. Sebaliknya, jarak antara deklarasi dan aksi juga jadi kelemahan: pemeriksaan dan kode yang dijaganya hidup di dua berkas berbeda, jadi refactoring method controller perlu diikuti cermat.

php
public function updateJudul(Request $request, Post $post): JsonResponse
{
    $post->update(['judul' => (string) $request->input('judul', $post->judul)]);

    return response()->json(['status' => 'diperbarui', 'judul' => $post->judul]);
}

Output yang Dihasilkan:

text
pemilik  PATCH /posts/{post}/judul status: 200
siti     PATCH /posts/{post}/judul status: 403

Memilih Gaya untuk Setiap Situasi #

PendekatanTempat menulisPerilaku saat gagalPaling cocok untuk
$this->authorize()Baris pertama methodException → 403Otorisasi per-aksi di controller
Gate::authorize()Controller/non-controllerException → 403Job, command, class non-controller
Gate::allows()Di mana sajaBoolean, tanpa exceptionBranching tampilan atau logika
Middleware can:Definisi routeException → 403Resource route seragam

Empat gaya tersebut menutup kebutuhan di sisi server. Namun pengguna tidak berinteraksi dengan controller — mereka melihat tombol dan menu. Chapter berikutnya membawa otorisasi ini ke Blade supaya tampilan ikut jujur, tanpa lupa bahwa tampilan hanyalah lapisan kosmetik.