Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Action Event Columns Laravel Package

bbs-lab/nova-action-event-columns

A Laravel Nova package that adds action event columns to your resources, making action and event data easier to display, scan, and review directly in Nova tables. Useful for quick visibility into recorded action activity and debugging workflows without writing custom column code.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require <vendor>/nova-action-event-columns   # check README for the exact vendor/name
php artisan vendor:publish --provider="Vendor\NovaActionEventColumns\NovaActionEventColumnsServiceProvider"
php artisan migrate

You get three publishables — the migration, a custom-column migration stub, and a config file. Start by reading the config file; it is the entire surface area of the package.

2. Register the resource

Add the package's ActionResource to NovaServiceProvider::resources() (or Nova::resources([...])) so the action_events table becomes browsable in Nova with the extra columns rendered. Without this you have columns in the DB and nothing to look at.

3. First use case — turn on the IP column

Flip the ip_address toggle in the config, run any Nova action (a delete is the quickest), then open the action events resource. You should see request()->ip() recorded against the row. That single toggle is the fastest way to verify the package is wired correctly before you register anything custom.

Order matters: publish + migrate before enabling a column in config/registry. A registered column with no matching DB column will fail writes (see Gotchas).

Implementation Patterns

Registering your own column. The registry is the package's main extension point — a name, a value resolver, and an optional Nova field factory:

// NovaServiceProvider::boot() (or a dedicated provider)
NovaActionEventColumns::column('tenant_id')
    ->resolve(fn () => tenant('id'))                 // runs on every write path
    ->field(fn () => BelongsTo::make('Tenant')->readOnly());

Omit ->field() and the resource falls back to a sensible read-only display field — fine for user_agent, trace_id, or other "just show me the value" columns. Provide a field factory when you want sortable(), filterable(), or a relationship.

Add the SQL column with the published stub. The stub exists so you don't hand-write an alter table. Publish it, name the column, migrate, then register the resolver. Keep custom columns nullable or give them a default.

Resolvers must be context-only, not model-only. The package fills columns from a creating hook on the ->save() paths and an insert() override on the mass-insert paths (deletes, force-deletes, restores, queued jobs, and every custom action). The insert() path has no model instance — so a resolver that reads $event->actionable will work for updates and explode on a bulk delete. Resolve from the request, the authenticated user, or cached app state instead:

->resolve(fn () => auth()->id())                     // safe on both paths
->resolve(fn () => app(TenantContext::class)->id())  // safe on both paths

Queued and console actions. Delete/restore jobs run on the queue, where request() is a CLI request. ip_address will record 127.0.0.1 (or empty) rather than the user who queued the action. If the IP matters for audit purposes, capture it into the action's meta at dispatch time and resolve from there instead of relying on the built-in.

Bulk actions are N rows, N resolver calls. A bulk delete of 500 models calls your resolver 500 times. Keep resolvers cheap and memoise anything expensive at the top of the request (tenant lookup, feature flags) rather than querying per row.

Retention on a schedule. Add the prune command to your scheduler so the table doesn't grow unbounded:

Schedule::command('action-events:prune --days=90')->daily();

--hours and --all are there for aggressive cleanups; --all truncates and prompts in production.

Don't touch your actions. The value here is that existing Action classes, Resource::actions(), and Nova's built-in delete/restore flows need zero changes — columns are filled below the action layer. If you find yourself editing an action to populate an audit column, you've missed the point of the package.

Gotchas and Tips

  • Nova 4 pins you to PHP ≤ 8.4 and Laravel 11. Through its Inertia dependency, Nova 4 can't go further. On PHP 8.5 or Laravel 12/13 you must be on Nova 5. Plan the Nova upgrade before the PHP upgrade, not after.
  • Register-then-migrate is the classic footgun. Enabling a column in config before the DB column exists breaks every action write, not just the ones you're testing. Migrate first, deploy config second.
  • Non-nullable columns without defaults will take down unrelated actions. These columns are written on every action event globally — a bad column definition doesn't fail your new feature, it fails deletes in a resource you've never opened. Nullable, always.
  • insert() bypasses Eloquent events. If you have your own observers or ActionEvent::created listeners, they will not fire on delete/restore/bulk paths. Anything hanging off model events on action_events is unreliable now.
  • Raw inserts elsewhere aren't hooked. If some other package or a legacy job writes to action_events via DB::table('action_events')->insert(...) from its own code path, the override may not apply. Grep for action_events before you assume full coverage.
  • The prune command is irreversible. Run --days=N against a scratch copy first and confirm the boundary — off-by-one on "90 days" is a support ticket either way. In scheduled/production contexts, make sure the confirmation prompt is satisfied non-interactively or the job silently hangs.
  • Field factories are presentation, resolvers are truth. Don't put computation in the field factory; it only renders the resource and won't affect what's stored.
  • Debugging a null column: check (1) the column exists in the DB, (2) the resolver is registered in a provider that actually boots, (3) whether the write path was save() or insert() — meta/changes populated means save(), a bare row with only IDs means the mass-insert path. Then (4) whether it ran on a queue worker and got a CLI request.
  • Don't inherit the coverage claim. 100% line coverage and high MSI apply to the package's code, not your resolvers. Write one integration test that runs an action and asserts the row — that's the only thing that catches a resolver that's context-dependent and broken in production.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo