ganyicz/nova-callbacks
Define Eloquent model event callbacks such as creating, updating, and deleting directly inside your Laravel Nova resources. Keeps event logic close to related fields and removes the need for separate observers.
What this package actually is. Roughly 100 lines of glue that hook Nova's resource store()/update()/destroy() flow and invoke named methods (afterCreate, afterUpdate, etc.) on the resource class with the affected model and NovaRequest. It adds no schema, no config, no routes, no assets. The scope is small enough that the source is more authoritative than the README, and small enough that the real question is not "does it work" but "who owns this code in two years."
Architecture fit. The package makes an implicit architectural bet: post-save side effects belong in the presentation layer (the Nova resource), not in model observers, domain events, or services. That is correct for a narrow and legitimate class of behavior — audit rows stamped with the acting admin, admin-scoped notifications, cache busts that only matter when a human edits a record. It is wrong for anything an API endpoint, console command, queue worker, or CSV import must also trigger. Adopting this package without drawing that line explicitly bakes a "why didn't this fire?" bug class into the codebase. Fit is therefore conditional: high for a small internal admin tool where Nova is the only write path; near-zero for a system where Nova is one of several mutation surfaces.
Integration feasibility. Installation is one composer require and a method on a resource class. The real friction is not technical, it is procurement and CI:
NOVA_USERNAME/NOVA_PASSWORD. Any team that has not already automated this will spend more time on CI secrets than on the package.composer require proves the constraint graph is satisfiable — it proves nothing about whether the callbacks still fire. That is a test, not a resolve.Technical risk. Ranked by expected cost:
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Silent breakage on Nova major upgrade | High | High — missing side effects on production writes, not a build error | Canary test per resource with a callback |
| Bulk paths (Actions, Lenses, CSV import) bypass the hooks | High | Medium — rows differ depending on how they were created | Test the import path explicitly; keep business rules out |
Method-override collisions with other Nova extensions wrapping store/update |
Medium | Medium — ordering depends on trait bootstrap order | Test combinations; limit to one such package per resource |
| No idempotency → duplicate side effects on user retry | Medium | Medium | Make every callback body idempotent or a single dispatch() |
| Single maintainer abandons it | Already true | Medium | Fork-and-vendor plan from day one |
| Nova core ships equivalent lifecycle hooks | Unknown | Would make the dependency redundant | Check the Nova changelog before committing |
Key questions to answer before any commitment.
composer.json permit, and does it resolve against our current pin today?->afterCommit() is required on dispatched jobs.store/update?cat vendor/ganyicz/nova-callbacks/src/*.php — five-minute read, more reliable than the README.)Technical evaluation bottom line. Small, well-shaped, low blast radius, and effectively unmaintained. Score 0.47 with 20 stars, zero dependents, and no release since June 2022 is the correct read: this is a pattern worth copying, not a dependency worth carrying. The capability is hours of work to replicate; the dependency is an unowned component in the admin write path.
Stack fit. Laravel 8/9-era, PHP 8.x, Nova 3/4-era. It fits cleanly if we are on a contemporaneous Nova major; it fits with unknown risk if we are on anything newer. The stack question that actually gates us is not Laravel — it is which Nova major we pin and whether we can run it in CI with credentials.
Compatibility policy: pin two dimensions. Nova major × Laravel major. Treat every Nova upgrade as a breaking change for this package specifically, regardless of what the changelog says. The failure mode is silent (side effects stop firing, saves still succeed), so the upgrade runbook entry needs a named owner and a re-validation step, not a note to "check things still work."
Migration path — abstract first, adopt second. Do not sprinkle package-specific method names across resource classes. Define one contract and implement it in a base resource:
abstract class Resource extends NovaResource // the only file that knows about the package
This is the difference between a one-file exit and a fifty-file mechanical edit. If we skip it, removal cost scales with the number of resources that ever touched a callback — which is exactly the number that will be largest when we need to remove it.
Sequencing. The order matters because each step is cheap and each can terminate the effort:
actingAs + PUT /nova-api/...), never by calling the method directly. A unit test of afterUpdate() proves nothing about whether the package invokes it.store()/update() wrapping; we are renting ergonomics, not capability.app/Support/, delete the composer requirement) is the realistic end state, not a fallback.Where the callbacks must route shared logic. Use a trait for behavior that recurs ("stamp updated_by, write an audit row"), mixed into resources that opt in. This also gives us the seam to swap the implementation when we inline.
Non-negotiable code-review rule. A callback body may do exactly two things: write to the same database, or dispatch() a queued job. Nothing else. External HTTP, mail, Slack, CRM sync — all of it goes behind a queue with ->afterCommit(), or a failing third-party endpoint 500s the admin's save.
Maintenance. Single maintainer, no release in ~3 years, zero dependents. We would be the only downstream consumer, which means we are the only ones who notice when it breaks and there is no community backpressure to produce a fix. Budget this as an owned fork, not a dependency with an upstream. The failure is not a red build; it is an audit row that quietly stops being written on a Tuesday.
Support. There are no logging or retry hooks in the package. If a callback throws, the only trace is an application exception and whatever the user saw. Wrap bodies in try/catch with Log::error(), or — better — keep the body to a single dispatch() and let the queue give us retries and a failed_jobs record for free. Do not let a Slack webhook 500 the admin's save.
Scaling. Callbacks execute inline in the Nova HTTP request, after the DB write, with no transaction and no retry. Every callback is added latency on every admin save. Trivial for an audit insert; unacceptable for anything that talks to a network. The scaling rule is the same as the review rule: write locally or dispatch. Anything that grows in duration must move to the queue before it grows in volume.
Failure modes to plan for.
ShouldBeUnique jobs, upsert on a natural key, Cache::lock on model + action).store/update. Records created by import or mass action will silently lack side effects. This is the highest-probability source of "why is this row different?" production bugs. Test the import path specifically.store/update (redirect-after-save, tabs), execution order is whichever bootstraps first. That is not a bug to debug from a support ticket. Test the combination before shipping it.$model is still readable at that point. Do not assume.Ramp-up. Engineering: negligible — reading the source is five minutes, and the first callback is an afternoon including the failing test. The real enablement cost is elsewhere:
Budget an enablement effort, not a spike. The spike is cheap; the convention is what we are actually buying, and we can buy it without the dependency.
Recommendation. Do not adopt as a production dependency. Use it as a reference implementation and a one-afternoon spike to make the requirement concrete and demoable, port the pattern into a first-party trait we control, and put the result behind an interface so the seam exists regardless. If it is already in the codebase, treat the finding as a maintenance signal: inline it on the next Nova upgrade, scheduled with an owner, rather than patching around it.
How can I help you explore Laravel packages today?