Discovery Page
Sebuah Panel menemukan Page dengan melakukan scan directory, bukan menyimpan daftar class secara manual. Arahkan discoverPages() ke sebuah folder, letakkan class Page di dalamnya, dan pada request berikutnya Page tersebut akan memiliki route, muncul di navigation, serta mengikuti authorization. Explicit registration tetap didukung dan digabungkan dengan hasil discovery, sehingga Anda tidak dipaksa memilih hanya salah satu pendekatan.
Halaman ini membahas Page secara khusus. Mekanisme yang sama digunakan untuk menemukan Resource dan Widget — lihat Discovery untuk rule yang berlaku bersama.
Contoh minimal yang berfungsi
<?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
Setiap concrete class di bawah app/Panels/Admin/Pages yang mengimplementasikan PandaPanel\Contracts\PageContract sekarang menjadi Page milik Admin Panel:
namespace App\Panels\Admin\Pages;
use PandaPanel\Pages\Page;
final class Settings extends Page {}2
3
4
5
php artisan route:list --name=panel.admin.pagesMethod
public function discoverPages(string ...$paths): self;
/** @return list<string> */
public function getPageDiscoveryPaths(): array;2
3
4
Method bersifat variadic, dan setiap pemanggilan menambahkan path daripada mengganti daftar sebelumnya. Karena itu plugin dapat menambahkan directory tanpa menghapus directory milik Panel:
$panel
->discoverPages(app_path('Panels/Admin/Pages'))
->discoverPages(base_path('modules/Billing/Pages'));2
3
Path harus berupa absolute filesystem path. Tidak ada directory yang dibuat otomatis: path yang bukan directory dilewati secara silent karena Panel yang memiliki optional module yang belum terpasang tetap harus dapat boot.
Class yang memenuhi syarat
PandaPanel\Discovery\PanelDiscoverer::pages() menelusuri setiap file .php di bawah setiap path secara recursive dan hanya mempertahankan class jika semua kondisi berikut terpenuhi:
| Pemeriksaan | Alasan |
|---|---|
| File dapat di-resolve menjadi class melalui PSR-4 prefix Composer | Membaca source file untuk mengetahui declaration di dalamnya berarti harus melakukan tokenization terhadap arbitrary source |
class_exists() | File yang namespace-nya tidak sesuai lokasi tidak dapat di-autoload |
| Bukan abstract dan bukan interface | PandaPanel\Pages\Page sendiri hidup di package, dan base page buatan aplikasi juga dapat hidup di tree yang sama |
implementsInterface(PageContract::class) | Contract inilah yang dipanggil registrar dan navigation builder |
use PandaPanel\Core\Panel;
use PandaPanel\Discovery\PanelDiscoverer;
$panel = Panel::make('admin')->discoverPages(app_path('Panels/Admin/Pages'));
app(PanelDiscoverer::class)->pages($panel);
// ['App\Panels\Admin\Pages\AccountsDashboard', 'App\Panels\Admin\Pages\Settings']2
3
4
5
6
7
Hasil di-sort() berdasarkan nama class sehingga dua machine dengan urutan filesystem berbeda tetap menghasilkan list yang sama. Duplicate dari beberapa path digabung menjadi satu.
Meng-extend Page adalah cara normal untuk memenuhi contract, tetapi bukan kewajiban. PageContract mendeklarasikan lima static method, dan class yang mengimplementasikannya secara langsung tetap ditemukan:
namespace PandaPanel\Contracts;
interface PageContract
{
public static function slug(): string;
public static function routePath(): string;
public static function canAccess(): bool;
public static function navigationItem(PanelContract $panel): ?NavigationItem;
public static function cluster(): ?string;
}2
3
4
5
6
7
8
9
10
Me-resolve file menjadi class
PandaPanel\Discovery\ClassResolver::forPath() memetakan path menjadi nama class menggunakan PSR-4 prefix yang sudah didaftarkan Composer. Namespace yang paling panjang dicoba lebih dahulu agar nested prefix menang dari parent prefix. Path di luar semua registered root menghasilkan null karena class di lokasi tersebut memang tidak dapat di-autoload.
Konsekuensi praktisnya: Page di directory yang tidak dipetakan Composer tidak akan pernah ditemukan. Jika Page tidak muncul, periksa autoload.psr-4 di composer.json dan jalankan kembali composer dump-autoload sebelum menyimpulkan ada masalah pada Panel.
Menggabungkan dengan explicit registration
public function pages(array $pages): self; // list<class-string>
/** @return list<class-string> */
public function getPages(): array;2
3
4
PanelManager membangun PageRegistry setiap Panel dari explicit list dan discovered list. Class yang muncul di kedua tempat tetap hanya muncul satu kali karena registry menggunakan slug sebagai key.
$panel
->discoverPages(app_path('Panels/Admin/Pages'))
->pages([\App\Support\Reporting\ThroughputPage::class]);2
3
Explicit registration adalah pilihan yang tepat untuk Page yang hidup di luar tree Panel — misalnya shared package, module, atau test fixture.
getPages() juga memasukkan tiga built-in account page kecuali Panel menonaktifkannya:
$panel->settings(false); // drops ProfileSettings, SecuritySettings, AppearanceSettingsPage bawaan tersebut digabung di getPages() dan bukan di manager, sehingga discovery, caching, dan route registration memperlakukannya sama seperti Page lain. Lihat Settings pages.
Dashboard tambahan
Dashboard yang diberikan melalui dashboards() tetap merupakan Page dalam semua aspek selain discovery — class-nya dideklarasikan pada Panel, bukan ditemukan dari directory:
use App\Panels\Admin\Pages\AccountsDashboard;
use PandaPanel\Pages\Dashboard;
$panel->dashboards([Dashboard::class, AccountsDashboard::class]);2
3
4
Dashboard pertama adalah root Panel. Dashboard lainnya didaftarkan sebagai Page bersama hasil discovery dan dideduplicate berdasarkan class. Dashboard yang juga berada di discovered path dapat masuk melalui dua jalur namun tetap hanya menjadi satu Page. Lihat Dashboards.
Registry
use PandaPanel\Core\PanelManager;
$pages = app(PanelManager::class)->pages('admin');
$pages->all(); // list<class-string<PageContract>>, sorted
$pages->has('settings'); // bool
$pages->bySlug('settings'); // class-string|null
$pages->count(); // int2
3
4
5
6
7
8
PandaPanel\Core\PageRegistry menolak dua kondisi saat registration, bukan menunggu sampai request:
| Situasi | Exception factory |
|---|---|
| Dua Page mengklaim slug yang sama | PanelRegistrationException::duplicatePageSlug() |
| Slug Page sudah digunakan sebuah Resource | PanelRegistrationException::slugCollidesWithResource() |
Mendaftarkan class yang sama dua kali adalah no-op, bukan error.
Caching
php artisan panel:cache
php artisan panel:clear2
php artisan panel:cache menulis bootstrap/cache/panels.php melalui PandaPanel\Cache\PanelManifest. Manifest hanya berisi nama class:
return array (
'admin' =>
array (
'resources' => array ( 0 => 'App\\Panels\\Admin\\Resources\\Users\\UserResource' ),
'pages' => array ( 0 => 'App\\Panels\\Admin\\Pages\\Settings' ),
'widgets' => array ( /* ... */ ),
),
);2
3
4
5
6
7
8
Ketika manifest tersedia, discovery tidak dijalankan: tidak ada filesystem scan, reflection, atau pekerjaan discovery per request. File ditulis secara atomically sehingga manifest setengah tertulis tidak pernah dapat dimuat. Letaknya bersama config dan route cache di bawah bootstrapPath('cache'), sehingga optimize:clear dapat menemukannya.
Data yang bergantung pada user tidak pernah di-cache — hasil authorization, navigation active state, badge value, maupun page props. Semua itu bergantung pada user dan URL saat ini; caching akan berisiko menyajikan hasil milik satu user kepada user lain. Lihat Caching.
Gotchas
- Page baru setelah
panel:cachetidak muncul. Manifest menjadi daftar authoritative. Jalankanphp artisan panel:clearatauoptimize:clearsaat development. - Discovery tidak mensyaratkan Page berada pada namespace Panel. Yang dibutuhkan adalah file berada di bawah declared path dan dapat di-resolve melalui PSR-4. File di
app/Panels/Admin/Pagesdengan namespaceApp\Pagestidak dapat di-resolve dari lokasi tersebut. - Dua Panel yang melakukan discovery pada directory yang sama sama-sama mendapatkan Page tersebut. Setiap Panel memiliki registry sendiri, sehingga shared directory valid digunakan untuk menerbitkan Page yang sama ke beberapa Panel — dan setiap Panel tetap melakukan authorization secara independen.
- Abstract base Page dalam folder yang sama dilewati secara silent. Enum, trait, dan value object juga dilewati. Karena itu tree Page dapat menyimpan helper miliknya sendiri.
- Discovery order bukan navigation registration order. Manifest ditentukan oleh nama class yang sudah di-sort; urutan sidebar ditentukan oleh
$navigationSortserta urutan group yang dideklarasikan Panel. Lihat Navigation groups.