-
-
Notifications
You must be signed in to change notification settings - Fork 268
Add admin activity logging for CRUD operations #2189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| <?php | ||
|
|
||
| namespace App\Listeners; | ||
|
|
||
| use App\Facades\Activity; | ||
| use Filament\Facades\Filament; | ||
| use Filament\Resources\Pages\Page; | ||
| use Illuminate\Database\Eloquent\Model; | ||
| use Illuminate\Support\Str; | ||
|
|
||
| class AdminActivityListener | ||
| { | ||
| protected const REDACTED_FIELDS = [ | ||
| 'password', | ||
| 'password_confirmation', | ||
| 'token', | ||
| 'secret', | ||
| 'api_key', | ||
| ]; | ||
|
|
||
| /** @param array<string, mixed> $data */ | ||
| public function handle(Model $record, array $data, Page $page): void | ||
| { | ||
| if (Filament::getCurrentPanel()?->getId() !== 'admin') { | ||
| return; | ||
| } | ||
|
|
||
| $resourceClass = $page::getResource(); | ||
| $modelClass = $resourceClass::getModel(); | ||
| $slug = Str::kebab(class_basename($modelClass)); | ||
|
|
||
| $action = $page instanceof \Filament\Resources\Pages\CreateRecord ? 'create' : 'update'; | ||
|
|
||
| $properties = $this->redactSensitiveFields($data); | ||
|
|
||
| Activity::event("admin:$slug.$action") | ||
| ->subject($record) | ||
| ->property($properties) | ||
| ->log(); | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $data | ||
| * @return array<string, mixed> | ||
| */ | ||
| protected function redactSensitiveFields(array $data): array | ||
| { | ||
| $redacted = []; | ||
|
|
||
| foreach ($data as $key => $value) { | ||
| if (in_array($key, self::REDACTED_FIELDS, true)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (is_array($value)) { | ||
| $redacted[$key] = $this->redactSensitiveFields($value); | ||
| } else { | ||
| $redacted[$key] = $value; | ||
| } | ||
| } | ||
|
|
||
| return $redacted; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,10 @@ | |
|
|
||
| namespace App\Providers; | ||
|
|
||
| use App\Listeners\AdminActivityListener; | ||
| use App\Listeners\DispatchWebhooks; | ||
| use Filament\Resources\Events\RecordCreated; | ||
| use Filament\Resources\Events\RecordUpdated; | ||
| use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; | ||
|
|
||
| class EventServiceProvider extends ServiceProvider | ||
|
|
@@ -15,5 +18,7 @@ class EventServiceProvider extends ServiceProvider | |
| 'eloquent.created*' => [DispatchWebhooks::class], | ||
| 'eloquent.deleted*' => [DispatchWebhooks::class], | ||
| 'eloquent.updated*' => [DispatchWebhooks::class], | ||
| RecordCreated::class => [AdminActivityListener::class], | ||
| RecordUpdated::class => [AdminActivityListener::class], | ||
|
Comment on lines
+21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find AdminActivityListener and check its handle method
echo "=== AdminActivityListener location and handle signature ==="
find . -name "*AdminActivityListener*" -type f
echo ""
echo "=== AdminActivityListener handle method signature ==="
rg -n "class AdminActivityListener|function handle|public function handle" app/ --type php -A 3 | grep -A 10 "AdminActivityListener"Repository: pelican-dev/panel Length of output: 1470 🏁 Script executed: #!/bin/bash
# Search for RecordCreated and RecordUpdated event definitions
echo "=== Searching for RecordCreated event definition ==="
rg -n "class RecordCreated" --type php -A 10
echo ""
echo "=== Searching for RecordUpdated event definition ==="
rg -n "class RecordUpdated" --type php -A 10Repository: pelican-dev/panel Length of output: 167 🏁 Script executed: #!/bin/bash
# Check how RecordCreated/RecordUpdated are dispatched to see actual parameters
echo "=== How RecordCreated is dispatched ==="
rg -n "RecordCreated::dispatch|dispatch.*RecordCreated" --type php -B 2 -A 2
echo ""
echo "=== How RecordUpdated is dispatched ==="
rg -n "RecordUpdated::dispatch|dispatch.*RecordUpdated" --type php -B 2 -A 2Repository: pelican-dev/panel Length of output: 141 🌐 Web query:
💡 Result: Filament doesn’t ship Laravel events named 1) Filament resource page hooks (recommended)Create page (
|
||
| ]; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: pelican-dev/panel
Length of output: 2420
🏁 Script executed:
Repository: pelican-dev/panel
Length of output: 2246
🏁 Script executed:
Repository: pelican-dev/panel
Length of output: 6736
🏁 Script executed:
#!/bin/bash cat -n tests/Filament/Admin/AdminActivityListenerTest.phpRepository: pelican-dev/panel
Length of output: 5485
🏁 Script executed:
Repository: pelican-dev/panel
Length of output: 1430
🌐 Web query:
Filament RecordCreated RecordUpdated event listener signature💡 Result:
Filament doesn’t expose Laravel events named
RecordCreated/RecordUpdatedthat you can register listeners for. Instead, record creation/update in a Resource page is meant to be customized via lifecycle hook methods (no parameters,voidreturn):Create page (
CreateRecord)(Also available:
beforeFill,afterFill,beforeValidate,afterValidate,beforeCreate,afterCreate.) [1]The created model is stored on the page as
$record(typeModel|null). [3]Edit page (
EditRecord)(Also available:
beforeFill,afterFill,beforeValidate,afterValidate,beforeSave,afterSave.) [2]If you truly need Laravel-style events, dispatch your own Laravel events from
afterCreate()/afterSave()and then use the normal listener signaturepublic function handle(YourEvent $event): void.Sources: [1] [2] [3]
🏁 Script executed:
Repository: pelican-dev/panel
Length of output: 2262
🌐 Web query:
Filament 4.5 Events RecordCreated RecordUpdated Laravel event listener💡 Result:
Filament v4.x (including 4.5) does not expose global Laravel events named
RecordCreated,RecordUpdated, etc. for Resource page CRUD in a way you can listen to viaEventServiceProvider.What to use instead
1) Filament Resource page lifecycle hooks (recommended for “only when done via Filament UI”)
On Create pages you have hooks like
beforeCreate()/afterCreate(); on Edit pages you havebeforeSave()/afterSave(). Use these hooks, and if you need a Laravel event, dispatch your own from there. [1] [2]Then register normal Laravel listeners for
PostCreatedFromFilament/PostUpdatedFromFilament.2) Eloquent model events / observers (recommended for “no matter where it’s created/updated”)
If your goal is “whenever the model changes (Filament or not)”, use Eloquent
created/updated(Observer) instead of Filament-specific hooks.Sources: Filament Resource Create lifecycle hooks (includes
afterCreate). [1] Filament Resource Edit lifecycle hooks (includesafterSave). [2][1] https://docs.laravel-filament.cn/docs/panels/resources/creating-records/
[2] https://docs.laravel-filament.cn/docs/4.x/resources/editing-records
This listener will never be triggered in production — Filament v4.5 does not expose
RecordCreated/RecordUpdatedas dispatchable Laravel events.The code registers
AdminActivityListenerfor these non-existent events inEventServiceProvider. Filament 4.5 uses lifecycle hook methods (afterCreate(),afterSave()) on Resource pages instead. If you need to log record creation/updates:created,updated) if activity should be logged regardless of how records are created.The listener tests pass only because they manually invoke
handle()directly—they don't validate that the listener is actually called through the event system.🤖 Prompt for AI Agents