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

Laravel Pivot Events Laravel Package

genealabs/laravel-pivot-events

Adds pivot table events to Laravel many-to-many relationships, firing PivotAttached, PivotDetached, PivotUpdated, and PivotSaved so you can hook into attach, detach, and update operations using listeners, observers, or custom pivot models.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit. This package solves a real gap rather than a cosmetic one: Laravel's attach(), detach(), sync(), and updateExistingPivot() deliberately bypass Eloquent model events because they write to the join table, not the model. Any domain where the pivot is the business object — role grants, team membership, entitlement assignment, tagging, project assignment — therefore has no first-class observation point today. The package closes that with a trait (PivotEventsTrait) that overrides newPivot() to return a package-provided Pivot subclass, and a vocabulary of six-plus events (pivotAttaching, pivotAttached, pivotDetaching, pivotDetached, pivotUpdating, pivotUpdated, plus pivotSynced). This is a small, comprehensible seam — one trait, one class override, string-named Eloquent events — and it composes with existing listener infrastructure (Event::listen, Model::booted(), Event::fake()). It does not patch the framework core, which is the single most important architectural property in its favour.

Strategic value. The strongest argument is not the events themselves but the epics it unlocks cheaply: audit/compliance trails for access review (SOC 2, GDPR erasure scope), cache and search-index invalidation for permission-derived datasets, activity feeds and customer-facing webhooks, seat-based billing accuracy, and warehouse/CRM propagation without standing up CDC. Each of these is a roadmap item that would otherwise require bespoke afterSave code scattered across a dozen models, or a much larger CDC investment. Build-vs-buy is genuinely favourable: in-house is roughly 1–2 days of engineering plus indefinite maintenance against Laravel's InteractsWithPivotTable internals. Buying at MIT with a vendoring option is the cheaper path, provided we accept the maintenance seam as a named risk rather than an implicit one.

Integration feasibility. Low-to-moderate. Install is a single composer require; wiring is one use statement per model. The genuine complexity is organisational, not technical: the trait must be applied to both sides of every belongsToMany relationship we care about, because newPivot() only fires on the side where the trait lives. Trait on User alone means $user->roles()->attach() emits events but $role->users()->attach() silently does not. That half-failure mode is the package's dominant support burden and is the primary delivery risk: it is invisible at code-review time and only surfaces as "the listener didn't fire" months later. Mitigation is mechanical — a required-application checklist, a static analysis or test that asserts the trait on both endpoints per relationship — not inspirational.

Technical risk. Four risks are material enough to name in a design review:

  • Write-path performance regression. Forcing a custom pivot class converts attach()'s single bulk insert() into one save() per row. On a 10k-row sync() this is measurable. This is not a reason to reject the package; it is a reason to scope it to human-scale and administrative writes and leave bulk import paths on stock behaviour.
  • Silent semantic drift in minor releases. 13.1.0 suppresses pivotSynced and pivotDetached when no change occurs. That is a good behaviour change — it eliminates pointless cache flushes and spurious seat events — but it is an event-volume semantics change shipped in a minor. Any audit design that recorded intent ("attempted detach of role 5") rather than outcome silently changed meaning on a patch bump. Pin a minor version and treat event semantics as part of the contract under test.
  • Shadowing and duplication. Laravel's native ->using(MyPivot::class) already routes pivot writes through a real model, and permission/role libraries frequently ship their own pivot classes. Running both mechanisms on one relationship produces duplicate or missing events. The rule must be one mechanism per relationship, chosen explicitly and documented in a comment.
  • Ecosystem immaturity signal. 141 stars and 0 dependents is a thin external validation base. Last release 2026-02-28 suggests active maintenance, but there is no downstream population to absorb breakage ahead of us. This argues for vendoring or a fork-ready posture, and for keeping our usage surface narrow (one trait, listeners registered centrally) so that replacing it is a contained exercise.

