Cek if ($user->id === $post->user_id) di controller, lalu cek yang sama di Blade, lalu cek lagi di form request — kalau ini terasa familiar, kita punya masalah yang sama. Logika otorisasi yang tersebar di mana-mana bukan hanya susah dipelihara, tapi juga rawan ketinggalan satu titik. Laravel Policies hadir untuk menyelesaikan persis masalah ini: satu tempat, satu kebenaran, untuk aturan "siapa boleh melakukan apa" terhadap sebuah model.
Artikel ini membahas Laravel Policies secara menyeluruh — dari setup awal di Laravel 13, penggunaan di berbagai lapisan aplikasi, Response object untuk kontrol penuh, hingga pola yang siap dipakai di aplikasi multi-tenant SaaS.
Policies vs Gates: Kapan Pakai yang Mana
Laravel menyediakan dua mekanisme otorisasi: Gates dan Policies. Gates adalah closure sederhana yang cocok untuk aturan yang tidak terikat model tertentu. Policies adalah class yang mengorganisasi aturan otorisasi di sekitar satu model atau resource.
Analoginya sederhana: kalau Gates seperti route closure, Policies seperti controller — keduanya berfungsi, tapi untuk kompleksitas yang berbeda.
Pakai Policy ketika:
- Aturan terikat pada model spesifik:
Article,Invoice,Team,Project - Ada banyak kemungkinan aksi:
view,create,update,delete,publish - Logic otorisasi perlu konsisten di controller, Blade, API, dan form request
- Aturan cukup kompleks untuk layak ditest secara terpisah
Membuat Policy Pertama
Generate policy dengan Artisan, sertakan flag --model agar scaffold langsung menyesuaikan nama model:
php artisan make:policy ArticlePolicy --model=Article
Laravel akan membuat app/Policies/ArticlePolicy.php dengan method standar yang sudah terhubung ke model Article:
<?php
namespace App\Policies;
use App\Models\Article;
use App\Models\User;
class ArticlePolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Article $article): bool
{
return true;
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail();
}
public function update(User $user, Article $article): bool
{
return $user->id === $article->author_id;
}
public function delete(User $user, Article $article): bool
{
return $user->id === $article->author_id;
}
}
Perhatikan bahwa viewAny dan create tidak menerima instance model — karena aksi tersebut tidak terikat pada resource yang sudah ada.
Auto-Discovery di Laravel 13
Laravel 13 menemukan policy secara otomatis selama mengikuti konvensi penamaan App\Models\Article ↔ App\Policies\ArticlePolicy. Tidak perlu registrasi manual di AuthServiceProvider untuk kebanyakan kasus.
Jika perlu mapping kustom atau policy ada di lokasi non-standar, gunakan Gate::guessPolicyNamesUsing() di AppServiceProvider:
Gate::guessPolicyNamesUsing(function (string $modelClass): string {
return 'App\\Authorization\\' . class_basename($modelClass) . 'Policy';
});
Menggunakan Policy di Berbagai Lapisan
Policy yang sudah dibuat harus digunakan secara konsisten di semua lapisan — controller, route, Blade, dan form request.
Di Controller
Cara paling ekspresif menggunakan $this->authorize():
public function update(ArticleUpdateRequest $request, Article $article): RedirectResponse
{
$this->authorize('update', $article);
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}
Jika user tidak memiliki izin, Laravel otomatis melempar AuthorizationException yang dikonversi ke respons HTTP 403. Alternatifnya pakai Gate facade langsung — keduanya memanggil policy yang sama:
use Illuminate\Support\Facades\Gate;
public function destroy(Article $article): RedirectResponse
{
Gate::authorize('delete', $article);
$article->delete();
return redirect()->route('articles.index');
}
Di Route
Proteksi di level route memastikan otorisasi berjalan sebelum controller dipanggil:
Route::put('/articles/{article}', [ArticleController::class, 'update'])
->middleware('can:update,article');
// atau dengan fluent method
Route::delete('/articles/{article}', [ArticleController::class, 'destroy'])
->can('delete', 'article');
Di Blade
Otorisasi harus tercermin di UI — tidak hanya di backend. Gunakan directive Blade agar tombol dan link yang akan selalu gagal tidak ditampilkan sama sekali:
@can('update', $article)
<a href="{{ route('articles.edit', $article) }}" class="btn-primary">
Edit Artikel
</a>
@endcan
@can('delete', $article)
<form method="POST" action="{{ route('articles.destroy', $article) }}">
@csrf @method('DELETE')
<button type="submit">Hapus</button>
</form>
@endcan
Directive @canany berguna jika ingin menampilkan elemen ketika user memiliki salah satu dari beberapa kemampuan:
@canany(['update', 'delete'], $article)
<div class="action-toolbar">
{{-- tombol aksi --}}
</div>
@endcanany
Di Form Request
Form request adalah tempat yang tepat untuk menyatukan validasi dan otorisasi dalam satu class:
<?php
namespace App\Http\Requests;
use App\Models\Article;
use Illuminate\Foundation\Http\FormRequest;
class ArticleUpdateRequest extends FormRequest
{
public function authorize(): bool
{
/** @var Article $article */
$article = $this->route('article');
return $this->user()->can('update', $article);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'body' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
];
}
}
Dengan pola ini, controller bisa sangat tipis — tidak perlu lagi $this->authorize() secara terpisah.
Response Object untuk Kontrol Penuh
Mengembalikan true atau false dari policy cukup untuk kasus sederhana. Tapi di aplikasi nyata, kita sering butuh pesan error yang spesifik atau HTTP status code yang berbeda. Kelas Illuminate\Auth\Access\Response menyelesaikan ini.
Pesan Error Kustom
use Illuminate\Auth\Access\Response;
public function update(User $user, Article $article): Response
{
if ($user->id === $article->author_id) {
return Response::allow();
}
if ($user->hasRole('editor') && $article->team_id === $user->team_id) {
return Response::allow();
}
return Response::deny('Hanya penulis atau editor tim yang bisa mengubah artikel ini.');
}
Pesan dari Response::deny() bisa diakses di exception handler untuk ditampilkan ke user.
Sembunyikan Resource dengan 404
Untuk resource sensitif, lebih baik mengembalikan 404 daripada 403 — agar keberadaan resource tidak bocor ke user yang tidak berhak:
public function view(User $user, Article $article): Response
{
if ($article->is_published) {
return Response::allow();
}
if ($user->id === $article->author_id) {
return Response::allow();
}
// User tidak tahu apakah artikel ini ada atau tidak
return Response::denyAsNotFound();
}
denyAsNotFound() sangat berguna untuk halaman detail resource privat. User yang tidak berhak mendapat 404 biasa, sehingga tidak ada informasi yang bocor tentang keberadaan resource tersebut.
Logic Domain yang Kompleks
Untuk skenario yang melibatkan peran, kepemilikan, dan relasi, policy bisa mengekspresikan aturan bisnis secara eksplisit:
public function publish(User $user, Article $article): Response
{
if ($article->status === 'published') {
return Response::deny('Artikel sudah dipublikasikan.');
}
if ($user->id === $article->author_id) {
return Response::allow();
}
if ($user->hasRole('editor') && $article->team_id === $user->team_id) {
return Response::allow();
}
return Response::deny('Hanya penulis atau editor tim yang bisa mempublikasikan artikel.');
}
Dibanding if yang tersebar di controller, semua aturan terbaca jelas dalam satu method.
Method before() untuk Super Admin dan Aturan Global
Method before() berjalan sebelum method policy lainnya. Ini tempat yang tepat untuk aturan global seperti super admin yang boleh melakukan segalanya, atau akun yang diblokir:
public function before(User $user, string $ability): ?bool
{
if ($user->isSuspended()) {
return false;
}
if ($user->hasRole('super-admin')) {
return true;
}
return null; // lanjut ke method policy spesifik
}
Mengembalikan null dari before() penting — ini yang membuat Laravel melanjutkan ke method seperti update() atau delete(). Jangan kembalikan false jika hanya ingin membiarkan flow berjalan normal.
Dengan pola ini, method-method lain di policy bisa fokus pada aturan bisnis tanpa perlu selalu mengecek apakah user adalah admin.
Pola Multi-Tenant SaaS
Policy benar-benar bersinar di aplikasi multi-tenant. Bayangkan platform SaaS di mana setiap Organization punya data masing-masing dan user hanya boleh mengakses data di organisasinya:
<?php
namespace App\Policies;
use App\Models\Report;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ReportPolicy
{
public function before(User $user, string $ability): ?bool
{
if ($user->isSuspended()) {
return false;
}
return null;
}
public function view(User $user, Report $report): Response
{
if ($user->organization_id !== $report->organization_id) {
return Response::denyAsNotFound();
}
return Response::allow();
}
public function update(User $user, Report $report): Response
{
if ($user->organization_id !== $report->organization_id) {
return Response::denyAsNotFound();
}
if (!$user->hasPermissionTo('reports.update')) {
return Response::deny('Akun kamu tidak memiliki izin mengubah laporan.');
}
return Response::allow();
}
public function delete(User $user, Report $report): Response
{
if ($user->organization_id !== $report->organization_id) {
return Response::denyAsNotFound();
}
if ($user->hasRole('org-admin')) {
return Response::allow();
}
if ($user->id === $report->created_by) {
return Response::allow();
}
return Response::deny('Hanya admin organisasi atau pembuat laporan yang bisa menghapus.');
}
}
Pola ini bisa direplikasi ke model lain di domain yang sama: Project, Invoice, Team, Contract. Sekali pola tenant-aware terbentuk, strukturnya konsisten di seluruh codebase.
PHP Attributes di Laravel 13
Laravel 13 memperkenalkan PHP attributes #[Authorize] dan #[Middleware] di controller, membuat otorisasi lebih deklaratif dan colocated dengan action yang dilindungi:
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class ArticleController
{
public function index(): \Inertia\Response
{
// semua user terautentikasi bisa lihat daftar
}
#[Authorize('update', Article::class)]
public function update(ArticleUpdateRequest $request, Article $article): \Illuminate\Http\RedirectResponse
{
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}
#[Authorize('delete', Article::class)]
public function destroy(Article $article): \Illuminate\Http\RedirectResponse
{
$article->delete();
return redirect()->route('articles.index');
}
}
Dengan attributes, developer baru bisa memahami lifecycle lengkap sebuah request — middleware dan otorisasi — hanya dengan membaca class controller, tanpa perlu buka file route.
Kesalahan Umum yang Perlu Dihindari
Beberapa pola yang sering membuat policy tidak efektif:
| Kesalahan | Dampak | Solusi |
|---|---|---|
| Logic otorisasi di controller, bukan policy | Duplikasi, susah ditest | Pindahkan ke policy method |
| Cek role saja, abaikan kepemilikan | User beda tenant bisa akses | Selalu gabungkan role + organization_id/user_id |
| Proteksi controller tapi lupa Blade | UI tampilkan aksi yang akan gagal | Pakai @can/@cannot konsisten |
Return false dari before() untuk semua | Admin pun tidak bisa akses | Return null untuk flow normal |
Tidak pakai Response object | Pesan error generik, 403 untuk semua | Gunakan deny(), denyAsNotFound() |
Checklist Implementasi
Sebelum dianggap selesai, pastikan setiap policy memenuhi poin berikut:
- Policy di-generate dengan
php artisan make:policy --model before()menangani super admin dan akun yang diblokir- Setiap method return
Responseobject, bukanboolmentah $this->authorize()dipanggil di controller atauauthorize()di form request- Route sensitif dilindungi dengan
->can()atau middlewarecan: - Directive
@can/@cannotdipakai di Blade untuk semua aksi - Di aplikasi multi-tenant, setiap method mengecek
organization_idatau padanannya
Kesimpulan
Policies mengubah logika otorisasi dari sesuatu yang tersebar dan rawan bug menjadi kode yang terpusat, mudah dibaca, dan siap ditest. Semakin kompleks domain aplikasi — multi-tenant, role bertingkat, resource privat — semakin besar manfaat yang didapat dari policy yang terstruktur. Untuk langkah berikutnya, eksplorasi integrasi policy dengan Spatie Laravel Permission bisa menjadi pelengkap yang kuat untuk manajemen role dan permission yang lebih granular.







