Gate mengambil satu closure per ability, dan itu terasa lega selama jumlahnya sedikit. Masalah muncul begitu satu model — misalnya Post — menuntut tujuh pertanyaan berbeda: boleh lihat daftar, boleh lihat satu, boleh buat, boleh ubah, boleh hapus, dan seterusnya. Menumpuk tujuh closure di service provider membuat kode sulit dipindai. Laravel menawarkan struktur yang lebih rapi untuk situasi ini: policy, yaitu satu class per model yang method-methodnya menjadi ability. Pada chapter ini kita membuat PostPolicy lewat artisan, mengisinya dengan aturan kepemilikan, lalu membuktikan auto-discovery yang membuatnya langsung aktif tanpa registrasi manual.
Membuat Policy dengan Artisan #
Perintah make:policy dengan flag --model= menghasilkan kerangka lengkap berisi method untuk tiap aksi CRUD:
php artisan make:policy PostPolicy --model=PostOutput yang Dihasilkan:
INFO Policy [app/Policies/PostPolicy.php] created successfully. Berkas app/Policies/PostPolicy.php berikut adalah hasil asli generator — seluruh method masih mengembalikan false, artinya semua aksi masih ditolak sebelum kita mengisinya:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function viewAny(User $user): bool
{
return false;
}
public function view(User $user, Post $post): bool
{
return false;
}
public function create(User $user): bool
{
return false;
}
public function update(User $user, Post $post): bool
{
return false;
}
public function delete(User $user, Post $post): bool
{
return false;
}
}Generator juga menyediakan restore() dan forceDelete() untuk model ber-soft delete; keduanya sengaja dipangkas dari cuplikan agar fokus pada lima method inti. Perhatikan pola parameternya: method seperti view, update, dan delete menerima User $user plus Post $post, sedangkan create hanya menerima $user karena tulisannya belum ada. Pemetaan ini berlaku dua arah — ketika controller memanggil $this->authorize('create') tanpa model, Laravel mencari method bernama create; ketika controller memanggil $this->authorize('update', $post), framework mencocokkan tipe parameter kedua dengan class model untuk memilih policy yang benar. Nama method policy dengan demikian menjadi kontrak API kecil yang harus kita jaga konsisten.
Mengisi Aturan Kepemilikan #
Kita isi policy dengan aturan yang wajar untuk blog: semua pengguna terdaftar boleh membaca dan menulis, tetapi mengubah dan menghapus hanya untuk pemilik:
class PostPolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Post $post): bool
{
return true;
}
public function create(User $user): bool
{
return true;
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('Hanya pemilik tulisan yang dapat menghapus.');
}
}Jantung aturan ini ada di perbandingan $user->id === $post->user_id — kolom user_id di tabel posts menjadi bukti kepemilikan. Method delete memakai gaya Response::allow()/deny() yang setara dengan true/false, tetapi memberi kelebihan satu hal: pesan alasan penolakan yang akan terlihat konsumen API nanti.
Auto-Discovery lewat Konvensi Nama #
Kita tidak mendaftarkan policy di atas secara manual. Laravel mengenali App\Policies\PostPolicy sebagai policy untuk App\Models\Post semata-mata lewat konvensi nama folder dan class. Pembuktiannya bisa dilihat langsung dari Gate:
$policy = Gate::getPolicyFor(Post::class);
echo 'Gate::getPolicyFor(Post::class) => '.get_class($policy)."\n";Output yang Dihasilkan:
Gate::getPolicyFor(Post::class) => App\Policies\PostPolicyJika nama policy menyimpang dari konvensi, kita selalu bisa mendaftarkannya eksplisit di AppServiceProvider:
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
Gate::policy(Post::class, PostPolicy::class);Memanggil Policy dari User dan Controller #
Cara paling ringkas memeriksa policy adalah method can() pada model User, yang meneruskan pemeriksaan ke Gate:
echo 'budi->can("update", $post) : '.var_export($budi->can('update', $post), true)."\n";
echo 'siti->can("update", $post) : '.var_export($siti->can('update', $post), true)."\n";
echo 'siti->can("view", $post) : '.var_export($siti->can('view', $post), true)."\n";Output yang Dihasilkan:
budi->can("update", $post) : true
siti->can("update", $post) : false
siti->can("view", $post) : trueSiti boleh membaca tulisan Budi tetapi tidak boleh mengubahnya — persis isi method view dan update. Bentuk dua argumen ini penting dipahami: ability di argumen pertama, model yang diperiksa di argumen kedua. Untuk ability yang tidak menuntut instance seperti viewAny, kita mengirim nama class Post::class sebagai argumen kedua — pola yang sama akan muncul lagi di Blade dan middleware. Di controller, pemeriksaan yang sama ditulis $this->authorize('update', $post). Dikombinasikan dengan route model binding {post}, framework mengambil Post dari database lalu menyuntikkannya ke policy. Feature test berikut membuktikan alur penuhnya:
public function test_pemilik_bisa_memperbarui_post(): void
{
$budi = User::factory()->create([
'name' => 'Budi', 'email' => '[email protected]', 'role' => 'penulis',
]);
$post = Post::create([
'user_id' => $budi->id,
'judul' => 'Belajar Policy Laravel',
'isi' => 'Isi tulisan pertama Budi.',
]);
$response = $this->actingAs($budi)
->putJson("/posts/{$post->id}", ['judul' => 'Judul Baru Budi']);
echo 'pemilik PUT /posts/{post} status: '.$response->status()."\n";
echo 'body: '.$response->getContent()."\n";
$response->assertOk();
}Output yang Dihasilkan:
pemilik PUT /posts/{post} status: 200
body: {"status":"diperbarui","judul":"Judul Baru Budi"}Dengan policy di tempatnya, pertanyaan berikutnya alami: bagaimana mengelompokkan pengguna ke dalam role dan permission supaya policy tidak dipenuhi $user->role === 'admin' yang di-copy-paste? Chapter berikutnya merancang tabelnya dari nol.