Key questions for engineering before commitment.

  1. Which specific belongsToMany relationships carry business meaning, and what is the write volume per day on each? Anything with bulk import traffic gets excluded from phase one.
  2. Do any of those relationships currently use ->using() or a third-party pivot class (Spatie permissions, role libraries)? If yes, do we standardise on that mechanism and drop this package for that relation?
  3. What is the actual listener argument type for our installed version — parent model or Pivot instance? This determines the shape of every audit and cache-invalidation handler and must be confirmed by inspection, not assumption.
  4. Do we need intent-level audit (attempts) or outcome-level audit (actual changes)? 13.1.0 forces this decision explicitly; intent must be captured at the call site.
  5. Are any listeners queued? If so, pivot attributes (granted_at, assigned_by, expires_at) must be snapshotted into the payload before dispatch, because ShouldQueue serialises models by ID only.
  6. Who owns the upgrade obligation when Laravel majors move InteractsWithPivotTable?

Integration Approach

Stack fit. Clean, provided we are on a supported Laravel major and PHP version — the 13.x line tracks modern Laravel and the license is MIT, so there is no procurement or legal friction. The package is idiomatic Laravel all the way down: Eloquent events, service-provider listeners, Event::fake() in tests. It requires no framework core changes, no custom service container bindings, and no configuration file. That means it slots into an existing application without altering deployment topology, queue configuration, or observability setup. Fit is strongest where pivots already carry domain meaning and weakest where the relationship is pure reference data.

Migration path. Non-breaking and incremental by construction. The trait is additive; removing it restores stock behaviour with no data migration, no schema change, and no backfill. Nothing about the pivot tables themselves changes. This is an unusually forgiving adoption profile: we can enable it per-model, per-relationship, and roll back per-model if a hot path misbehaves. The only truly irreversible decision is the audit data we begin writing — once we start persisting pivot audit rows, the downstream consumers (access reviews, compliance exports) create expectations. So the migration is technically free but operationally sticky, and should be sequenced as such.

Compatibility. Two compatibility surfaces need explicit handling. First, third-party packages that supply their own pivot classes — the trait's newPivot() override can either be shadowed by theirs or stomp on theirs. Resolve per relationship with $relation->getPivotClass() as the diagnostic, and record the decision. Second, Laravel minor and major upgrades: the package tracks InteractsWithPivotTable internals, so every Laravel upgrade is a re-validation event for our pivot-event tests, not a routine bump. We should pin the package to a minor in composer.json and hold the pin deliberately.

Sequencing. Recommended four-stage rollout, each stage independently valuable and independently reversible:

  1. Discovery and observability (days). Install, apply the trait to both sides of one low-risk relationship, and log ten seconds of Event::listen('eloquent.*', ...) output. This confirms actual event names, firing order, and listener argument types for the installed version. Cheap, and it prevents designing against assumptions.
  2. Single high-value consumer (1–2 weeks). Pick one consumer — cache invalidation is the best first choice because it is high value, low blast radius, and easy to verify. Ship listeners in a service provider, not scattered across models, and add regression tests for the 13.1.0 suppression behaviour (assertNotDispatched after a no-op sync() and after detaching absent rows).
  3. Audit trail (parallel or next). Add the audit table and listeners. Decide intent-vs-outcome explicitly and document it. Snapshot queued payloads. Gate behind a feature flag if compliance consumers are external.
  4. Broaden and instrument. Extend to remaining business-meaningful relationships, excluding bulk-import paths. Add a CI guard that fails when a new belongsToMany relationship of a flagged type lacks the trait on both endpoints.

The critical sequencing constraint: do not start with the audit trail across all models. Start with one relationship and one consumer, prove the event semantics against the installed version, then generalise. The failure mode we are buying insurance against is a broad rollout that discovers the trait-on-one-side problem in production.

Operational Impact

