Otorisasi di controller menghentikan request yang melanggar, tetapi halaman yang ditampilkan tetap memuat semua tombol — termasuk tombol yang akan berujung 403. Pengguna yang mengklik tombol Edit lalu ditolak akan merasa menghadapi pintu terkunci tanpa penanda. Blade menutup celah pengalaman ini dengan directive @can dan kerabatnya: elemen tampilan bisa muncul atau hilang mengikuti hasil pemeriksaan yang sama persis dengan server. Pada chapter ini kita menampilkan halaman detail tulisan untuk pemilik dan pengguna lain, lalu membandingkan HTML nyata yang dirender untuk keduanya — termasuk peringatan penting soal batas keamanan lapisan tampilan ini.

@can dan @cannot #

Directive @can menerima nama ability dan opsional model yang diperiksa — bentuk persis seperti $user->can():

blade
<div class="post">
    <h1>{{ $post->judul }}</h1>
    <p>{{ $post->isi }}</p>

    @can('update', $post)
        <a href="{{ route('posts.edit', $post) }}" class="btn-edit">Edit Tulisan</a>
    @endcan

    @cannot('delete', $post)
        <p class="pesan-readonly">Tulisan ini hanya bisa dibaca.</p>
    @endcannot
</div>

Ketika PostPolicy::update() mengembalikan true, tautan Edit ikut dirender; sebaliknya blok hilang tanpa menyisakan jejak. Directive @cannot adalah kebalikannya — bloknya tampil justru ketika ability ditolak, cocok untuk pesan penjelas seperti contoh di atas. Keduanya dievaluasi pada policy yang sama dengan middleware can:, jadi tidak ada logika ganda yang harus kita jaga. Di balik layar, @can dikompilasi menjadi panggilan Gate::allows() — artinya kecepatan pemeriksaan di view sama dengan di controller, dan hasilnya konsisten karena keduanya membaca aturan yang sama pada satu request.

@canany dan Rantai @elsecanany #

Untuk pemeriksaan multi-ability, @canany lolos bila salah satu ability terpenuhi, dan bisa dirantai dengan @elsecanany:

blade
@canany(['update', 'delete'], $post)
    <span class="badge-pemilik">Pemilik</span>
@elsecanany(['viewAny'], App\Models\Post::class)
    <span class="badge-pembaca">Pembaca</span>
@endcanany

Pengguna yang bisa update atau delete tulisan ini mendapat lencana Pemilik; sisanya yang tetap boleh viewAny mendapat lencana Pembaca. Perhatikan argumen kedua pada rantai else: ketika ability tidak menuntut instance model, kita mengirim nama class App\Models\Post::class — sama seperti memanggil $user->can('viewAny', Post::class). Rantai serupa tersedia untuk directive tunggal berupa @elsecan dan @else, sehingga struktur @can ... @elsecan ... @else ... @endcan juga sah bila kita membutuhkan tiga cabang keputusan.

HTML Nyata untuk Dua Pengguna #

Teori di atas mudah diuji: satu route GET /posts/{post} yang merender view tersebut, dikunjungi pemilik lalu pengguna lain. Feature test menangkap HTML keduanya:

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

echo "=== HTML untuk pemilik ===\n";
echo $this->actingAs($budi)->get("/posts/{$post->id}")->getContent()."\n";
echo "=== HTML untuk pengguna lain ===\n";
echo $this->actingAs($siti)->get("/posts/{$post->id}")->getContent()."\n";

Output yang Dihasilkan:

text
=== HTML untuk pemilik ===
<div class="post">
    <h1>Authorization di Blade</h1>
    <p>Isi tulisan Budi.</p>

            <a href="http://localhost:8000/posts/1/edit" class="btn-edit">Edit Tulisan</a>
    
    
            <span class="badge-pemilik">Pemilik</span>
    </div>

=== HTML untuk pengguna lain ===
<div class="post">
    <h1>Authorization di Blade</h1>
    <p>Isi tulisan Budi.</p>

    
            <p class="pesan-readonly">Tulisan ini hanya bisa dibaca.</p>
    
            <span class="badge-pembaca">Pembaca</span>
    </div>

Bagian paling penting justru yang tidak ada: pada HTML pengguna lain, tautan btn-edit lenyap sama sekali — bukan sekadar disembunyikan dengan CSS. Pengguna seperti Siti juga melihat pesan pesan-readonly menggantikan tombol, dan lencananya berubah menjadi Pembaca. Baris-baris kosong yang tampak pada output adalah bekas blok directive yang tidak dirender — Blade tetap menyisakan baris baru kosong, sesuatu yang perlu diingat bila kita membandingkan HTML secara literal. Test di atas dilengkapi assertion assertSee('btn-edit') dan assertDontSee('btn-edit') supaya regresi tertangkap otomatis.

@can di Dalam Komponen #

Directive yang sama berlaku di dalam anonymous component — misalnya komponen tombol aksi yang menerima $post lewat attribute:

blade
@props(['post'])

<div {{ $attributes }}>
    @can('update', $post)
        <a href="{{ route('posts.edit', $post) }}" class="btn btn-primary">Edit</a>
    @endcan

    @can('delete', $post)
        <button type="button" class="btn btn-danger" onclick="hapus()">Hapus</button>
    @endcan
</div>

Dengan menempatkan @can di dalam komponen <x-tombol-aksi :post="$post" />, setiap pemakaian komponen otomatis jujur tanpa kita menulis ulang pemeriksaan di setiap halaman.

Lapisan tampilan selesai, tapi ada satu medan yang tak menyentuh Blade sama sekali: API. Konsumen API tidak melihat tombol apa pun — mereka melihat pesan JSON 403. Chapter berikutnya menunjukkan bagaimana otorisasi bekerja di sana, termasuk kemampuan token yang membatasi hak bahkan untuk pengguna yang sah.