feat: Add pivot model events support for BelongsToMany and MorphToMany relationships #325
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds support for firing model events on custom pivot models when using
->using()on BelongsToMany and MorphToMany relationships, matching Laravel's behavior.The Problem
When using a custom pivot model via
->using(CustomPivot::class)on a BelongsToMany or MorphToMany relationship, Hyperf uses raw queries forattach(),detach(), andupdateExistingPivot(). This means model events (creating,created,deleting,deleted,saving,saved, etc.) never fire on the pivot model.The Solution
Following Laravel's implementation pattern, this PR:
Creates
InteractsWithPivotTabletrait - Overridesattach(),detach(),updateExistingPivot(), andnewPivot()to check if$this->usingis set. When a custom pivot class is specified, operations use model methods (save(),delete()) instead of raw queries, enabling model events to fire.See Laravel's InteractsWithPivotTable
Updates
BelongsToMany- Uses the new trait.Updates
MorphToMany- Uses the new trait, with an override ofgetCurrentlyAttachedPivots()to properly set morph type/class on the pivot models (matching Laravel's pattern).Fixes
Pivotclass - Two issues were discovered and fixed:PivotextendsHyperf\Database\Model\Relations\Pivotwhich extendsHyperf\Database\Model\Modeldirectly. Unlike regular models (which go throughHyperf\DbConnection\Model\Model), it didn't have theHasContainertrait that providesgetEventDispatcher()from the container. AddedHasContainerto fix this.AsPivot::delete()only fires events when the pivot has a primary key. Most pivot tables use composite foreign keys instead. Added adelete()override that manually firesdeleting/deletedevents for composite key pivots (matching Laravel's behavior).Fixes
MorphPivotclass - Same issues asPivot, plus:HasContainerandHasCallbackstraitsdelete()to fire events AND include the morph type constraint (so only the correct polymorphic records are deleted)Adds
HasCallbacks- EnablesregisterCallback()on pivot models for event registration.Performance Consideration
This is opt-in and preserves existing performance characteristics:
->using()->using(CustomPivot::class)save()/delete()- events fireUsers choose the tradeoff. If you need pivot model events, use
->using(). If you need maximum performance and don't need events, don't use->using().Example Usage
BelongsToMany
MorphToMany
Files Changed
src/core/src/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php(new)src/core/src/Database/Eloquent/Relations/BelongsToMany.php(modified)src/core/src/Database/Eloquent/Relations/MorphToMany.php(modified)src/core/src/Database/Eloquent/Relations/Pivot.php(modified)src/core/src/Database/Eloquent/Relations/MorphPivot.php(modified)tests/Core/Database/Eloquent/Relations/BelongsToManyPivotEventsTest.php(new)tests/Core/Database/Eloquent/Relations/MorphToManyPivotEventsTest.php(new)tests/Core/Database/Eloquent/Relations/migrations/(new/modified)Tests
25 tests covering:
BelongsToMany (13 tests):
attach()- single and multiple, with and without custom pivotdetach()- single, multiple, and all, with and without custom pivotupdateExistingPivot()- fires events when dirty, skips when not dirtysync()- fires events for attach, detach, and update operationstoggle()- fires events for both attach and detachMorphToMany (12 tests):
attach()- single and multiple, with and without custom morph pivotdetach()- single, multiple, and all, with and without custom morph pivotupdateExistingPivot()- fires events when dirty, skips when not dirtysync()- fires events for attach, detach, and update operationstoggle()- fires events for both attach and detach