Maintenance. Low absolute effort, but it is a pinned dependency with a narrow upstream, not a fire-and-forget library. Ongoing obligations are: re-running pivot-event tests on every Laravel upgrade and every package bump; reviewing release notes specifically for event-volume semantics changes (13.1.0 is the precedent); and monitoring whether the maintainer continues shipping. With 0 dependents there is no ecosystem buffer, so we should assume we may need to fork. Keeping our usage surface to one trait plus centrally-registered listeners makes a fork or replacement a contained, day-scale exercise rather than an archaeology project. Recommend vendoring or a documented fork path as a standing option rather than a contingency to improvise later.

Support. The support burden concentrates in one place: trait applied to one side only, which presents as "the listener doesn't fire" and consumes disproportionate debugging time. A second, growing source is the new ambiguity introduced by 13.1.0 — "my listener didn't fire" now means either broken or nothing changed. Both are addressable with cheap tooling: a ten-second Event::listen('eloquent.*') logger in the runbook, and a documented decision tree ("run the wildcard logger before hunting for a trait/using() conflict"). Team-level mitigation is a written convention — trait on both sides by default, one pivot mechanism per relationship, listeners in providers not models — plus the CI guard mentioned above. Third-party-library owners (permissions, roles) must be consulted early; their pivot classes are the main external support dependency.

Scaling. Two distinct scaling concerns. Write-path: attach() becomes N save() calls instead of one bulk insert(). On administrative and human-scale writes this is negligible; on a 10k-row sync() it is a real regression. Scope the package to business-meaningful pivots and keep high-volume import paths on stock Laravel behaviour — this is a design decision, not a tuning exercise, and it should be written down. Listener fan-out: attach([1..50]) dispatches fifty event pairs. A listener that performs a query or HTTP call per event will collapse. Mitigation is ours, not the package's: buffer into a DB::afterCommit flush, or suppress per-row handling for bulk operations and emit one domain event after the loop. 13.1.0 thins redundant events; it does nothing for genuinely large ones. Note also that 13.1.0's change-only emission is a measurable reduction in background-job cost on our most common write pattern (re-saving unchanged memberships), which partially offsets the write-path regression in aggregate.

Failure modes. Rank by likelihood × impact:

  • Half-wired trait (high likelihood, moderate impact). Events silently missing on one direction of a relationship. Detected only by absence. Mitigate with a both-sides convention, the wildcard logger, and a CI check.
  • Recursion (low likelihood, severe impact). Writing to the pivot table from inside a pivot listener — $model->roles()->attach(...) inside a pivotAttached handler — is unbounded recursion. Enforce as a hard review rule.
  • Atomicity gaps (moderate likelihood, moderate impact). Pivot events fire immediately around the write, so listener side effects are only atomic with the pivot change if the transaction wraps the write. Queued listeners run after commit and outside that guarantee — design them for at-least-once, not exactly-once, and make downstream consumers idempotent.
  • Queued payload loss (moderate likelihood, moderate impact). Pivot attributes read back inside handle() will be absent because models are serialised by ID. Snapshot before dispatch.
  • Duplicate/missing events from ->using() conflict (moderate likelihood, moderate impact). Per-relationship mitigation: pick one mechanism, document it.
  • Semantics drift on upgrade (low likelihood, high impact if audit is live). A minor bump changes what gets emitted, and compliance data silently changes meaning. Mitigate with a pinned minor and regression tests covering suppression.
  • Abandonment (low likelihood, moderate impact). Mitigated by narrow surface and a rehearsed fork path.

Ramp-up. Effectively immediate for a competent Laravel engineer — install, trait, listener, done in an afternoon. The README is short and the source shorter; reading src/Pivot.php (a few dozen lines) is the single highest-leverage onboarding step, because it is the source of truth for event names, event subject type, whether …ing events honour a false return, and exactly when events are suppressed. Budget half a day of onboarding per engineer and require two artefacts before any production listener ships: a dd() of the listener argument, and ten seconds of wildcard event log captured against the installed version. For the TPM: the ramp-up cost is not in learning the package, it is in learning which of our relationships are pivot-bearing and business-meaningful — that inventory work is the real prerequisite and should be scheduled explicitly rather than assumed.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib