From cdd6f211722caadf731adb369be1c1c2fc0d4aa0 Mon Sep 17 00:00:00 2001 From: Jarek Tkaczyk Date: Wed, 24 Aug 2016 10:06:18 +0800 Subject: [PATCH 1/2] fire eloquent events on BelongsToMany operations (sync/attach/detach/toggle) + refactored brittle tests --- .../Eloquent/Relations/BelongsToMany.php | 149 +++++++++------ .../DatabaseEloquentBelongsToManyTest.php | 180 ++++++++++++++---- .../DatabaseEloquentMorphToManyTest.php | 1 + 3 files changed, 238 insertions(+), 92 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 08811b49698c..6fe44e78bff6 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Collection as BaseCollection; use Illuminate\Database\Eloquent\ModelNotFoundException; class BelongsToMany extends Relation @@ -801,7 +802,7 @@ public function createMany(array $records, array $joinings = []) * Each existing model is detached, and non existing ones are attached. * * @param mixed $ids - * @return array + * @return array|false */ public function toggle($ids) { @@ -809,28 +810,21 @@ public function toggle($ids) 'attached' => [], 'detached' => [], ]; - if ($ids instanceof Model) { - $ids = $ids->getKey(); - } - - if ($ids instanceof Collection) { - $ids = $ids->modelKeys(); - } - // First we will execute a query to get all of the current attached IDs for // the relationship, which will allow us to determine which of them will // be attached and which of them will be detached from the join table. - $current = $this->newPivotQuery() - ->pluck($this->otherKey)->all(); + $current = $this->newPivotQuery()->pluck($this->otherKey); + + $records = $this->formatRecordsList($ids); - $records = $this->formatRecordsList((array) $ids); + if ($this->fireParentEvent("toggling.{$this->relationName}", $records) === false) { + return false; + } // Next, we will determine which IDs should get removed from the join table // by checking which of the given ID / records is in the list of current // records. We will then remove all those rows from the joining table. - $detach = array_values(array_intersect( - $current, array_keys($records) - )); + $detach = $current->intersect($records->keys())->values()->all(); if (count($detach) > 0) { $this->detach($detach, false); @@ -841,7 +835,7 @@ public function toggle($ids) // Finally, for all of the records that were not detached, we'll attach the // records into the intermediate table. Then we'll add those attaches to // the change list and be ready to return these results to the caller. - $attach = array_diff_key($records, array_flip($detach)); + $attach = array_diff_key($records->all(), array_flip($detach)); if (count($attach) > 0) { $this->attach($attach, [], false); @@ -853,6 +847,8 @@ public function toggle($ids) $this->touchIfTouching(); } + $this->fireParentEvent("toggled.{$this->relationName}", collect($changes), false); + return $changes; } @@ -872,7 +868,7 @@ public function syncWithoutDetaching($ids) * * @param \Illuminate\Database\Eloquent\Collection|array $ids * @param bool $detaching - * @return array + * @return array|false */ public function sync($ids, $detaching = true) { @@ -880,18 +876,18 @@ public function sync($ids, $detaching = true) 'attached' => [], 'detached' => [], 'updated' => [], ]; - if ($ids instanceof Collection) { - $ids = $ids->modelKeys(); - } - // First we need to attach any of the associated models that are not currently // in this joining table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. - $current = $this->newPivotQuery()->pluck($this->otherKey)->all(); + $current = $this->newPivotQuery()->pluck($this->otherKey); $records = $this->formatRecordsList($ids); - $detach = array_diff($current, array_keys($records)); + if ($this->fireParentEvent("syncing.{$this->relationName}", $records) === false) { + return false; + } + + $detach = $current->diff($records->keys())->values()->all(); // Next, we will take the differences of the currents and given IDs and detach // all of the entities that exist in the "current" array but are not in the @@ -906,27 +902,29 @@ public function sync($ids, $detaching = true) // touching until after the entire operation is complete so we don't fire a // ton of touch operations until we are totally done syncing the records. $changes = array_merge( - $changes, $this->attachNew($records, $current, false) + $changes, $this->attachNew($records->all(), $current->all(), false) ); if (count($changes['attached']) || count($changes['updated'])) { $this->touchIfTouching(); } + $this->fireParentEvent("synced.{$this->relationName}", collect($changes), false); + return $changes; } /** * Format the sync/toggle list so that it is keyed by ID. * - * @param array $records - * @return array + * @param mixed $records + * @return \Illuminate\Support\Collection */ - protected function formatRecordsList(array $records) + protected function formatRecordsList($records) { $results = []; - foreach ($records as $id => $attributes) { + foreach ($this->parseRecordsArg($records) as $id => $attributes) { if (! is_array($attributes)) { list($id, $attributes) = [$attributes, []]; } @@ -934,7 +932,7 @@ protected function formatRecordsList(array $records) $results[$id] = $attributes; } - return $results; + return collect($results); } /** @@ -1010,36 +1008,56 @@ public function updateExistingPivot($id, array $attributes, $touch = true) /** * Attach a model to the parent. * - * @param mixed $id + * @param mixed $ids * @param array $attributes * @param bool $touch - * @return void + * @return null|false */ - public function attach($id, array $attributes = [], $touch = true) + public function attach($ids, array $attributes = [], $touch = true) { - if ($id instanceof Model) { - $id = $id->getKey(); - } + $attached = $this->createAttachRecords($ids, $attributes); - if ($id instanceof Collection) { - $id = $id->modelKeys(); + if ($this->fireParentEvent("attaching.{$this->relationName}", $attached) === false) { + return false; } - $query = $this->newPivotStatement(); - - $query->insert($this->createAttachRecords((array) $id, $attributes)); + $this->newPivotStatement()->insert($attached->all()); if ($touch) { $this->touchIfTouching(); } + + $this->fireParentEvent("attached.{$this->relationName}", $attached, false); + } + + /** + * Fire the given event for the parent model. + * + * @param string $event + * @param bool $halt + * @return mixed + */ + protected function fireParentEvent($event, $records, $halt = true) + { + $dispatcher = $this->getParent()->getEventDispatcher(); + + if (! $dispatcher) { + return true; + } + + $event = "eloquent.{$event}: ".get_class($this->getParent()); + + $method = $halt ? 'until' : 'fire'; + + return $dispatcher->$method($event, [$this->getParent(), $records]); } /** * Create an array of records to insert into the pivot table. * - * @param array $ids + * @param mixed $ids * @param array $attributes - * @return array + * @return \Illuminate\Support\Collection */ protected function createAttachRecords($ids, array $attributes) { @@ -1051,11 +1069,34 @@ protected function createAttachRecords($ids, array $attributes) // To create the attachment records, we will simply spin through the IDs given // and create a new record to insert for each ID. Each ID may actually be a // key in the array, with extra attributes to be placed in other columns. - foreach ($ids as $key => $value) { + foreach ($this->parseRecordsArg($ids) as $key => $value) { $records[] = $this->attacher($key, $value, $attributes, $timed); } - return $records; + return collect($records); + } + + /** + * Parse records argument for the relation attaching/detaching/syncing/toggling actions. + * + * @param mixed $records + * @return array + */ + protected function parseRecordsArg($records) + { + if ($records instanceof Model) { + $records = $records->getKey(); + } + + if ($records instanceof Collection) { + $records = $records->modelKeys(); + } + + if ($records instanceof BaseCollection) { + $records = $records->all(); + } + + return (array) $records; } /** @@ -1146,27 +1187,23 @@ protected function setTimestampsOnAttach(array $record, $exists = false) * * @param mixed $ids * @param bool $touch - * @return int + * @return int|false */ public function detach($ids = [], $touch = true) { - if ($ids instanceof Model) { - $ids = $ids->getKey(); - } - - if ($ids instanceof Collection) { - $ids = $ids->modelKeys(); - } - $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. - $ids = (array) $ids; + $ids = collect($this->parseRecordsArg($ids)); + + if ($this->fireParentEvent("detaching.{$this->relationName}", $ids) === false) { + return false; + } if (count($ids) > 0) { - $query->whereIn($this->otherKey, $ids); + $query->whereIn($this->otherKey, $ids->all()); } // Once we have all of the conditions set on the statement, we are ready @@ -1178,6 +1215,8 @@ public function detach($ids = [], $touch = true) $this->touchIfTouching(); } + $this->fireParentEvent("detached.{$this->relationName}", $ids, false); + return $results; } diff --git a/tests/Database/DatabaseEloquentBelongsToManyTest.php b/tests/Database/DatabaseEloquentBelongsToManyTest.php index 491852b3ef45..3dc3eec98b87 100755 --- a/tests/Database/DatabaseEloquentBelongsToManyTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyTest.php @@ -138,13 +138,12 @@ public function testEagerConstraintsAreProperlyAdded() public function testAttachInsertsPivotTableRecord() { - $relation = $this->getMockBuilder('Illuminate\Database\Eloquent\Relations\BelongsToMany')->setMethods(['touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); - $query = m::mock('stdClass'); - $query->shouldReceive('from')->once()->with('user_role')->andReturn($query); - $query->shouldReceive('insert')->once()->with([['user_id' => 1, 'role_id' => 2, 'foo' => 'bar']])->andReturn(true); - $relation->getQuery()->shouldReceive('getQuery')->andReturn($mockQueryBuilder = m::mock('StdClass')); - $mockQueryBuilder->shouldReceive('newQuery')->once()->andReturn($query); - $relation->expects($this->once())->method('touchIfTouching'); + $relation = $this->getRelation(); + $relation->getParent()->shouldReceive('touches')->andReturn(false); + $relation->getRelated()->shouldReceive('touches')->andReturn(false); + + $query = $this->getNewStatement($relation); + $query->shouldReceive('insert')->once()->with([['user_id' => 1, 'role_id' => 2, 'foo' => 'bar']]); $relation->attach(2, ['foo' => 'bar']); } @@ -609,53 +608,159 @@ public function testTouchIfTouching() $relation->touchIfTouching(); } - public function testSyncMethodConvertsCollectionToArrayOfKeys() + public function testSyncMethodAcceptsCollectionOfModels() { - $relation = $this->getMockBuilder('Illuminate\Database\Eloquent\Relations\BelongsToMany')->setMethods(['attach', 'detach', 'touchIfTouching', 'formatRecordsList'])->setConstructorArgs($this->getRelationArguments())->getMock(); - $query = m::mock('stdClass'); - $query->shouldReceive('from')->once()->with('user_role')->andReturn($query); - $query->shouldReceive('where')->once()->with('user_id', 1)->andReturn($query); - $relation->getQuery()->shouldReceive('getQuery')->andReturn($mockQueryBuilder = m::mock('StdClass')); - $mockQueryBuilder->shouldReceive('newQuery')->once()->andReturn($query); - $query->shouldReceive('pluck')->once()->with('role_id')->andReturn(new BaseCollection([1, 2, 3])); + $relation = $this->getMockBuilder('Illuminate\Database\Eloquent\Relations\BelongsToMany')->setMethods(['attach', 'detach', 'touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); + $query = $this->getNewQuery($relation); + $query->shouldReceive('pluck')->once()->with('role_id')->andReturn(new BaseCollection()); $collection = new Collection([ m::mock(['getKey' => 1]), m::mock(['getKey' => 2]), - m::mock(['getKey' => 3]), ]); - $relation->expects($this->once())->method('formatRecordsList')->with([1, 2, 3])->will($this->returnValue([1 => [], 2 => [], 3 => []])); - $relation->sync($collection); + $this->assertEquals(['attached' => [1, 2], 'detached' => [], 'updated' => []], $relation->sync($collection)); } public function testWherePivotParamsUsedForNewQueries() { - $relation = $this->getMockBuilder('Illuminate\Database\Eloquent\Relations\BelongsToMany')->setMethods(['attach', 'detach', 'touchIfTouching', 'formatRecordsList'])->setConstructorArgs($this->getRelationArguments())->getMock(); + $relation = $this->getMockBuilder('Illuminate\Database\Eloquent\Relations\BelongsToMany')->setMethods(['attach', 'detach', 'touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); + $relation->getQuery()->shouldReceive('where')->once()->with('user_role.foo', '=', 'bar', 'and')->andReturnSelf(); + $currentQuery = $this->getNewQuery($relation); + $currentQuery->shouldReceive('pluck')->andReturn(new BaseCollection([1, 2, 3])); + + // This is our test! The wherePivot() params also need to be called + $currentQuery->shouldReceive('where')->once()->with('foo', '=', 'bar')->andReturnSelf(); + + $relation->wherePivot('foo', '=', 'bar')->sync([1, 2, 3]); + } + + public function testAttachingFiresEvents() + { + $query = $this->getNewStatement($relation = $this->getRelation()); + $query->shouldReceive('insert')->once()->andReturn(1); - // we expect to call $relation->wherePivot() - $relation->getQuery()->shouldReceive('where')->once()->andReturn($relation); + $relation->getRelated()->shouldReceive('touches')->andReturn(false); + list($parent, $events) = $this->getEventsParent($relation); - // Our sync() call will produce a new query - $mockQueryBuilder = m::mock('stdClass'); - $query = m::mock('stdClass'); - $relation->getQuery()->shouldReceive('getQuery')->andReturn($mockQueryBuilder); - $mockQueryBuilder->shouldReceive('newQuery')->once()->andReturn($query); + $events->shouldReceive('until')->once()->with('eloquent.attaching.relation_name: '.get_class($parent), [$parent, new BaseCollection([['user_id' => 1, 'role_id' => 5, 'pivot_column' => 'extra_data']])])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.attached.relation_name: '.get_class($parent), [$parent, new BaseCollection([['user_id' => 1, 'role_id' => 5, 'pivot_column' => 'extra_data']])])->andReturn(true); - // BelongsToMany::newPivotStatement() sets this - $query->shouldReceive('from')->once()->with('user_role')->andReturn($query); + $relation->attach(5, ['pivot_column' => 'extra_data']); + } - // BelongsToMany::newPivotQuery() sets this - $query->shouldReceive('where')->once()->with('user_id', 1)->andReturn($query); + public function testDetachingFiresEvents() + { + $query = $this->getNewQuery($relation = $this->getRelation()); + $query->shouldReceive('whereIn')->once()->with('role_id', [5]); + $query->shouldReceive('delete')->once()->andReturn(1); - // This is our test! The wherePivot() params also need to be called - $query->shouldReceive('where')->once()->with('foo', '=', 'bar')->andReturn($query); + $relation->getRelated()->shouldReceive('touches')->andReturn(false); + list($parent, $events) = $this->getEventsParent($relation); - // This is so $relation->sync() works - $query->shouldReceive('pluck')->once()->with('role_id')->andReturn(new BaseCollection([1, 2, 3])); - $relation->expects($this->once())->method('formatRecordsList')->with([1, 2, 3])->will($this->returnValue([1 => [], 2 => [], 3 => []])); + $events->shouldReceive('until')->once()->with('eloquent.detaching.relation_name: '.get_class($parent), [$parent, new BaseCollection(5)])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.detached.relation_name: '.get_class($parent), [$parent, new BaseCollection(5)])->andReturn(true); + + $relation->detach(5); + } + + public function testSyncingFiresEvents() + { + $current = $this->getNewQuery($relation = $this->getRelation()); + $current->shouldReceive('pluck')->with('role_id')->andReturn(new BaseCollection(5)); + + $queryForId = $this->getNewQuery($relation); + $queryForId->shouldReceive('where')->with('role_id', 5)->andReturnSelf(); + $queryForId->shouldReceive('update')->once()->with(['pivot_column' => 'extra_data'])->andReturn(1); + + $relation->getRelated()->shouldReceive('touches')->andReturn(false); + list($parent, $events) = $this->getEventsParent($relation); + + $events->shouldReceive('until')->once()->with('eloquent.syncing.relation_name: '.get_class($parent), [$parent, new BaseCollection([5 => ['pivot_column' => 'extra_data']])])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.synced.relation_name: '.get_class($parent), [$parent, new BaseCollection(['attached' => [], 'detached' => [], 'updated' => [5]])])->andReturn(true); + + $relation->sync([5 => ['pivot_column' => 'extra_data']]); + } + + public function testTogglingFiresEvents() + { + $current = $this->getNewQuery($relation = $this->getRelation()); + $current->shouldReceive('pluck')->with('role_id')->andReturn(new BaseCollection(4)); + + $detaching = $this->getNewQuery($relation); + $detaching->shouldReceive('whereIn')->once()->with('role_id', [4]); + $detaching->shouldReceive('delete')->once()->andReturn(1); + + $attaching = $this->getNewStatement($relation); + $attaching->shouldReceive('insert')->once()->andReturn(1); + + $relation->getRelated()->shouldReceive('touches')->andReturn(false); + list($parent, $events) = $this->getEventsParent($relation); + + $events->shouldReceive('until')->once()->with('eloquent.toggling.relation_name: '.get_class($parent), [$parent, new BaseCollection([4 => [], 5 => ['pivot_column' => 'extra_data']])])->andReturn(true); + $events->shouldReceive('until')->once()->with('eloquent.detaching.relation_name: '.get_class($parent), [$parent, new BaseCollection(4)])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.detached.relation_name: '.get_class($parent), [$parent, new BaseCollection(4)])->andReturn(true); + $events->shouldReceive('until')->once()->with('eloquent.attaching.relation_name: '.get_class($parent), [$parent, new BaseCollection([['user_id' => 1, 'role_id' => 5, 'pivot_column' => 'extra_data']])])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.attached.relation_name: '.get_class($parent), [$parent, new BaseCollection([['user_id' => 1, 'role_id' => 5, 'pivot_column' => 'extra_data']])])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.toggled.relation_name: '.get_class($parent), [$parent, new BaseCollection(['attached' => [5], 'detached' => [4]])])->andReturn(true); + + $relation->toggle([4, 5 => ['pivot_column' => 'extra_data']]); + } + + public function testSyncingFiresAllEvents() + { + $relation = $this->getRelation(); + $relation->getRelated()->shouldReceive('touches')->andReturn(false); + $syncData = [ + 5 => ['pivot_column' => 'extra_updated'], + 6 => ['pivot_column' => 'extra_attached'], + ]; + + $current = $this->getNewQuery($relation); + $current->shouldReceive('pluck')->with('role_id')->andReturn(new BaseCollection([4, 5])); + + $detaching = $this->getNewQuery($relation); + $detaching->shouldReceive('whereIn')->once()->with('role_id', [4]); + $detaching->shouldReceive('delete')->once()->andReturn(1); + + $updating = $this->getNewQuery($relation); + $updating->shouldReceive('where')->with('role_id', 5)->andReturnSelf(); + $updating->shouldReceive('update')->once()->with(['pivot_column' => 'extra_updated'])->andReturn(1); + + $attaching = $this->getNewStatement($relation); + $attaching->shouldReceive('insert')->once()->andReturn(1); + + list($parent, $events) = $this->getEventsParent($relation); + + $events->shouldReceive('until')->once()->with('eloquent.syncing.relation_name: '.get_class($parent), [$parent, new BaseCollection($syncData)])->andReturn(true); + $events->shouldReceive('until')->once()->with('eloquent.detaching.relation_name: '.get_class($parent), [$parent, new BaseCollection(4)])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.detached.relation_name: '.get_class($parent), [$parent, new BaseCollection(4)])->andReturn(true); + $events->shouldReceive('until')->once()->with('eloquent.attaching.relation_name: '.get_class($parent), [$parent, new BaseCollection([['user_id' => 1, 'role_id' => 6, 'pivot_column' => 'extra_attached']])])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.attached.relation_name: '.get_class($parent), [$parent, new BaseCollection([['user_id' => 1, 'role_id' => 6, 'pivot_column' => 'extra_attached']])])->andReturn(true); + $events->shouldReceive('fire')->once()->with('eloquent.synced.relation_name: '.get_class($parent), [$parent, new BaseCollection(['attached' => [6], 'detached' => [4], 'updated' => [5]])])->andReturn(true); - $relation = $relation->wherePivot('foo', '=', 'bar'); // these params are to be stored - $relation->sync([1, 2, 3]); // triggers the whole process above + $relation->sync($syncData); + } + + public function getEventsParent($relation) + { + $parent = $relation->getParent(); + $parent->shouldReceive('touches')->andReturn(false); + $parent->shouldReceive('getEventDispatcher')->andReturn($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); + return [$parent, $events]; + } + + public function getNewStatement($relation) + { + $relationQuery = $relation->getQuery(); + $relationQuery->shouldReceive('getQuery->newQuery->from')->once()->andReturn($newPivotStatement = m::mock('Illuminate\Database\Query\Builder')); + return $newPivotStatement; + } + + public function getNewQuery($relation) + { + $statement = $this->getNewStatement($relation); + $statement->shouldReceive('where')->once()->with('user_id', 1)->andReturnSelf(); + return $statement; } public function getRelation() @@ -671,6 +776,7 @@ public function getRelationArguments() $parent->shouldReceive('getKey')->andReturn(1); $parent->shouldReceive('getCreatedAtColumn')->andReturn('created_at'); $parent->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at'); + $parent->shouldReceive('getEventDispatcher')->byDefault(); $builder = m::mock('Illuminate\Database\Eloquent\Builder'); $related = m::mock('Illuminate\Database\Eloquent\Model'); diff --git a/tests/Database/DatabaseEloquentMorphToManyTest.php b/tests/Database/DatabaseEloquentMorphToManyTest.php index 84e104521506..b78d69810c8c 100644 --- a/tests/Database/DatabaseEloquentMorphToManyTest.php +++ b/tests/Database/DatabaseEloquentMorphToManyTest.php @@ -82,6 +82,7 @@ public function getRelationArguments() $parent->shouldReceive('getCreatedAtColumn')->andReturn('created_at'); $parent->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at'); $parent->shouldReceive('getMorphClass')->andReturn(get_class($parent)); + $parent->shouldReceive('getEventDispatcher')->byDefault(); $builder = m::mock('Illuminate\Database\Eloquent\Builder'); $related = m::mock('Illuminate\Database\Eloquent\Model'); From 8530bcba7aba96e533c9337bd238408e1e302672 Mon Sep 17 00:00:00 2001 From: Jarek Tkaczyk Date: Wed, 24 Aug 2016 10:06:22 +0800 Subject: [PATCH 2/2] Added model registrars for BelongsToMany events styleCI --- src/Illuminate/Database/Eloquent/Model.php | 104 ++++++++++++++++++ .../DatabaseEloquentBelongsToManyTest.php | 3 + 2 files changed, 107 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 0f32d2a369ed..4329f789baee 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1242,6 +1242,110 @@ public static function deleted($callback, $priority = 0) static::registerModelEvent('deleted', $callback, $priority); } + /** + * Register a relation attaching model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function attaching($relation, $callback, $priority = 0) + { + static::registerModelEvent("attaching.{$relation}", $callback, $priority); + } + + /** + * Register a relation attached model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function attached($relation, $callback, $priority = 0) + { + static::registerModelEvent("attached.{$relation}", $callback, $priority); + } + + /** + * Register a relation detaching model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function detaching($relation, $callback, $priority = 0) + { + static::registerModelEvent("detaching.{$relation}", $callback, $priority); + } + + /** + * Register a relation detached model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function detached($relation, $callback, $priority = 0) + { + static::registerModelEvent("detached.{$relation}", $callback, $priority); + } + + /** + * Register a relation syncing model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function syncing($relation, $callback, $priority = 0) + { + static::registerModelEvent("syncing.{$relation}", $callback, $priority); + } + + /** + * Register a relation synced model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function synced($relation, $callback, $priority = 0) + { + static::registerModelEvent("synced.{$relation}", $callback, $priority); + } + + /** + * Register a relation toggling model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function toggling($relation, $callback, $priority = 0) + { + static::registerModelEvent("toggling.{$relation}", $callback, $priority); + } + + /** + * Register a relation toggled model event with the dispatcher. + * + * @param string $relation + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function toggled($relation, $callback, $priority = 0) + { + static::registerModelEvent("toggled.{$relation}", $callback, $priority); + } + /** * Remove all of the event listeners for the model. * diff --git a/tests/Database/DatabaseEloquentBelongsToManyTest.php b/tests/Database/DatabaseEloquentBelongsToManyTest.php index 3dc3eec98b87..9db53b378e2e 100755 --- a/tests/Database/DatabaseEloquentBelongsToManyTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyTest.php @@ -746,6 +746,7 @@ public function getEventsParent($relation) $parent = $relation->getParent(); $parent->shouldReceive('touches')->andReturn(false); $parent->shouldReceive('getEventDispatcher')->andReturn($events = m::mock('Illuminate\Contracts\Events\Dispatcher')); + return [$parent, $events]; } @@ -753,6 +754,7 @@ public function getNewStatement($relation) { $relationQuery = $relation->getQuery(); $relationQuery->shouldReceive('getQuery->newQuery->from')->once()->andReturn($newPivotStatement = m::mock('Illuminate\Database\Query\Builder')); + return $newPivotStatement; } @@ -760,6 +762,7 @@ public function getNewQuery($relation) { $statement = $this->getNewStatement($relation); $statement->shouldReceive('where')->once()->with('user_id', 1)->andReturnSelf(); + return $statement; }