chelout/laravel-relationship-events
Adds the missing relationship events to Laravel Eloquent models. Traits for hasOne, belongsTo, hasMany, belongsToMany, morphOne, morphTo, morphMany, morphToMany, and morphedByMany let you listen for saved, updated, attached, detached, and other events, useful for cache invalidation and audit logs.
Architecture fit. The package fills a genuine gap in Eloquent: Laravel fires model lifecycle events (creating, saved, deleted) but has no first-class hooks for relationship operations such as attach, detach, sync, associate, dissociate, or save on a relation. In our architecture, that gap is usually filled either by explicit domain events in service/action classes or by ad-hoc observer logic scattered across models. This package offers a third option — declarative, per-relation-type event hooks registered on the model itself — that fits cleanly into a standard Laravel domain layer without introducing a service container binding, config file, or schema change. For teams already using model observers (documented pattern with HasRelationshipObservables), it extends an existing convention rather than introducing a new one. Best fit: small-to-mid Laravel apps with pivot-heavy domains (RBAC, tagging, memberships) where auditing and side effects (cache invalidation, indexing, notifications) need to fire on relationship mutation.
Integration feasibility. Low-effort, additive integration. composer require plus a trait per model per relation type (HasOneEvents, HasBelongsToManyEvents, HasMorphManyEvents, and six more), with listeners registered in boot() or via observer classes. Optional HasDispatchableEvents lets us route to real event classes via $dispatchesEvents, which is the path a TPM should standardize on — it keeps listeners queueable, testable, and decoupled from the model. No migrations, no config publishing, no service provider registration required. The compatibility matrix is explicit and tracks Laravel majors (v5.x→Laravel 13/PHP 8.3, v4.x→12, v3.x→11, v2.x→10), which means version pinning maps 1:1 to our framework upgrade train.
Technical risk. The core risk is that the package achieves this by swapping in its own relationship classes, which means it sits on top of Eloquent internals rather than a public extension point. That makes it sensitive to framework-level refactors in HasOneOrMany, BelongsToMany, and MorphTo. The package has kept pace through five Laravel majors, which is reassuring, but it is a single-maintainer project (526 stars, 0 dependents on Packagist) — no downstream packages exercise it, so we would be among the more demanding consumers. Secondary risks: the README still lists "Tests, tests, tests" as a TODO, so coverage depth is unverified and we should treat correctness of edge paths (custom pivot models, syncWithoutDetaching, toggle, updateExistingPivot, chunked/collection attaches) as unproven until we test them. Documentation has visible defects (the belongsTo example in the many-to-many doc labels hasManySaving/hasManySaved for a belongsTo save; the morphTo doc is truncated), which signals docs are not carefully curated — budget for empirical verification. There is also documented extra query cost: detach/sync/toggle issue an additional query to resolve affected IDs, and HasMany::update loads a collection, both of which can become N+1 or memory hotspots in bulk operations if listeners are naive.
Key questions.
DB::table, queued jobs, imports), which no Eloquent-layer package can give?Relation subclasses, relation macros, packages that patch BelongsToMany)? Any collision here is a hard blocker.attach, sync, syncWithoutDetaching, toggle, updateExistingPivot, pivot-model events)? This defines the test matrix.Stack fit. Native fit for a conventional Laravel monolith or modular monolith on Laravel 10–13. Trait-based, opt-in per model, no framework-level patching, no global middleware, no runtime bootstrapping beyond the trait. Compatible with standard observer registration in AppServiceProvider::boot() and with Laravel's event dispatcher, so listeners can implement ShouldQueue and use the normal queue/retry infrastructure. It does not require or conflict with Redis, Horizon, Octane semantics beyond normal model boot, or our existing observability stack.
Migration path. Entirely additive and reversible on a per-model basis: add trait → register listeners → ship. No schema change, no data backfill, no dual-write period. Removal is equally mechanical (drop trait and listeners), making this a low-commitment decision we can pilot and roll back. Note that behavior added before any listener exists is a no-op, so incremental adoption carries no risk of silent behavior change.
Compatibility. Pin the package minor version to our Laravel major (v4.x on Laravel 12, v5.x on Laravel 13) and add it to the framework-upgrade checklist alongside our own code. PHP floor rises to 8.2/8.3 depending on branch — confirm against our deployment images before adopting v5.x. Verify coexistence with any package that decorates Eloquent relations (activity logging, auditing, tenancy packages that scope relations) during the pilot, not after.
Sequencing.
User role assignments), and empirically validate attach/detach/sync/toggle/updateExistingPivot event payloads and ordering against our real relation definitions, including a custom Pivot model.HasDispatchableEvents + dedicated event classes, not closures in boot(). Closures are convenient but untestable in isolation and invisible to static analysis; a TPM should reject them as the house style before they spread.afterCommit and a feature-flagged rollout.sync of large ID sets, HasMany::update).Maintenance. Low ongoing operational burden: no daemon, no storage, no config drift. The real maintenance cost is upgrade-coupled — each Laravel major requires a coordinated package bump, and we should assume a lag between a new Laravel release and a compatible tag. Budget for the possibility of pinning Laravel one minor behind, or of a short vendor fork if a break lands mid-cycle. Because we are adopting a low-adoption dependency, assign a named owner for tracking upstream releases.
Support. Community-supported MIT project with a single primary maintainer; no vendor SLA. Internal support must come from our own docs: a short internal note specifying which models use which traits, which event classes exist, and the exact listener contract. The upstream docs are usable but demonstrably imperfect, so do not treat them as the source of truth — our tests should be.
Scaling. Event firing is per-operation and synchronous by default, so listener cost lands on the request path. Queue all non-trivial listeners (ShouldQueue) to keep write latency flat, and watch the two documented extra-query paths: detach/sync/toggle resolve affected IDs with an additional query, and update materializes a collection. In bulk sync operations (hundreds of IDs) this can turn a single statement into a fan-out of events and queries; enforce chunking and cap listener work per event. Under Octane, verify no static state accumulates in the relation subclasses.
Failure modes.
sync and toggle fire both the aggregate event and the underlying attach/detach events — listeners must be idempotent or explicitly scoped to one level.insert, raw queries, DB::table pivot writes, imports) produce no events, so any audit trail built solely on this package is inherently incomplete.morphTo/belongsTo marks the related model dirty and requires a subsequent save; listeners that act on the association may observe pre-save state.Ramp-up. Very low for any Laravel developer — an hour to read the docs and mirror an existing model's pattern. The learning curve is not the API but the event-naming matrix (nine traits × creating/created/saving/saved/attaching/attached/detaching/detached/associating/associated/updating/updated). Mitigate with a one-page internal reference table and two or three canonical examples in the codebase rather than links to upstream docs. Estimated onboarding cost: half a day for one engineer to become the internal owner.
How can I help you explore Laravel packages today?