Konteks Panel
PandaPanel\Support\PanelContext menyimpan state yang hanya berlaku untuk satu request: Panel mana yang sedang aktif, serta data lain yang memiliki scope yang sama. Class ini merupakan container singleton yang di-bind menggunakan scoped(), sehingga masa hidupnya tepat selama request atau test case berlangsung. Gunakan class ini — biasanya melalui helper panel() — ketika kode backend perlu mengetahui sedang berjalan di dalam Panel mana.
Membaca Panel yang Sedang Aktif
$panel = panel(); // the panel for this request, or null
$admin = panel('admin'); // an explicit panel; throws if unknown
$panel?->getId(); // 'admin'2
3
4
Itulah kasus penggunaan yang paling umum. panel() adalah function global yang di-autoload dari src/Support/helpers.php. Function ini dilindungi dengan function_exists() sehingga jika application Anda sudah memiliki function dengan nama yang sama, PandaBear tidak akan menimpanya.
/**
* @throws PandaPanel\Exceptions\PanelRegistrationException
*/
function panel(?string $id = null): ?Panel2
3
4
| Pemanggilan | Mengembalikan | Exception |
|---|---|---|
panel() | Panel yang sedang di-resolve, atau null jika request berada di luar Panel | tidak pernah |
panel('admin') | Panel dengan id tersebut | PanelRegistrationException::unknownPanel() |
Perbedaan behavior ini memang disengaja. Pertanyaan "saya sedang berada di Panel mana?" memiliki jawaban yang valid berupa null, sehingga setiap consumer harus mampu menanganinya. Sebaliknya, "berikan Panel bernama nope" tidak memiliki kondisi valid seperti itu — id yang tidak dikenal adalah developer error, bukan state yang seharusnya ditoleransi secara diam-diam.
Kapan Context Diisi
PandaPanel\Http\Middleware\ResolvePanel mengisinya di dalam route group milik Panel, setelah middleware auth dan sebelum controller dijalankan:
$this->manager->setCurrentPanel($panel);
abort_unless($panel->isAccessibleTo($request->user()), 403);
$panel->boot();2
3
4
5
PandaPanel\Http\Middleware\ResetPanelContext membersihkannya pada awal setiap request web. Pasangan middleware inilah yang memastikan bahwa "tidak ada current Panel di luar route Panel" benar karena desain, bukan hanya kebetulan:
$this->actingAs($user)->get('/admin');
panel()?->getId(); // 'admin'
$this->actingAs($user)->get('/dashboard');
panel(); // null2
3
4
5
Tanpa proses reset tersebut, route non-Panel dapat mewarisi state yang ditinggalkan oleh request sebelumnya. Pada request PHP klasik, container dibangun ulang setiap request sehingga kebocoran ini sulit terlihat. Di bawah Octane, atau di dalam test yang menjalankan beberapa request dalam satu proses, masalah tersebut menjadi nyata.
API
use PandaPanel\Support\PanelContext;
$context = app(PanelContext::class);2
3
| Method | Signature | Catatan |
|---|---|---|
setPanel | setPanel(?Panel $panel): void | Dipanggil oleh ResolvePanel. |
panel | panel(): ?Panel | |
hasPanel | hasPanel(): bool | |
set | set(string $key, mixed $value): void | Generic bag untuk state request-scoped tambahan. |
get | get(string $key, mixed $default = null): mixed | |
forget | forget(): void | Membersihkan Panel dan seluruh bag. |
$context->setPanel(app(PanelManager::class)->get('admin'));
$context->hasPanel(); // true
$context->panel()?->getId();// 'admin'
$context->set('report.range', 'quarter');
$context->get('report.range'); // 'quarter'
$context->get('report.currency', 'USD');// 'USD'
$context->forget();
$context->hasPanel(); // false2
3
4
5
6
7
8
9
10
11
Tidak ada state static di sini. Itulah aturan desain utama yang ingin ditegakkan oleh class ini: state Panel yang disimpan secara static dapat bocor antar-request di Octane dan antar-test case dalam satu proses. Kedua masalah tersebut biasanya terlihat sebagai test yang lulus ketika dijalankan sendiri tetapi gagal ketika dijalankan bersama seluruh suite.
PandaPanel\Core\PanelManager mendelegasikan state current Panel ke context ini, bukan menyimpannya sendiri:
$manager->currentPanel(); // $context->panel()
$manager->hasCurrentPanel(); // $context->hasPanel()
$manager->setCurrentPanel($p); // $context->setPanel($p)2
3
Apa Lagi yang Disimpan di Dalam Bag
Dua fitur framework lain menyimpan nilai per-request di sini alih-alih menggunakan static, dengan alasan yang sama seperti current Panel. Keduanya menyediakan accessor sendiri; Anda tidak diharapkan membaca key internal-nya secara langsung.
Parent Record
PandaPanel\Support\ParentRecord menyimpan data pada key panel.parent-record.
use PandaPanel\Support\ParentRecord;
ParentRecord::current(); // ?Model
ParentRecord::require(PostResource::class); // Model, or PanelRegistrationException
ParentRecord::routeParameter(); // 'parentRecord'2
3
4
5
ResolveParentRecord melakukan binding dari route, sedangkan Resource::query() membacanya. Bagian lain sebaiknya tidak membaca parent secara langsung — nested resource yang mengambil parent di tempat selain query akan menciptakan lokasi kedua tempat scope dapat terlupakan.
require() melempar exception alih-alih mengembalikan null, karena nested resource tanpa parent yang ter-bind berarti route didaftarkan secara salah. Membiarkan query berjalan tanpa scope dalam kondisi tersebut dapat menampilkan seluruh child dari seluruh parent.
Tenant
PandaPanel\Tenancy\Tenancy menyimpan data pada key panel.tenant.
use PandaPanel\Tenancy\Tenancy;
Tenancy::current(); // ?Model
Tenancy::require(); // Model, or PanelRegistrationException
Tenancy::bind($team); // ResolveTenant and tests only2
3
4
5
Aturannya sama: resource yang tenant-scoped tetapi tidak memiliki tenant ter-bind berarti route tersebut berjalan tanpa ResolveTenant. Membiarkan proses berlanjut dalam kondisi itu berpotensi menampilkan record milik semua tenant kepada user yang melakukan request.
Menggunakannya di Kode Anda Sendiri
Generic bag tersedia agar module dapat menambahkan request scope tanpa harus mengubah bentuk PanelContext. Bungkus key tersebut dalam accessor bernama daripada menyebarkan string key di berbagai tempat:
<?php
declare(strict_types=1);
namespace App\Panels\Support;
use PandaPanel\Support\PanelContext;
final class ReportRange
{
private const KEY = 'app.report-range';
public static function bind(string $range): void
{
app(PanelContext::class)->set(self::KEY, $range);
}
public static function current(): string
{
$range = app(PanelContext::class)->get(self::KEY);
return is_string($range) ? $range : 'month';
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Lakukan binding dari middleware di dalam Panel group, atau dari boot callback:
$panel->bootUsing(static function (): void {
ReportRange::bind(request()->string('range', 'month')->toString());
});2
3
Boot callback dijalankan di ResolvePanel setelah access check berhasil, sehingga callback boleh mengasumsikan current Panel sudah tersedia dan user memang diizinkan masuk.
Catatan
panel()di dalamSharePanelDatabernilainullpada setiap page non-Panel, termasuk page bawaan starter kit. Props tetap di-share untuk setiap requestweb; hanya data Panel yang bersifat conditional.- Nilai dalam bag bertipe
mixed. Reader sebaiknya melakukan narrowing seperti$v instanceof Modelatauis_string($v), bukan melakukan casting paksa. Nilai dengan tipe yang tidak sesuai sebaiknya dianggap sebagai "tidak ada data yang ter-bind", bukan melempar exception di lokasi yang tidak berhubungan. forget()membersihkan current Panel sekaligus seluruh bag. Tidak ada API untuk menghapus satu key saja; sebuah request memiliki context lengkap atau tidak sama sekali.- Context di-bind menggunakan
scoped(), bukansingleton(). Di bawah Octane, scoped binding di-flush antar-request sehingga instance baru akan dibuat.ResetPanelContexttetap melindungi kondisi ketika proses tersebut tidak terjadi. - Test yang membutuhkan Panel tanpa menjalankan HTTP request dapat mengaturnya langsung:
app(PanelContext::class)->setPanel(panel('admin')). Ingat bahwa state tersebut tetap aktif sampai test case selesai.