Membuat Plugin
Halaman ini membangun sebuah plugin mulai dari bentuk paling kecil yang sudah bekerja hingga versi yang didistribusikan sebagai Composer package sendiri. Gunakan ketika Anda sudah memutuskan bahwa sekumpulan konfigurasi Panel perlu digunakan ulang; Konsep Plugin membantu menentukan kapan sebuah fitur layak dijadikan plugin.
Plugin paling kecil yang sudah bekerja
Extend PandaPanel\Plugins\Plugin dan implementasikan satu method:
<?php
declare(strict_types=1);
namespace App\Panels\Plugins;
use App\Panels\Admin\Resources\Reports\ReportResource;
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
11
12
13
14
15
16
17
use App\Panels\Plugins\ReportingPlugin;
$panel->plugins([
new ReportingPlugin,
]);2
3
4
5
Base class sudah menyediakan id(), boot(), metadata(), dan publishes(), sehingga hanya register() yang wajib ditulis. make() tidak diwajibkan oleh framework — itu hanya convention agar chaining lebih enak dibaca.
Menambahkan static constructor
public static function make(): self
{
return new self;
}2
3
4
$panel->plugins([
ReportingPlugin::make(),
]);2
3
Tambahkan method ini ketika plugin memiliki fluent setter karena ReportingPlugin::make()->withCharts() lebih mudah dibaca daripada (new ReportingPlugin)->withCharts(). Jika tidak memberikan manfaat, tidak perlu dibuat.
Membuat plugin configurable
Plugin yang sama sekali tidak menerima konfigurasi sering kali hanya menjadi class yang sebenarnya dapat ditulis langsung oleh aplikasi. Pola yang berguna adalah fluent setter yang menyimpan state, mengembalikan $this, lalu state tersebut dibaca di register():
<?php
declare(strict_types=1);
namespace App\Panels\Plugins;
use App\Panels\Admin\Resources\Reports\ReportResource;
use App\Panels\Admin\Widgets\RevenueChart;
use PandaPanel\Core\Panel;
use PandaPanel\Plugins\Plugin;
final class ReportingPlugin extends Plugin
{
private bool $charts = true;
private ?string $group = 'Insights';
private string $currency = 'usd';
public static function make(): self
{
return new self;
}
public function withCharts(bool $charts = true): self
{
$this->charts = $charts;
return $this;
}
public function group(?string $group): self
{
$this->group = $group;
return $this;
}
public function currency(string $currency): self
{
$this->currency = $currency;
return $this;
}
/** Read back by the plugin's own resources. */
public function getCurrency(): string
{
return $this->currency;
}
public function register(Panel $panel): void
{
if ($this->group !== null) {
$panel->navigationGroups([$this->group]);
}
$panel->resources([ReportResource::class]);
if ($this->charts) {
$panel->widgets([RevenueChart::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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Satu plugin dapat menghasilkan dua bentuk berbeda pada dua Panel tanpa masing-masing Panel perlu membuat class baru:
$admin->plugins([ReportingPlugin::make()->currency('eur')]);
$app->plugins([ReportingPlugin::make()->withCharts(false)->group(null)]);2
Konfigurasi yang wajib dan tidak boleh terlupa lebih tepat diletakkan pada constructor daripada setter:
public function __construct(private readonly string $currency) {}
public static function make(string $currency): self
{
return new self($currency);
}2
3
4
5
6
Membaca kembali konfigurasi plugin
Resource milik plugin biasanya perlu membaca setting sebagaimana plugin dipasang. Ada dua cara, dan cara kedua biasanya lebih nyaman:
use PandaPanel\Contracts\PanelPlugin;
$plugin = panel()?->plugin('reporting'); // ?PanelPlugin2
3
use App\Panels\Plugins\ReportingPlugin;
$currency = ReportingPlugin::in(panel())?->getCurrency() ?? 'usd';2
3
Plugin::in(?Panel $panel): ?static melakukan lookup berdasarkan class, sehingga return value sudah bertipe plugin Anda sendiri dan tidak membutuhkan instance check. Method mengembalikan null ketika Panel tidak memasang plugin, termasuk ketika panel() sendiri menghasilkan null karena kode berjalan di luar request Panel. Karena itulah contoh di atas menggunakan fallback ?? 'usd'.
Melakukan pekerjaan pada boot()
register() berjalan saat aplikasi masih melakukan boot service provider. Semua hal yang membutuhkan container, request, URL, atau authenticated user harus diletakkan di boot():
use PandaPanel\Core\Panel;
use PandaPanel\Enums\RenderHook;
public function boot(Panel $panel): void
{
// A route name only exists once routes are registered, and the user
// only exists once the request has been authenticated. Both are true
// here and neither is true in register().
$panel->renderHook(
RenderHook::SidebarEnd,
'Panels/AcmeReporting/Hooks/ReportShortcuts',
[
'url' => route($panel->routeName('resources.reports.index')),
'name' => auth()->user()?->name,
],
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boot() berjalan sekali untuk setiap request yang mencapai Panel, setelah access check. Register dan Boot menjelaskan ordering serta aturan idempotency yang diperlukan karena method berjalan per request.
Memberi nama plugin
Base class menurunkan ID dan display name dari nama class. Override ketika hasil default tidak sesuai:
use PandaPanel\Plugins\PluginMetadata;
public function id(): string
{
return 'acme-reporting';
}
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
12
13
14
15
16
package digunakan oleh php artisan panel:plugins untuk mencari versi package yang benar-benar terpasang melalui Composer, sedangkan requiresPanel diperiksa saat registration. Detail keduanya ada di Plugin Metadata dan Kompatibilitas Versi.
Mengirim Vue component
Component yang masih berada di package plugin tidak dapat di-resolve secara langsung. Semua component registry menggunakan import.meta.glob terhadap resources/js/pages/Panels/** milik aplikasi. Deklarasikan source file plugin dan destination-nya:
/**
* @return array<string, string> absolute source => absolute destination
*/
public function publishes(): array
{
return [
__DIR__.'/../resources/js' => resource_path('js/pages/Panels/Reporting'),
];
}2
3
4
5
6
7
8
9
php artisan panel:publish reporting
npm run build2
Plugin Assets menjelaskan behavior command secara lengkap beserta aturan directory yang diharapkan registry.
Mengirim plugin sebagai package
Plugin yang didistribusikan melalui Packagist sebaiknya mengimplementasikan PandaPanel\Contracts\PanelPlugin secara langsung, bukan extend Plugin. Package yang extend base class aplikasi menjadi lebih terikat pada implementation class tersebut, padahal framework sendiri hanya berbicara melalui contract — setiap lookup, hook, dan panel:publish menggunakan PanelPlugin.
<?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 static function make(): self
{
return new self;
}
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',
requiresPanel: '^0.1',
);
}
/** @return array<string, string> */
public function publishes(): array
{
return [
__DIR__.'/../resources/js' => resource_path('js/pages/Panels/AcmeReporting'),
];
}
}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
46
47
48
49
Mengimplementasikan contract langsung berarti menulis semua lima method, termasuk dua method yang pada base class memiliki default. Trade-off-nya adalah lebih sedikit coupling dengan concrete base class dan sedikit lebih banyak boilerplate.
Service provider milik package
PanelPlugin bukan service provider dan tidak pernah di-resolve oleh container. Migration, config file, translation, event listener, dan route di luar Panel tetap berada pada Laravel service provider biasa yang dikirim bersama package:
<?php
declare(strict_types=1);
namespace Acme\Reporting;
use Illuminate\Support\ServiceProvider;
final class ReportingServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->mergeConfigFrom(__DIR__.'/../config/reporting.php', 'reporting');
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "acme/panda-reporting",
"require": {
"php": "^8.2",
"chocoalano/panel": "^0.1"
},
"autoload": {
"psr-4": {
"Acme\\Reporting\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Acme\\Reporting\\ReportingServiceProvider"
]
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Composer package discovery mendaftarkan service provider secara otomatis. Object plugin tetap harus dipasang secara eksplisit pada panel provider. Package yang hanya terpasang tidak seharusnya diam-diam menambahkan resource dan navigation ke admin Panel tanpa baris konfigurasi aplikasi yang menyatakan hal tersebut.
Struktur directory yang disarankan
packages/reporting/
├── composer.json
├── resources/
│ └── js/
│ └── Widgets/
│ └── RevenueChart.vue
└── src/
├── ReportingPlugin.php
├── ReportingServiceProvider.php
├── Resources/
│ └── Reports/
│ ├── ReportResource.php
│ ├── Forms/ReportForm.php
│ └── Tables/ReportsTable.php
└── Widgets/
└── RevenueChart.php2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Untuk plugin yang hanya hidup di aplikasi dan tidak akan menjadi package, app/Panels/Plugins/ adalah lokasi yang masuk akal. Tidak ada generator untuk kedua jenis plugin: command make:panel-plugin memang tidak tersedia karena sebuah plugin hanya membutuhkan satu class dengan satu method wajib, dan stub generator justru dapat lebih panjang daripada class yang dihasilkan.
Catatan
- Object plugin dibangun oleh aplikasi, bukan container. Tidak ada constructor injection otomatis. Resolve dependency yang diperlukan di dalam
boot()menggunakanapp(). register()dipanggil langsung olehPanel::plugins()mengikuti urutan array. Plugin dapat membaca konfigurasi yang sudah ditambahkan plugin sebelumnya, tetapi bergantung pada perilaku tersebut membuat install order menjadi penting.- Discovery path dapat berasal dari plugin. Itulah cara package mendaftarkan seluruh directory resource:
$panel->discoverResources(__DIR__.'/Resources'). Path tersebut di-cache olehpanel:cacheseperti discovery path lain.