Mendefinisikan Panel
Panel adalah satu antarmuka admin: sebuah prefix URL, middleware stack, kumpulan resource, page, dan widget, serta shell yang merender semuanya. Anda mendefinisikannya dengan membuat subclass PandaPanel\Core\PanelProvider lalu mendaftarkannya di config/panda-panel.php. Hal lain — route, navigation, dan Inertia props — diturunkan dari satu object tersebut.
Panel minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->discoverResources(app_path('Panels/Admin/Resources'))
->discoverPages(app_path('Panels/Admin/Pages'))
->discoverWidgets(app_path('Panels/Admin/Widgets'));
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// config/panda-panel.php
'panels' => [
App\Panels\Admin\AdminPanelProvider::class,
],2
3
4
5
Konfigurasi tersebut sudah menghasilkan panel yang berfungsi pada /admin, lengkap dengan dashboard, tiga account settings page, serta seluruh class yang berada di tiga discovery path. php artisan make:panel Admin menulis file yang sama dan membuat directory-nya.
Provider
PanelProvider memiliki tiga method dan biasanya hanya satu yang perlu Anda override.
| Method | Signature | Fungsinya |
|---|---|---|
panel() | abstract public function panel(Panel $panel): Panel | Mengonfigurasi panel. Harus mengembalikan panel tersebut. |
panelId() | public function panelId(): string | Menghasilkan id dari nama class. |
build() | public function build(): Panel | Memanggil panel() menggunakan Panel yang id-nya sudah diisi dari panelId(). |
Id berasal dari basename class setelah suffix PanelProvider dihapus, lalu sisanya diubah menjadi kebab-case: AdminPanelProvider menjadi admin, BackOfficePanelProvider menjadi back-office. Override panelId() untuk mengubahnya, atau panggil ->id() di dalam panel() — lihat Panel IDs, Paths, and Domains.
panel() berjalan saat provider boot, sebelum request-scoped binding siap digunakan. Jangan me-resolve service, membaca authenticated user, atau membangun URL di dalamnya. Pekerjaan yang membutuhkan hal-hal tersebut harus diletakkan pada bootUsing(), yang berjalan per request:
use Illuminate\Contracts\Auth\Authenticatable;
use PandaPanel\Core\Panel;
$panel->bootUsing(static function (Panel $panel): void {
// Runs on every request into this panel, after the access check passes.
});2
3
4
5
6
Boot callback bersifat akumulatif, dan Panel::boot() menjalankan plugin panel terlebih dahulu agar callback milik aplikasi masih dapat mengubah atau membatalkan konfigurasi yang dibuat plugin.
Registrasi bersifat eksplisit
Panel didaftarkan secara manual di config/panda-panel.php. Class di dalam panel dapat ditemukan melalui discovery; panel-nya sendiri tidak. Ada dua alasan. Pertama, daftar tersebut merupakan keseluruhan panel yang dimiliki aplikasi — termasuk urutan kandidat panel tujuan user yang sudah login ketika request tidak menyebut panel, karena firstAccessibleTo() berjalan berdasarkan kumpulan panel tersebut. Kedua, menambah panel harus menjadi perubahan konfigurasi yang disengaja, bukan side effect karena sebuah file kebetulan muncul di filesystem.
Class name pada daftar tersebut yang tidak dapat di-resolve akan dilewati daripada menyebabkan fatal error. Fatal saat boot terjadi terlalu awal, bahkan sebelum route yang bisa menampilkan pesan error tersedia. php artisan panel:cache melaporkan daftar panel yang sama sehingga kesalahan registrasi dapat terlihat jelas.
Panel yang terdaftar dua kali dengan id yang sama hanya didaftarkan sekali; tetapi panel kedua yang mengklaim id yang sama, atau path yang sama pada domain yang sama, melempar PandaPanel\Exceptions\PanelRegistrationException.
Untuk mendaftarkan panel tanpa provider — misalnya pada test atau package — gunakan PandaPanel\Core\PanelManager:
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelManager;
app(PanelManager::class)->register(
Panel::make('reports')->path('reports')->settings(false),
);2
3
4
5
6
PanelManager::register() mengisi registry tetapi tidak mendaftarkan route. Untuk route, gunakan app(PandaPanel\Routing\PanelRouteRegistrar::class)->register($panel).
Mendaftarkan class
Discovery path dan explicit list digabungkan, dan keduanya bersifat akumulatif, bukan saling menggantikan.
use App\Panels\Admin\Resources\Users\UserResource;
use App\Panels\Admin\Widgets\UserStats;
use App\Reports\Pages\MonthlyReport;
$panel
->discoverResources(app_path('Panels/Admin/Resources'))
->discoverPages(app_path('Panels/Admin/Pages'), app_path('Reports/Pages'))
->discoverWidgets(app_path('Panels/Admin/Widgets'))
->resources([UserResource::class])
->pages([MonthlyReport::class])
->widgets([UserStats::class]);2
3
4
5
6
7
8
9
10
11
| Method | Signature | Catatan |
|---|---|---|
discoverResources | discoverResources(string ...$paths): self | Variadic, akumulatif, duplikat dihapus. |
discoverPages | discoverPages(string ...$paths): self | Sama. |
discoverWidgets | discoverWidgets(string ...$paths): self | Sama. |
resources | resources(array $resources): self | Class string atau object ResourceConfiguration. |
pages | pages(array $pages): self | Class string. |
widgets | widgets(array $widgets): self | Class string. |
Class yang didaftarkan secara eksplisit dan juga ditemukan discovery tetap hanya muncul sekali karena registry dikey berdasarkan slug dan widget id. Lihat Discovery untuk cara class name diturunkan, dan Per-Panel Configuration untuk ResourceConfiguration.
Contoh panel yang lebih lengkap
Berikut isi examples/app/Panels/Admin/AdminPanelProvider.php, yang benar-benar dijalankan oleh test suite.
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use App\Models\User;
use App\Panels\Admin\Pages\AccountsDashboard;
use Illuminate\Contracts\Auth\Authenticatable;
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Enums\ActionVariant;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
use PandaPanel\Pages\Dashboard;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->name('Administrator')
->brandName((string) config('app.name'))
->icon('shield')
->sidebar(appearance: 'sidebar')
->auth()
->navigationGroups([
'User Management',
'System',
])
->dashboards([
Dashboard::class,
AccountsDashboard::class,
])
->discoverResources(app_path('Panels/Admin/Resources'))
->discoverPages(app_path('Panels/Admin/Pages'))
->discoverWidgets(app_path('Panels/Admin/Widgets'))
->configureActions(static function (Action $action): void {
if ($action->getVariant() === ActionVariant::Destructive) {
$action->requiresConfirmation();
}
})
->canAccess(static fn (?Authenticatable $user): bool => $user instanceof User && $user->is_admin);
}
}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
37
38
39
40
41
42
43
44
45
Behavior switch
$panel
->databaseTransactions() // on by default
->strictAuthorization() // off by default
->unsavedChangesAlerts() // on by default
->settings() // on by default
->broadcasting() // on by default
->notifications() // on by default
->darkMode(); // on by default2
3
4
5
6
7
8
| Method | Signature | Default | Efek |
|---|---|---|---|
databaseTransactions | databaseTransactions(bool $databaseTransactions = true): self | true | Membungkus create/update resource dan setiap action di dalam transaction. |
strictAuthorization | strictAuthorization(bool $strictAuthorization = true): self | false | Policy yang hilang melempar PanelAuthorizationException daripada sekadar deny. |
unsavedChangesAlerts | unsavedChangesAlerts(bool $unsavedChangesAlerts = true): self | true | Memberi warning sebelum meninggalkan create/edit form yang masih dirty. |
settings | settings(bool $settings = true): self | true | Menambahkan profile, security, dan appearance page. |
broadcasting | broadcasting(bool $broadcasting = true): self | true | Membuat Panel subscribe ke notification channel milik user. |
notifications | notifications(bool $notifications = true): self | true | Menampilkan notification bell. |
darkMode | darkMode(bool $darkMode = true): self | true | Dikirim sebagai panel.darkMode. |
Resolusi transaction berjalan dari konfigurasi paling spesifik ke paling umum: databaseTransaction(bool) milik action, lalu protected static ?bool $hasDatabaseTransactions milik resource page, lalu konfigurasi panel, lalu default aktif. Nilai null pada dua level pertama berarti "tidak menentukan", sehingga page tetap dapat override panel ke arah mana pun. Di luar panel, jawabannya tetap aktif.
configureActions() menerapkan default pada setiap action yang dibangun panel pada saat action tersebut dibuat, sehingga schema yang secara eksplisit menentukan konfigurasinya sendiri tetap menang:
use Closure;
use PandaPanel\Actions\Action;
$panel->configureActions(static function (Action $action): void {
$action->icon('circle-alert');
});
$panel->actionConfigurator(); // the Closure, or null2
3
4
5
6
7
8
Mengakses current panel
panel(); // the Panel for this request, or null outside one
panel('admin'); // an explicit panel; throws PanelRegistrationException if unknown2
Jawaban yang sama tersedia melalui PanelManager:
use PandaPanel\Core\PanelManager;
$manager = app(PanelManager::class);
$manager->currentPanel(); // ?Panel
$manager->hasCurrentPanel(); // bool
$manager->all(); // list<Panel>, sorted by id
$manager->has('admin'); // bool
$manager->get('admin'); // Panel, throws if unknown2
3
4
5
6
7
8
9
Current panel merupakan request-scoped state yang disimpan oleh PandaPanel\Support\PanelContext, lalu dibersihkan pada awal setiap web request oleh ResetPanelContext — lihat Panel Context.
Catatan
panel()pada provider berjalan sekali per boot, bukan per request. Nilai yang dihitung di sana dihitung untuk semua user.- Object panel tetap mutable setelah registrasi.
app(PanelManager::class)->get('admin')->assets(...)pada test atau service provider tetap bekerja, tetapi route registration sudah selesai pada tahap itu. Artinya konfigurasi yang memengaruhi routing seperti path, domain, dan pages sudah terlambat untuk diubah. getPages()mengembalikan built-in settings page terlebih dahulu ketikasettings()aktif. Karena ituPanel::make('x')->getPages()tidak kosong pada fresh panel.- Tidak ada nilai yang tidak dapat diserialisasi dikirim ke frontend.
toSharedArray()adalah keseluruhan contract; discovery path, middleware, transaction, dan boot callback tetap berada di server.