diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 0272c4d1132c..4f07581db122 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -68,6 +68,13 @@ class BelongsToMany extends Relation */ protected $pivotUpdatedAt; + /** + * A custom pivot model class name. + * + * @var string + */ + protected $pivotModel; + /** * Create a new belongs to many relationship instance. * @@ -89,6 +96,28 @@ public function __construct(Builder $query, Model $parent, $table, $foreignKey, parent::__construct($query, $parent); } + /** + * Set the custom pivot model class. + * + * @param string $class + */ + public function setPivotModel($class) + { + $this->pivotModel = $class; + + return $this; + } + + /** + * Gets the custom pivot model class. + * + * @return string + */ + public function getPivotModel() + { + return $this->pivotModel; + } + /** * Get the results of the relationship. * @@ -880,7 +909,7 @@ public function updateExistingPivot($id, array $attributes, $touch = true) * * @param mixed $id * @param array $attributes - * @param bool $touch + * @param bool $touch * @return void */ public function attach($id, array $attributes = [], $touch = true) @@ -891,13 +920,59 @@ public function attach($id, array $attributes = [], $touch = true) $query = $this->newPivotStatement(); - $query->insert($this->createAttachRecords((array) $id, $attributes)); + $records = $this->createAttachRecords((array) $id, $attributes); + + // If we have a custom pivot model, we route the record + // creatation through a separate process to make the models, + // fire their events and save them individually. + if (class_exists($this->getPivotModel())) { + $this->insertModelRecords($records, $query); + } + + // Otherwise we bulk insert them straight to the database. + else { + $query->insert($records); + } if ($touch) { $this->touchIfTouching(); } } + /** + * Insert each of the associated records and + * fire thier models events. + * + * @param array $records + * @param Query $query + * @return void + */ + protected function insertModelRecords(array $records, $query) + { + // Loop through each of the records, firing their + // events and saving them. + foreach ($records as $key => $record) { + $model = $this->newPivot($record); + + if ($this->firePivotModelEvent('saving', $model) === false + || $this->firePivotModelEvent('creating', $model) === false) { + + // We don't want to create this + // model, so continue on without inserting. + continue; + } + + $query->insert([$model->getAttributes()]); + + $this->firePivotModelEvent('created', $model, false); + $this->firePivotModelEvent('saved', $model, false); + + $model->exists = true; + + $model->wasRecentlyCreated = true; + } + } + /** * Create an array of records to insert into the pivot table. * @@ -1018,6 +1093,18 @@ public function detach($ids = [], $touch = true) $ids = (array) $ids->getKey(); } + if (class_exists($this->getPivotModel())) { + + // If we have a custom pivot model, we fire the events + // for each model and get the resulting id array to parse below. + $ids = $this->getDetachableModelIds($ids); + + // Return an empty array if we've deleted nothing + if (0 === count($ids)) { + return []; + } + } + $query = $this->newPivotQuery(); // If associated IDs were passed to the method we will only delete those @@ -1041,6 +1128,49 @@ public function detach($ids = [], $touch = true) return $results; } + /** + * Get a list of model IDs that can be detached. + * + * @param mixed $ids + * @return array + */ + protected function getDetachableModelIds($ids) + { + $deleted = []; + + // If we haven't been given an ID to delete + // we get everything and delete the lot. + if (count($ids) === 0) { + $ids = $this->newPivotQuery()->get(); + } + + foreach ((array) $ids as $key => $id) { + + // Expect the ID to be a valid record if it's an object. + if (is_object($id)) { + $model = $this->newPivotModelFromRecord($id); + } + + // Otherwise expect the ID to be the otherKey. + else { + $model = $this->newPivotModelFromId($id); + } + + // Fire the models events + if (! $this->firePivotModelDeletionEvents($model)) { + + // We don't want to delete this model, so continue + // on without adding it's ID to the deleted array. + continue; + } + + // Add the ID to the array of deleted IDs. + $deleted[] = $model->{$this->otherKey}; + } + + return array_unique($deleted); + } + /** * If we're touching the parent model, touch. * @@ -1118,16 +1248,93 @@ public function newPivotStatementForId($id) * Create a new pivot model instance. * * @param array $attributes - * @param bool $exists + * @param bool $exists * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newPivot(array $attributes = [], $exists = false) { - $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists); + if ($class = $this->getPivotModel()) { + $pivot = new $class($this->parent, $attributes, $this->table, $exists); + } else { + $related = $this->related; + + $pivot = $related->newPivot($this->parent, $attributes, $this->table, $exists); + } return $pivot->setPivotKeys($this->foreignKey, $this->otherKey); } + /** + * Get a pivot mode instance from ID. + * + * @param int $id + * @return Pivot + */ + protected function newPivotModelFromId($id) + { + $record = $this->newPivotStatementForId($id)->first(); + + return $this->newPivotModelFromRecord($record); + } + + /** + * Get a pivot model instance from record. + * + * @param object $record + * @return Pivot + */ + protected function newPivotModelFromRecord($record) + { + // Create a new instance of the model and fire it's events. + return $this->newExistingPivot(get_object_vars($record)); + } + + /** + * Fire the deleting events for a pivot model. + * + * @param Pivot $model + * @return bool + */ + protected function firePivotModelDeletionEvents($model) + { + if ($this->firePivotModelEvent('deleting', $model) === false) { + + // We don't want to delete this model. + return false; + } + + $model->exists = false; + + $this->firePivotModelEvent('deleted', $model, false); + + return true; + } + + /** + * Fire the given event for the model. + * + * @param string $event + * @param bool $halt + * @return mixed + */ + protected function firePivotModelEvent($event, $model, $halt = true) + { + $dispatcher = $model::getEventDispatcher(); + + if (! isset($dispatcher)) { + return true; + } + + // We will append the names of the class to the event to distinguish it from + // other model events that are fired, allowing us to listen on each model + // event set individually instead of catching event for all the models. + $event = "eloquent.{$event}: ".get_class($model); + + $method = $halt ? 'until' : 'fire'; + + return $dispatcher->$method($event, $model); + } + /** * Create a new existing pivot model instance. *