Lifecycle Hooks
Lifecycle hook adalah titik-titik tempat create atau edit page mengizinkan Anda ikut campur: membentuk value saat form pertama dibuka, mengubah data yang divalidasi atau disimpan, menjalankan side effect setelah write, atau menghentikan seluruh proses. Semua hook berada pada PandaPanel\Resources\Concerns\HasLifecycleHooks, yang sudah digunakan setiap Resource page, sehingga cukup meng-override hook yang dibutuhkan tanpa registration tambahan.
Hook minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts\Pages;
use App\Panels\Admin\Resources\Posts\PostResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use PandaPanel\Resources\Pages\CreateRecord;
final class CreatePost extends CreateRecord
{
protected static string $resource = PostResource::class;
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeSave(array $data, ?Model $record): array
{
$data['slug'] = Str::slug((string) $data['title']);
return $data;
}
}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
Tidak ada bagian lain yang perlu diubah. Hook dipanggil sebelum proses write dan slug ikut dipersist bersama field yang dideklarasikan form.
Dua jenis hook — dan pemisahan ini penting
Hook mutate* menerima data lalu mengembalikannya kembali. Tujuannya adalah membentuk value yang akan ditampilkan atau disimpan. Hook lain tidak mengembalikan data; fungsinya untuk side effect dan untuk menghentikan lifecycle. Jika keduanya dicampur, akan ada dua tempat untuk mengubah value dan tidak ada tempat yang jelas untuk melakukan halt.
fill: beforeFill → mutateFormDataBeforeFill → afterFill
create: beforeValidate → validate → afterValidate → beforeCreate
→ mutateFormDataBeforeCreate → mutateFormDataBeforeSave → beforeSave
→ handleRecordCreation → saveRelations → afterCreate → afterSave
update: beforeValidate → validate → afterValidate
→ mutateFormDataBeforeSave → beforeSave → handleRecordUpdate
→ saveRelations → afterSave2
3
4
5
6
7
8
9
Urutan tersebut dikunci oleh test package menggunakan invocation nyata, bukan hanya berdasarkan dokumentasi ini.
Saat form dirender
protected function beforeFill(): void;
protected function mutateFormDataBeforeFill(array $data): array;
protected function afterFill(array $data): void;2
3
Ketiganya berjalan pada create maupun edit saat page sedang dirender. Pada create, value berasal dari default field. Pada edit, value berasal dari record.
Data diberikan sebagai map datar name => value, bukan serialized component tree. Dengan demikian page yang ingin mengubah satu field tidak perlu memahami bagaimana schema di-nest.
use Illuminate\Support\Str;
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeFill(array $data): array
{
$data['author_id'] = auth()->id();
return $data;
}2
3
4
5
6
7
8
9
10
11
12
beforeFill() dapat melakukan halt. Halt saat rendering berarti page memutuskan tidak boleh ditampilkan dan user diarahkan kembali ke Resource index.
Saat validation
protected function beforeValidate(array $input): array;
protected function afterValidate(array $data): array;2
beforeValidate() menerima raw request body lalu mengembalikan input yang benar-benar akan divalidasi. Dengan demikian value yang tidak pernah ditampilkan kepada user tetap dapat dimasukkan ke validation:
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
protected function beforeValidate(array $input): array
{
$input['team_id'] = auth()->user()->current_team_id;
return $input;
}2
3
4
5
6
7
8
9
10
afterValidate() menerima data yang sudah tervalidasi lalu mengembalikannya kembali. Hanya field yang dideklarasikan schema yang divalidasi; extra key dari request body dibuang daripada ikut masuk mass assignment.
beforeValidate() juga berjalan pada per-step validation milik Wizard. Value yang ditambahkan hook ini tersedia untuk step validation maupun final submit.
Saat create
protected function beforeCreate(): void;
protected function mutateFormDataBeforeCreate(array $data): array;2
beforeCreate() berjalan setelah validation dan sebelum data dibentuk atau ditulis. Ini adalah titik terakhir untuk melakukan halt dengan biaya paling kecil. mutateFormDataBeforeCreate() adalah transformation yang hanya berlaku pada create. Logic yang harus berlaku pada create dan update sebaiknya ditempatkan satu tahap setelahnya.
Kedua hook tersebut tidak pernah dipanggil saat update. EditRecord yang meng-override keduanya sedang meng-override method yang tidak akan pernah dijalankan pada flow tersebut.
Sebelum save pada create dan update
protected function mutateFormDataBeforeSave(array $data, ?Model $record): array;
protected function beforeSave(?Model $record): void;2
$record bernilai null pada create dan berisi record yang sedang diedit pada update. Inilah yang memungkinkan satu implementation digunakan pada dua flow:
use Illuminate\Database\Eloquent\Model;
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeSave(array $data, ?Model $record): array
{
$data['edited_by'] = auth()->id();
if ($record === null) {
$data['created_by'] = auth()->id();
}
return $data;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeSave() berjalan tepat sebelum write — dan berada di luar transaction, karena transaction baru dibuka pada persist step.
Proses write
use Illuminate\Database\Eloquent\Model;
protected function handleRecordCreation(array $attributes): Model; // CreateRecord
protected function handleRecordUpdate(Model $record, array $attributes): Model; // EditRecord2
3
4
Kedua method ini adalah proses write itu sendiri dan menerima dehydrated attributes, bukan raw form data. Override jika persistence perlu dilakukan melalui service, factory, atau relation, bukan save langsung:
use App\Services\PostPublisher;
use Illuminate\Database\Eloquent\Model;
/**
* @param array<string, mixed> $attributes
*/
protected function handleRecordCreation(array $attributes): Model
{
return app(PostPublisher::class)->create($attributes);
}2
3
4
5
6
7
8
9
10
Segera setelah itu, masih di transaction yang sama, schema menulis related row dan pivot row milik record. Data relasi membutuhkan primary key yang sesaat sebelumnya belum ada; karena itu relation write tidak dimasukkan sebagai attribute utama dan afterCreate() sudah dapat melihat hasil relation write tersebut.
Setelah write
use Illuminate\Database\Eloquent\Model;
protected function afterCreate(Model $record): void; // create only
protected function afterSave(Model $record): void; // create and update2
3
4
use App\Jobs\WarmPostCache;
use Illuminate\Database\Eloquent\Model;
protected function afterCreate(Model $record): void
{
WarmPostCache::dispatch($record->getKey());
}2
3
4
5
6
7
Keduanya berjalan di dalam transaction. Jika hook melempar exception, write ikut di-rollback daripada meninggalkan perubahan setengah selesai. Dispatch job dari hook ini hanya aman jika queue connection menghormati after_commit.
Menghentikan lifecycle
protected function halt(): never;$this->halt() menghentikan lifecycle dari hook mana pun. Tidak ada hook setelahnya yang dijalankan dan tidak ada data yang ditulis. Method melempar PandaPanel\Exceptions\Halt, yang ditangkap oleh page. Halt adalah keputusan page, bukan error, sehingga tidak muncul sebagai 500 dan tidak membocorkan stack trace.
protected function beforeCreate(): void
{
if (! auth()->user()->hasQuotaRemaining()) {
session()->flash('error', 'Your plan is at its limit.');
$this->halt();
}
}2
3
4
5
6
7
8
Destination user bergantung pada kapan halt terjadi. Halt saat handle() mengembalikan user ke tempat sebelumnya, sedangkan halt saat render() mengarahkan user ke Resource index.
Transactions
protected static ?bool $hasDatabaseTransactions = null;null berarti mengikuti setting Panel, yang default-nya mengaktifkan transaction. Persist step, relation write, dan hook after* berjalan dalam satu transaction. Seluruh bagian sebelum handleRecord* berjalan di luar transaction.
Nonaktifkan pada page yang write-nya tidak masuk akal bila transaction terus dibuka, misalnya flow yang juga memanggil external service:
protected static ?bool $hasDatabaseTransactions = false;Di luar context Panel — misalnya page controller dipanggil langsung dari test atau queued job — tidak ada Panel yang dapat ditanyakan sehingga default jawabannya adalah transaction aktif.
Yang terjadi setelah lifecycle selesai
use Illuminate\Database\Eloquent\Model;
protected function getRedirectUrl(Model $record): string;
protected function createdNotification(Model $record): ?array; // CreateRecord
protected function savedNotification(Model $record): ?array; // EditRecord2
3
4
5
Kedua notification method mengembalikan ['type' => ..., 'message' => ...], atau null jika page tidak ingin menampilkan notification. Lihat CRUD pages.
Delete tidak memiliki lifecycle hook di sini
Delete berjalan melalui Action endpoint tanpa instance page, sehingga beforeDelete() pada concern ini tidak akan memiliki kesempatan untuk dipanggil. Gunakan hook milik Action itu sendiri; hook tersebut menggunakan transaction milik Action:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use PandaPanel\Actions\DeleteAction;
DeleteAction::make(PostResource::class)
->before(static function (Model $record, array $data): void {
Cache::forget('post:'.$record->getKey());
})
->after(static function (Model $record, array $data): void {
Log::info('Deleted post '.$record->getKey());
});2
3
4
5
6
7
8
9
10
11
12
Kedua callback menerima record dan form data milik Action, dan keduanya berjalan di dalam transaction Action.
Lihat Actions.
Referensi
| Hook | Signature | Berjalan pada | Return |
|---|---|---|---|
beforeFill | protected function beforeFill(): void | render | — |
mutateFormDataBeforeFill | protected function mutateFormDataBeforeFill(array $data): array | render | Value yang digunakan saat form dibuka |
afterFill | protected function afterFill(array $data): void | render | — |
beforeValidate | protected function beforeValidate(array $input): array | create, update, Wizard step | Input yang akan divalidasi |
afterValidate | protected function afterValidate(array $data): array | create, update | Validated data |
beforeCreate | protected function beforeCreate(): void | create | — |
mutateFormDataBeforeCreate | protected function mutateFormDataBeforeCreate(array $data): array | create | Data |
mutateFormDataBeforeSave | protected function mutateFormDataBeforeSave(array $data, ?Model $record): array | create, update | Data |
beforeSave | protected function beforeSave(?Model $record): void | create, update | — |
handleRecordCreation | protected function handleRecordCreation(array $attributes): Model | create | Record baru |
handleRecordUpdate | protected function handleRecordUpdate(Model $record, array $attributes): Model | update | Record |
afterCreate | protected function afterCreate(Model $record): void | create | — |
afterSave | protected function afterSave(Model $record): void | create, update | — |
halt | protected function halt(): never | Di mana pun | Tidak pernah return |
Catatan penting
- Setiap hook memiliki no-op default yang benar-benar dipanggil. Meng-override hook adalah seluruh wiring yang diperlukan; tidak ada registration step.
- Hook
mutate*yang lupa mengembalikan array akan menghilangkan data. Hook tersebut adalah transformation, bukan side effect. beforeSave()berada di luar transaction. Hanya write dan hookafter*yang berada di dalamnya. Side effect padabeforeSave()tetap terjadi walaupun write kemudian gagal.mutateFormDataBeforeCreate()danbeforeCreate()tidak berjalan saat update. GunakanmutateFormDataBeforeSave()untuk logic yang dibutuhkan kedua flow.- Hook bukan tempat menyembunyikan workflow besar. Gunakan untuk shaping data dan side effect kecil. Business logic yang substansial sebaiknya berada dalam Action atau service class yang dipanggil hook.
- Halt bukan failure. Tidak ada exception yang ditampilkan ke user, tidak ada data yang ditulis, dan user tidak mendapat error page.