Plugin Contract
PandaPanel\Contracts\PanelPlugin adalah seluruh API plugin: lima method, tanpa container binding, tanpa attribute, dan tanpa file registrasi khusus. Gunakan halaman ini ketika Anda mengimplementasikan interface secara langsung — pola yang cocok untuk plugin yang dikirim sebagai package sendiri — atau ketika Anda perlu mengetahui secara tepat method apa yang dipanggil framework dan kapan dipanggil.
Interface
<?php
namespace PandaPanel\Contracts;
use PandaPanel\Core\Panel;
use PandaPanel\Plugins\PluginMetadata;
interface PanelPlugin
{
public function id(): string;
public function register(Panel $panel): void;
public function boot(Panel $panel): void;
public function metadata(): PluginMetadata;
/** @return array<string, string> absolute source path => absolute destination path */
public function publishes(): array;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Contoh implementasi lengkap tanpa inheritance:
<?php
declare(strict_types=1);
namespace Acme\Reporting;
use PandaPanel\Contracts\PanelPlugin;
use PandaPanel\Core\Panel;
use PandaPanel\Plugins\PluginMetadata;
final class ReportingPlugin implements PanelPlugin
{
public function id(): string
{
return 'acme-reporting';
}
public function register(Panel $panel): void
{
$panel->resources([Resources\ReportResource::class]);
}
public function boot(Panel $panel): void
{
//
}
public function metadata(): PluginMetadata
{
return new PluginMetadata(
name: 'Acme Reporting',
package: 'acme/panda-reporting',
);
}
public function publishes(): array
{
return [];
}
}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
Siapa memanggil apa, dan dari mana
| Method | Dipanggil oleh | Kapan |
|---|---|---|
id() | Panel::plugins() | sekali, untuk memberi key plugin dan mendeteksi duplicate |
id() | panel:publish | untuk mencocokkan optional argument plugin |
register() | Panel::plugins() | langsung, mengikuti urutan array |
metadata() | PluginCompatibility::assert() | saat plugins(), sebelum register() |
metadata() | panel:plugins | sekali untuk setiap row laporan |
boot() | Panel::boot() | per request yang masuk ke Panel tersebut, setelah access check |
publishes() | panel:publish | hanya ketika command tersebut dijalankan |
Tidak ada bagian framework lain yang menyentuh plugin. Tidak ada discovery, container resolution, maupun serialization. panel:cache menyimpan nama class resource, page, dan widget, bukan object plugin yang meregistrasikannya.
id(): string
Nama stabil yang digunakan sebagai key plugin di dalam Panel. Dua plugin tidak boleh menggunakan ID yang sama, dan Panel dapat ditanya apakah memiliki plugin tertentu tanpa perlu mengetahui class plugin.
public function id(): string
{
return 'acme-reporting';
}2
3
4
ID harus stabil lintas versi. Aplikasi yang memanggil hasPlugin('acme-reporting') sedang menanyakan plugin, bukan release plugin. Mengganti ID pada minor release akan merusak aplikasi yang bergantung pada ID tersebut.
Default Plugin menurunkan ID dari nama class — lihat base class di bawah.
Dua plugin dengan satu ID menghasilkan exception dari plugins():
use PandaPanel\Exceptions\PanelRegistrationException;
// Two plugins claim the id [reporting] in panel [admin]. A plugin id is how a
// panel is asked whether it has one, so it has to be unique.2
3
4
register(Panel $panel): void
Mengonfigurasi Panel. Method ini berjalan saat Panel sedang dibangun di dalam Panel::plugins(), sebelum plugin dapat digunakan dan sebelum request tersedia.
use PandaPanel\Core\Panel;
public function register(Panel $panel): void
{
$panel
->navigationGroups(['Insights'])
->discoverResources(__DIR__.'/Resources')
->widgets([Widgets\RevenueChart::class]);
}2
3
4
5
6
7
8
9
Jangan melakukan query database, resolve route, atau membaca current user di sini. Belum ada request, dan method ini berjalan saat service provider boot untuk setiap request yang dilayani aplikasi, baik request tersebut masuk ke Panel maupun tidak.
Return value diabaikan. Method mengubah object Panel yang diberikan, bukan membuat Panel baru.
boot(Panel $panel): void
Dijalankan setelah Panel berhasil di-resolve untuk request, melalui Panel::boot().
use PandaPanel\Core\Panel;
use PandaPanel\Enums\RenderHook;
public function boot(Panel $panel): void
{
if (auth()->user()?->is_admin) {
$panel->renderHook(RenderHook::HeaderEnd, 'Panels/Admin/Hooks/AuditLink');
}
}2
3
4
5
6
7
8
9
Sebagian besar plugin hanya berisi konfigurasi dan tidak membutuhkan pekerjaan di sini. Karena itu Plugin memberikan default no-op. Jika mengimplementasikan contract secara langsung, method kosong tetap harus ditulis.
Jaminan urutannya: seluruh boot() plugin berjalan sebelum callback bootUsing() milik Panel. Dengan demikian aplikasi selalu memiliki keputusan terakhir atas plugin yang dipasang. Lihat Register dan Boot.
metadata(): PluginMetadata
Menjelaskan identitas plugin untuk laporan yang dibaca manusia sekaligus versi framework yang dibutuhkan.
use PandaPanel\Plugins\PluginMetadata;
public function metadata(): PluginMetadata
{
return new PluginMetadata(
name: 'Acme Reporting',
package: 'acme/panda-reporting',
requiresPanel: '^1.2',
url: 'https://github.com/acme/panda-reporting',
);
}2
3
4
5
6
7
8
9
10
11
Method ini ada pada contract, bukan hanya base class, karena dua pertanyaan saat plugin bermasalah selalu "plugin yang mana?" dan "versi berapa?". Keduanya tidak dapat dijawab hanya dari nama class. Lihat Plugin Metadata untuk value object dan Kompatibilitas Versi untuk perilaku requiresPanel.
Karena metadata() dipanggil saat registration, method ini juga tidak boleh melakukan query atau resolve sesuatu yang bergantung pada request.
publishes(): array
Daftar file yang akan disalin plugin ke aplikasi, dengan source sebagai key dan destination sebagai value.
/**
* @return array<string, string>
*/
public function publishes(): array
{
return [
__DIR__.'/../resources/js' => resource_path('js/pages/Panels/AcmeReporting'),
__DIR__.'/../stubs/report.stub' => base_path('stubs/report.stub'),
];
}2
3
4
5
6
7
8
9
10
Source dapat berupa file atau directory. Directory disalin recursive dengan mempertahankan relative path. Kedua sisi menggunakan absolute path, sehingga bentuk method lebih tepat daripada configuration statis: hanya plugin yang mengetahui lokasi file miliknya.
Method ini berada pada contract karena panel:publish harus dapat menanyakan file publish kepada semua plugin, termasuk plugin yang tidak meng-extend base class. Plugin yang tidak membawa file mengembalikan [], dan itulah kasus paling umum.
Base class
PandaPanel\Plugins\Plugin mengimplementasikan contract dengan default untuk semua method kecuali register():
| Method | Default |
|---|---|
id() | Str::kebab(Str::beforeLast(class_basename(static::class), 'Plugin')) — ReportingPlugin → reporting |
register() | abstract dari interface — Anda menulisnya |
boot() | no-op |
metadata() | new PluginMetadata(name: Str::headline($this->id())) — tanpa package, version, atau constraint |
publishes() | [] |
use PandaPanel\Core\Panel;
use PandaPanel\Plugins\Plugin;
final class ReportingPlugin extends Plugin
{
public function register(Panel $panel): void
{
$panel->resources([ReportResource::class]);
}
}2
3
4
5
6
7
8
9
10
Base class juga menambahkan satu static method yang tidak ada di interface.
Plugin::in(?Panel $panel): ?static
Mengambil instance plugin sebagaimana dikonfigurasi pada Panel tertentu. Ini adalah kebalikan dari Panel::plugin() dan biasanya digunakan oleh resource milik plugin untuk membaca setting plugin:
use App\Panels\Plugins\ReportingPlugin;
$currency = ReportingPlugin::in(panel())?->getCurrency() ?? 'usd';2
3
Jika Panel tidak memiliki plugin tersebut, hasilnya null, bukan exception. Resource yang dipakai bersama dua Panel dan plugin hanya tersedia pada salah satu Panel merupakan konfigurasi normal. panel() juga menghasilkan null di luar request Panel, dan in(null) tetap menghasilkan null.
Lookup dilakukan berdasarkan class, bukan id(), karena membaca ID dengan membangun instance baru tidak selalu mungkin. Plugin dengan constructor yang membutuhkan konfigurasi tidak dapat diinstansiasi tanpa konfigurasi tersebut.
Plugin yang mengimplementasikan contract langsung tidak mendapatkan in(). Anda dapat menyalin logic sederhana tersebut atau menggunakan lookup Panel dan melakukan type narrowing:
use Acme\Reporting\ReportingPlugin;
$plugin = panel()?->plugin('acme-reporting');
$currency = $plugin instanceof ReportingPlugin ? $plugin->getCurrency() : 'usd';2
3
4
5
Mana yang sebaiknya digunakan
extends Plugin | implements PanelPlugin | |
|---|---|---|
| Tinggal di aplikasi | ya | dapat digunakan, tetapi Anda menulis empat method tambahan |
| Dikirim sebagai Composer package | membuat package bergantung pada base class ini | ya |
| Method yang harus ditulis | register() | semua lima method |
Mendapat in() | ya | tidak |
Keduanya didukung setara oleh framework. Framework tidak pernah membutuhkan concrete Plugin; seluruh lookup, hook, dan panel:publish bekerja melalui contract.
Sisi Panel
use PandaPanel\Contracts\PanelPlugin;
use PandaPanel\Core\Panel;
/** @param array<array-key, PanelPlugin> $plugins */
public function plugins(array $plugins): self;
/** @return array<string, PanelPlugin> keyed by id */
public function getPlugins(): array;
public function hasPlugin(string $id): bool;
public function plugin(string $id): ?PanelPlugin;2
3
4
5
6
7
8
9
10
11
12
plugins() dapat dipanggil lebih dari sekali. Setiap pemanggilan menambahkan plugin. Duplicate-ID check mencakup seluruh plugin yang sudah pernah ditambahkan, bukan hanya array pada pemanggilan saat ini.
Catatan
- Interface tidak memiliki
make(). Itu hanya convention pada implementation, bukan contract method, dan framework tidak pernah memanggilnya. - Gunakan type
PanelPlugin, bukan concretePlugin, ketika menerima plugin dari pihak lain.Panel::plugin()sendiri sudah mengembalikan contract. register()danmetadata()sama-sama dipanggil saat Panel dibangun, danmetadata()dipanggil lebih dulu karena compatibility check harus berjalan sebelum plugin boleh mengubah Panel.