Kenapa PandaBear
Halaman ini adalah argumennya, bukan tur fiturnya. Isinya: masalah apa yang ingin dipecahkan oleh bentuk framework ini, dan masalah apa yang tidak. Bacalah sebelum mengadopsi paketnya — kalau tidak satu pun masalah di bawah ini adalah masalah Anda, controller CRUD tulisan tangan adalah jawaban yang lebih kecil; dan kalau masalah pertama adalah masalah Anda, sisanya tidak sepenting itu.
Argumen satu barisnya
Aplikasi yang frontend-nya Vue seharusnya tidak perlu menjalankan model komponen kedua hanya untuk punya panel admin.
namespace App\Panels\Admin\Resources\Users;
use App\Models\User;
use App\Panels\Admin\Resources\Users\Pages\ListUsers;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
use PandaPanel\Resources\Resource;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class UserResource extends Resource
{
protected static string $model = User::class;
public static function table(TableSchema $table): TableSchema
{
return $table->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('email')->searchable(),
]);
}
public static function form(FormSchema $schema): FormSchema
{
return $schema->schema([
TextInput::make('name')->required()->maxLength(255),
TextInput::make('email')->email()->required()->maxLength(255),
]);
}
/** @return array<string, class-string> */
public static function pages(): array
{
return ['index' => ListUsers::class];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Itu PHP. Yang merendernya adalah komponen Vue yang bisa Anda buka, baca, dan sunting; dikompilasi oleh build Vite yang sama dengan sisa aplikasi, ditata oleh tema Tailwind yang sama, dan diberi tipe oleh tsconfig yang sama. Tidak ada build kedua, tidak ada model state kedua, dan tidak ada kumpulan komponen kedua yang harus dipelajari.
Satu model komponen
Keputusan yang melahirkan semua hal lainnya: PHP mendeskripsikan, Vue merender, Inertia mengangkut. Skema diserialisasi menjadi skalar dan array. Closure dievaluasi di server, dan hanya hasilnya yang menyeberang.
use App\Panels\Admin\Resources\Users\UserResource;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Tables\Columns\TextColumn;
TextColumn::make('name')
->tooltip(static fn (Model $record): ?string => $record->getAttribute('email'))
->url(static fn (Model $record): ?string => UserResource::url('view', $record));2
3
4
5
6
7
tooltip(), url(), formatUsing(), dan extraAttributes() semuanya menerima closure, dan semuanya dijalankan saat serialisasi. Yang sampai di browser adalah sebuah sel berisi satu string dan satu href. Tidak ada yang bisa dieksekusi menyeberangi kabel — dan itulah sebabnya definisi panel boleh memuat logika aplikasi tanpa logika itu ikut menjadi bagian dari bundel klien.
Otorisasi dijawab di tempat datanya berada
Setiap kemampuan resource di-resolve ke policy Laravel biasa. Tidak ada bagian policy yang perlu tahu bahwa panel itu ada.
Metode Resource | Kemampuan Gate |
|---|---|
canViewAny(): bool | viewAny |
canView(Model $record): bool | view |
canCreate(): bool | create |
canEdit(Model $record): bool | update |
canDelete(Model $record): bool | delete |
canDeleteAny(): bool | deleteAny |
canRestore(Model $record): bool | restore |
canRestoreAny(): bool | restoreAny |
canForceDelete(Model $record): bool | forceDelete |
canForceDeleteAny(): bool | forceDeleteAny |
Menyembunyikan tombol hanyalah kenyamanan. Route, action, page, dan widget masing-masing melakukan otorisasi sendiri, dan setiap pemeriksaan itu ditutupi test yang mengakses URL-nya secara langsung.
Karena itu, policy yang tidak ada berarti menolak. Itu default yang disengaja, tapi sekaligus hal yang bisa menyembunyikan kesalahan — jadi sebuah panel boleh menuntut agar policy-nya benar-benar bisa menjawab:
$panel->strictAuthorization(); // mati secara defaultDengan mode itu, model tanpa policy — atau policy tanpa metode untuk kemampuan yang ditanyakan — memunculkan PandaPanel\Exceptions\PanelAuthorizationException, bukan diam-diam menolak. Policy yang mendefinisikan before() dikecualikan, karena metode itu bisa menjawab semua kemampuan. Setiap pemeriksaan lewat PandaPanel\Support\PolicyGate::allows(), jadi ini satu aturan, bukan sepuluh.
Satu query, di-scope sekali
Resource::query() adalah satu-satunya pintu masuk untuk setiap record yang bisa dijangkau sebuah resource: list, view, edit, update, delete, bulk, pencarian action, dan pencarian global.
use Illuminate\Database\Eloquent\Builder;
public static function query(): Builder
{
return parent::query()->where('team_id', currentTeamId());
}2
3
4
5
6
Timpa sekali, dan record di luar scope itu menjadi 404 di setiap route — bukan sekadar baris yang tersaring di salah satunya. Halaman yang meng-query model secara langsung adalah bug, dan ada test yang menjaganya.
Sambungan yang sama juga membawa tenancy. Sebuah resource menyebutkan relasi yang menuju tenant, lalu scope-nya diterapkan untuknya:
protected static ?string $tenantRelationship = 'team';Di luar tenant, resource ber-scope akan memunculkan exception alih-alih berjalan tanpa scope — karena query tanpa scope mengembalikan record milik semua tenant dan tampak seperti halaman yang berfungsi. Pekerjaan di konsol dan antrean masuk ke tenant secara eksplisit:
use PandaPanel\Tenancy\Tenancy;
Tenancy::for($tenant, fn () => InvoiceResource::query()->count());2
3
URL adalah state tabelnya
Halaman, jumlah baris per halaman, pencarian, sorting, arah, filter, kolom yang tampak, grouping, dan tab aktif — semuanya hidup di query string:
/admin/users?search=ada&sort=name&direction=asc&perPage=25&page=2&filters[verified]=true&tab=adminsTombol back, forward, refresh, dan bookmark bekerja tanpa store di sisi klien, dan satu tiket support bisa membawa tautan ke persis apa yang dilihat seseorang. Setiap nilai dikembalikan melalui skema tabel sebelum sampai ke query builder, jadi kolom sorting yang tidak dikenal, perPage di luar rentang, atau filter yang tidak dikenali akan diabaikan, bukan dieksekusi.
Biayanya adalah satu perjalanan bolak-balik ke server per interaksi. Itu dinyatakan terang-terangan di Batasan dan trade-off, karena itu memang pertukaran yang diambil desain ini — bukan detail kecil.
Panel tidak bocor satu sama lain
Isolasinya bersifat struktural, bukan sekadar konvensi:
- Setiap panel punya registry resource, page, widget, dan navigasinya sendiri.
- Route didaftarkan per panel; resource yang tidak terdaftar di sebuah panel tidak punya route di sana.
Resource::url()melempar exception bila dimintai URL di panel yang tidak mendaftarkannya.- Endpoint action ada di setiap panel dan me-resolve resource yang disebut terhadap registry panel itu, sehingga sesi yang sah di satu panel tidak bisa dipakai mengalamati resource panel lain melaluinya.
UserResource::url(); // /admin/users, di panel saat ini
UserResource::url('edit', $user); // /admin/users/3/edit
UserResource::url('index', null, 'app'); // URL di panel app, atau exception2
3
Kelas yang sama juga bisa berarti berbeda di tiap panel tanpa perlu subclass:
use PandaPanel\Resources\ResourceConfiguration;
$panel->resources([
ResourceConfiguration::for(UserResource::class)
->slug('people')
->pluralLabel('People')
->navigationGroup('Company')
->modifyQueryUsing(fn (Builder $query) => $query->where('is_admin', false)),
]);2
3
4
5
6
7
8
9
Frontend-nya milik Anda
Komponen Vue milik panel di-publish ke resources/js saat instalasi. Komponen itu ada di repositori Anda, ikut di-build, dan boleh disunting. Hal ini dipaksakan oleh registry komponennya — masing-masing adalah import.meta.glob atas pohon berkas aplikasi sendiri, sehingga komponen yang tidak pernah dilihat proses build tidak bisa di-resolve bagaimanapun sebuah request mengejanya — dan sekaligus memang itulah intinya: komponen yang kode sumbernya tidak bisa Anda baca adalah komponen yang tidak bisa Anda debug.
Biayanya, composer update tidak bisa memperbaiki berkas yang kini Anda miliki, dan vendor:publish tidak bisa membedakan berkas yang tertinggal versi dari berkas yang sudah disunting. panel:assets bisa, karena .panel-assets.json mencatat rupa setiap berkas saat dipublikasikan:
php artisan panel:assets # apa yang tertinggal, apa yang Anda ubah, apa yang bentrok
php artisan panel:assets --update # hanya menulis berkas yang belum pernah Anda sentuh2
| Di disk | Di paket | Dilaporkan sebagai | --update |
|---|---|---|---|
| tidak berubah | tidak berubah | mutakhir | — |
| tidak berubah | berubah | tertinggal versi | ditulis |
| berubah | tidak berubah | milik Anda | dibiarkan |
| berubah | berubah | konflik | tidak pernah ditulis |
Eksplisit mengalahkan sihir
Panel didaftarkan di config/panda-panel.php alih-alih ditemukan otomatis, karena daftar panel application semestinya terlihat di satu tempat. Ketika request tidak menyebut panel, PandaBear berjalan berdasarkan id panel, bukan urutan config. Kelas-kelas di dalam panel ditemukan otomatis, karena mendaftar setiap resource satu per satu adalah boilerplate yang layak dihapus.
Setter fluent memakai nama polos (->id(), ->path()); pembacanya berawalan get (getId(), getPath()). PHP tidak bisa melakukan overload, dan accessor gabungan yang mengembalikan string|static justru contoh sihir yang dihindari framework ini.
Test bisa bertanya ke mesin yang sesungguhnya
Paketnya mengirim helper sebagai fungsi global lewat files yang di-autoload composer, jadi suite milik aplikasi tidak perlu import dan tidak perlu base class:
panelTable(UserResource::class)
->search('Grace')
->sort('name', 'asc')
->assertCanSeeRecord($grace)
->assertCanNotSeeRecord($ada)
->assertCount(1);
panelForm(UserResource::class)
->assertFieldIsRequired('name')
->assertDehydratesTo(['name' => 'Ada', 'unknown' => 'x'], ['name' => 'Ada']);
panelRecordActions(UserResource::class)->assertExists('edit');
panelTableActions(UserResource::class)->assertCanNotRun('purgeUnverified');
panelBulkActions(UserResource::class)->call('delete');
panelInfolistActions(UserResource::class)->assertVisible('impersonate', $user);
fakePanelNotifications();
assertPanelNotificationSentTo($user, 'Saved.');2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Semuanya melewati skema, query, dan action yang sama dengan yang dilewati aplikasi. Helper ini adalah cara yang lebih enak untuk bertanya, bukan implementasi kedua dari jawabannya — helper yang menghitung versinya sendiri tentang isi sebuah tabel akan lulus test justru ketika tabelnya rusak.
Kapan sebaiknya tidak memakainya
- Frontend Anda Blade, React, atau Svelte. Setiap layar panel adalah respons Inertia yang dirender oleh SFC Vue. Separuh sisi server-nya diserialisasi menjadi array biasa dan tidak terikat framework, tapi belum ada renderer lain yang ditulis dan belum ada rencana untuk itu.
- Anda hanya butuh satu layar CRUD. Satu controller dan satu view Blade jauh lebih sedikit mesin daripada sebuah panel.
- Anda di Laravel 11 atau lebih lama. Tidak didukung, dan tidak bisa dipasang — lihat Matriks Kompatibilitas.
- Anda ingin tabel yang bekerja di sisi klien. Sorting, filter, dan paginasi memang dirancang di sisi server. TanStack Table hanya dipakai untuk model kolom, visibilitas, dan pemilihan baris.
Catatan
- Mengadopsi paket ini tidak memindahkan autentikasi Anda. Fortify tetap memegang setiap endpoint auth; halaman login milik panel mengirim POST ke route Fortify alih-alih menduplikasi rate limiting, dua faktor, passkey, dan penanganan sesi.
- Dua alamat bawaan starter kit berubah perilaku dan keduanya tetap menjadi alamat:
/dashboardmengarahkan pengguna yang sudah login ke panel pertama yang boleh dimasukinya (home_redirect.enabled => falsemematikannya), dan/settings/*tidak disentuh paket ini. strictAuthorization()layak dinyalakan di lingkungan test meski produksi berjalan tanpanya: metode policy yang lupa ditulis seseorang adalah persis kegagalan yang ingin diberi nama olehnya.
Baca juga
- Ringkasan — apa paket ini dan cara memasangnya
- Ikhtisar fitur — semua yang tersedia, disebut per nama kelas
- Arsitektur sekilas — bagaimana request menjadi layar
- Perbandingan dengan konsep Filament — apa yang dipinjam, apa yang tidak
- Batasan dan trade-off — biaya dari setiap keputusan di atas
- Otorisasi dan Otorisasi Resource
- Konsep Tenancy
- Helper Testing