diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php index 7fe75e6d4be5..b77d7db80d82 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php @@ -42,6 +42,10 @@ public static function fromAttributes(Model $parent, $attributes, $table, $exist { $instance = new static; + // if this factory was presented valid timestamp columns, set the $timestamps + // property accordingly + $instance->timestamps = $instance->hasTimestampAttributes($attributes); + // The pivot model is a "dynamic" model since we will set the tables dynamically // for the instance. This allows it work for any intermediate tables for the // many to many relationship that are defined by this developer's classes. @@ -57,8 +61,6 @@ public static function fromAttributes(Model $parent, $attributes, $table, $exist $instance->exists = $exists; - $instance->timestamps = $instance->hasTimestampAttributes(); - return $instance; } @@ -75,9 +77,11 @@ public static function fromRawAttributes(Model $parent, $attributes, $table, $ex { $instance = static::fromAttributes($parent, [], $table, $exists); - $instance->setRawAttributes($attributes, true); + // if this factory was presented valid timestamp columns, set the $timestamps + // property accordingly + $instance->timestamps = $instance->hasTimestampAttributes($attributes); - $instance->timestamps = $instance->hasTimestampAttributes(); + $instance->setRawAttributes($attributes, true); return $instance; } @@ -110,11 +114,22 @@ protected function setKeysForSaveQuery(Builder $query) */ public function delete() { + // support for pivot classes that container a non-composite primary key if (isset($this->attributes[$this->getKeyName()])) { - return parent::delete(); + return (int) parent::delete(); } - return $this->getDeleteQuery()->delete(); + if ($this->fireModelEvent('deleting') === false) { + return 0; + } + + $this->touchOwners(); + + $affectedRows = $this->getDeleteQuery()->delete(); + + $this->fireModelEvent('deleted', false); + + return $affectedRows; } /** @@ -193,13 +208,15 @@ public function setPivotKeys($foreignKey, $relatedKey) } /** - * Determine if the pivot model has timestamp attributes. + * Determine if the pivot model has timestamp attributes in either a provided + * array of attributes or the currently tracked attributes inside the model. * + * @param $attributes array|null Options attributes to check instead of properties * @return bool */ - public function hasTimestampAttributes() + public function hasTimestampAttributes($attributes = null) { - return array_key_exists($this->getCreatedAtColumn(), $this->attributes); + return array_key_exists($this->getCreatedAtColumn(), $attributes ?? $this->attributes); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index 1984ec690611..2cd6abc9f9e0 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -184,6 +184,19 @@ protected function attachNew(array $records, array $current, $touch = true) */ public function updateExistingPivot($id, array $attributes, $touch = true) { + if ($this->using) { + $updated = $this->newPivot([ + $this->foreignPivotKey => $this->parent->getKey(), + $this->relatedPivotKey => $this->parseId($id), + ], true)->fill($attributes)->save(); + + if ($touch) { + $this->touchIfTouching(); + } + + return (int) $updated; + } + if (in_array($this->updatedAt(), $this->pivotColumns)) { $attributes = $this->addTimestampsToAttachment($attributes, true); } @@ -209,12 +222,21 @@ public function updateExistingPivot($id, array $attributes, $touch = true) */ public function attach($id, array $attributes = [], $touch = true) { - // Here we will insert the attachment records into the pivot table. Once we have - // inserted the records, we will touch the relationships if necessary and the - // function will return. We can parse the IDs before inserting the records. - $this->newPivotStatement()->insert($this->formatAttachRecords( - $this->parseIds($id), $attributes - )); + if ($this->using) { + $records = $this->formatAttachRecords( + $this->parseIds($id), $attributes + ); + foreach ($records as $record) { + $this->newPivot($record, false)->save(); + } + } else { + // Here we will insert the attachment records into the pivot table. Once we have + // inserted the records, we will touch the relationships if necessary and the + // function will return. We can parse the IDs before inserting the records. + $this->newPivotStatement()->insert($this->formatAttachRecords( + $this->parseIds($id), $attributes + )); + } if ($touch) { $this->touchIfTouching(); @@ -355,26 +377,36 @@ protected function hasPivotColumn($column) */ public function detach($ids = null, $touch = true) { - $query = $this->newPivotQuery(); + if ($this->using) { + $results = 0; + foreach ($this->parseIds($ids) as $id) { + $results += $this->newPivot([ + $this->foreignPivotKey => $this->parent->getKey(), + $this->relatedPivotKey => $id, + ], true)->delete(); + } + } else { + $query = $this->newPivotQuery(); + + // If associated IDs were passed to the method we will only delete those + // associations, otherwise all of the association ties will be broken. + // We'll return the numbers of affected rows when we do the deletes. + if (! is_null($ids)) { + $ids = $this->parseIds($ids); - // If associated IDs were passed to the method we will only delete those - // associations, otherwise all of the association ties will be broken. - // We'll return the numbers of affected rows when we do the deletes. - if (! is_null($ids)) { - $ids = $this->parseIds($ids); + if (empty($ids)) { + return 0; + } - if (empty($ids)) { - return 0; + $query->whereIn($this->relatedPivotKey, (array) $ids); } - $query->whereIn($this->relatedPivotKey, (array) $ids); + // Once we have all of the conditions set on the statement, we are ready + // to run the delete on the pivot table. Then, if the touch parameter + // is true, we will go ahead and touch all related models to sync. + $results = $query->delete(); } - // Once we have all of the conditions set on the statement, we are ready - // to run the delete on the pivot table. Then, if the touch parameter - // is true, we will go ahead and touch all related models to sync. - $results = $query->delete(); - if ($touch) { $this->touchIfTouching(); } diff --git a/src/Illuminate/Database/Eloquent/Relations/Pivot.php b/src/Illuminate/Database/Eloquent/Relations/Pivot.php index 2ec8235156bb..95799b8505af 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Pivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/Pivot.php @@ -9,6 +9,8 @@ class Pivot extends Model { use AsPivot; + public $incrementing = false; + /** * The attributes that aren't mass assignable. * diff --git a/tests/Database/DatabaseEloquentPivotTest.php b/tests/Database/DatabaseEloquentPivotTest.php index 5f461ea5111e..4f09aba25ee7 100755 --- a/tests/Database/DatabaseEloquentPivotTest.php +++ b/tests/Database/DatabaseEloquentPivotTest.php @@ -119,7 +119,8 @@ public function testDeleteMethodDeletesModelByKeys() $query->shouldReceive('delete')->once()->andReturn(true); $pivot->expects($this->once())->method('newQueryWithoutRelationships')->will($this->returnValue($query)); - $this->assertTrue($pivot->delete()); + $rowsAffected = $pivot->delete(); + $this->assertEquals(1, $rowsAffected); } public function testPivotModelTableNameIsSingular() diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php new file mode 100644 index 000000000000..a81ce022d683 --- /dev/null +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -0,0 +1,115 @@ +increments('id'); + $table->string('email'); + $table->timestamps(); + }); + + Schema::create('projects', function ($table) { + $table->increments('id'); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('project_users', function ($table) { + $table->integer('user_id'); + $table->integer('project_id'); + $table->string('role')->nullable(); + }); + } + + public function test_pivot_will_trigger_events_to_be_fired() + { + $user = PivotEventsTestUser::forceCreate(['email' => 'taylor@laravel.com']); + $user2 = PivotEventsTestUser::forceCreate(['email' => 'ralph@ralphschindler.com']); + $project = PivotEventsTestProject::forceCreate(['name' => 'Test Project']); + + $project->collaborators()->attach($user); + $this->assertEquals(['saving', 'creating', 'created', 'saved'], PivotEventsTestCollaborator::$eventsCalled); + + PivotEventsTestCollaborator::$eventsCalled = []; + $project->collaborators()->sync([$user2->id]); + $this->assertEquals(['deleting', 'deleted', 'saving', 'creating', 'created', 'saved'], PivotEventsTestCollaborator::$eventsCalled); + + PivotEventsTestCollaborator::$eventsCalled = []; + $project->collaborators()->sync([$user->id => ['role' => 'owner'], $user2->id => ['role' => 'contributor']]); + $this->assertEquals(['saving', 'creating', 'created', 'saved', 'saving', 'updating', 'updated', 'saved'], PivotEventsTestCollaborator::$eventsCalled); + } +} + +class PivotEventsTestUser extends Model +{ + public $table = 'users'; +} + +class PivotEventsTestProject extends Model +{ + public $table = 'projects'; + + public function collaborators() + { + return $this->belongsToMany( + PivotEventsTestUser::class, 'project_users', 'project_id', 'user_id' + )->using(PivotEventsTestCollaborator::class); + } +} + +class PivotEventsTestCollaborator extends Pivot +{ + public $table = 'project_users'; + + public static $eventsCalled = []; + + public static function boot() + { + parent::boot(); + + static::creating(function ($model) { + static::$eventsCalled[] = 'creating'; + }); + + static::created(function ($model) { + static::$eventsCalled[] = 'created'; + }); + + static::updating(function ($model) { + static::$eventsCalled[] = 'updating'; + }); + + static::updated(function ($model) { + static::$eventsCalled[] = 'updated'; + }); + + static::saving(function ($model) { + static::$eventsCalled[] = 'saving'; + }); + + static::saved(function ($model) { + static::$eventsCalled[] = 'saved'; + }); + + static::deleting(function ($model) { + static::$eventsCalled[] = 'deleting'; + }); + + static::deleted(function ($model) { + static::$eventsCalled[] = 'deleted'; + }); + } +}