table = $table; $this->otherKey = $otherKey; $this->foreignKey = $foreignKey; $this->relationName = $relationName; parent::__construct($query, $parent); } /** * Get the results of the relationship. * * @return mixed */ public function getResults() { return $this->get(); } /** * Set a where clause for a pivot table column. * * @param string $column * @param string $operator * @param mixed $value * @param string $boolean * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function wherePivot($column, $operator = null, $value = null, $boolean = 'and') { $this->pivotWheres[] = func_get_args(); return $this->where($this->table.'.'.$column, $operator, $value, $boolean); } /** * Set an or where clause for a pivot table column. * * @param string $column * @param string $operator * @param mixed $value * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function orWherePivot($column, $operator = null, $value = null) { return $this->wherePivot($column, $operator, $value, 'or'); } /** * Execute the query and get the first result. * * @param array $columns * @return mixed */ public function first($columns = array('*')) { $results = $this->take(1)->get($columns); return count($results) > 0 ? $results->first() : null; } /** * Execute the query and get the first result or throw an exception. * * @param array $columns * @return \Illuminate\Database\Eloquent\Model|static * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function firstOrFail($columns = array('*')) { if ( ! is_null($model = $this->first($columns))) return $model; throw new ModelNotFoundException; } /** * Execute the query as a "select" statement. * * @param array $columns * @return \Illuminate\Database\Eloquent\Collection */ public function get($columns = array('*')) { // First we'll add the proper select columns onto the query so it is run with // the proper columns. Then, we will get the results and hydrate out pivot // models with the result of those columns as a separate model relation. $columns = $this->query->getQuery()->columns ? array() : $columns; $select = $this->getSelectColumns($columns); $models = $this->query->addSelect($select)->getModels(); $this->hydratePivotRelation($models); // If we actually found models we will also eager load any relationships that // have been specified as needing to be eager loaded. This will solve the // n + 1 query problem for the developer and also increase performance. if (count($models) > 0) { $models = $this->query->eagerLoadRelations($models); } return $this->related->newCollection($models); } /** * Get a paginator for the "select" statement. * * @param int $perPage * @param array $columns * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function paginate($perPage = null, $columns = array('*')) { $this->query->addSelect($this->getSelectColumns($columns)); $paginator = $this->query->paginate($perPage, $columns); $this->hydratePivotRelation($paginator->items()); return $paginator; } /** * Paginate the given query into a simple paginator. * * @param int $perPage * @param array $columns * @return \Illuminate\Contracts\Pagination\Paginator */ public function simplePaginate($perPage = null, $columns = array('*')) { $this->query->addSelect($this->getSelectColumns($columns)); $paginator = $this->query->simplePaginate($perPage, $columns); $this->hydratePivotRelation($paginator->items()); return $paginator; } /** * Chunk the results of the query. * * @param int $count * @param callable $callback * @return void */ public function chunk($count, callable $callback) { $this->query->addSelect($this->getSelectColumns()); $this->query->chunk($count, function($results) use ($callback) { $this->hydratePivotRelation($results->all()); call_user_func($callback, $results); }); } /** * Hydrate the pivot table relationship on the models. * * @param array $models * @return void */ protected function hydratePivotRelation(array $models) { // To hydrate the pivot relationship, we will just gather the pivot attributes // and create a new Pivot model, which is basically a dynamic model that we // will set the attributes, table, and connections on so it they be used. foreach ($models as $model) { $pivot = $this->newExistingPivot($this->cleanPivotAttributes($model)); $model->setRelation('pivot', $pivot); } } /** * Get the pivot attributes from a model. * * @param \Illuminate\Database\Eloquent\Model $model * @return array */ protected function cleanPivotAttributes(Model $model) { $values = array(); foreach ($model->getAttributes() as $key => $value) { // To get the pivots attributes we will just take any of the attributes which // begin with "pivot_" and add those to this arrays, as well as unsetting // them from the parent's models since they exist in a different table. if (strpos($key, 'pivot_') === 0) { $values[substr($key, 6)] = $value; unset($model->$key); } } return $values; } /** * Set the base constraints on the relation query. * * @return void */ public function addConstraints() { $this->setJoin(); if (static::$constraints) $this->setWhere(); } /** * Add the constraints for a relationship count query. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parent * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationCountQuery(Builder $query, Builder $parent) { if ($parent->getQuery()->from == $query->getQuery()->from) { return $this->getRelationCountQueryForSelfJoin($query, $parent); } $this->setJoin($query); return parent::getRelationCountQuery($query, $parent); } /** * Add the constraints for a relationship count query on the same table. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $parent * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationCountQueryForSelfJoin(Builder $query, Builder $parent) { $query->select(new Expression('count(*)')); $tablePrefix = $this->query->getQuery()->getConnection()->getTablePrefix(); $query->from($this->table.' as '.$tablePrefix.$hash = $this->getRelationCountHash()); $key = $this->wrap($this->getQualifiedParentKeyName()); return $query->where($hash.'.'.$this->foreignKey, '=', new Expression($key)); } /** * Get a relationship join table hash. * * @return string */ public function getRelationCountHash() { return 'self_'.md5(microtime(true)); } /** * Set the select clause for the relation query. * * @param array $columns * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ protected function getSelectColumns(array $columns = array('*')) { if ($columns == array('*')) { $columns = array($this->related->getTable().'.*'); } return array_merge($columns, $this->getAliasedPivotColumns()); } /** * Get the pivot columns for the relation. * * @return array */ protected function getAliasedPivotColumns() { $defaults = array($this->foreignKey, $this->otherKey); // We need to alias all of the pivot columns with the "pivot_" prefix so we // can easily extract them out of the models and put them into the pivot // relationships when they are retrieved and hydrated into the models. $columns = array(); foreach (array_merge($defaults, $this->pivotColumns) as $column) { $columns[] = $this->table.'.'.$column.' as pivot_'.$column; } return array_unique($columns); } /** * Determine whether the given column is defined as a pivot column. * * @param string $column * @return bool */ protected function hasPivotColumn($column) { return in_array($column, $this->pivotColumns); } /** * Set the join clause for the relation query. * * @param \Illuminate\Database\Eloquent\Builder|null * @return $this */ protected function setJoin($query = null) { $query = $query ?: $this->query; // We need to join to the intermediate table on the related model's primary // key column with the intermediate table's foreign key for the related // model instance. Then we can set the "where" for the parent models. $baseTable = $this->related->getTable(); $key = $baseTable.'.'.$this->related->getKeyName(); $query->join($this->table, $key, '=', $this->getOtherKey()); return $this; } /** * Set the where clause for the relation query. * * @return $this */ protected function setWhere() { $foreign = $this->getForeignKey(); $this->query->where($foreign, '=', $this->parent->getKey()); return $this; } /** * Set the constraints for an eager load of the relation. * * @param array $models * @return void */ public function addEagerConstraints(array $models) { $this->query->whereIn($this->getForeignKey(), $this->getKeys($models)); } /** * Initialize the relation on a set of models. * * @param array $models * @param string $relation * @return array */ public function initRelation(array $models, $relation) { foreach ($models as $model) { $model->setRelation($relation, $this->related->newCollection()); } return $models; } /** * Match the eagerly loaded results to their parents. * * @param array $models * @param \Illuminate\Database\Eloquent\Collection $results * @param string $relation * @return array */ public function match(array $models, Collection $results, $relation) { $dictionary = $this->buildDictionary($results); // Once we have an array dictionary of child objects we can easily match the // children back to their parent using the dictionary and the keys on the // the parent models. Then we will return the hydrated models back out. foreach ($models as $model) { if (isset($dictionary[$key = $model->getKey()])) { $collection = $this->related->newCollection($dictionary[$key]); $model->setRelation($relation, $collection); } } return $models; } /** * Build model dictionary keyed by the relation's foreign key. * * @param \Illuminate\Database\Eloquent\Collection $results * @return array */ protected function buildDictionary(Collection $results) { $foreign = $this->foreignKey; // First we will build a dictionary of child models keyed by the foreign key // of the relation so that we will easily and quickly match them to their // parents without having a possibly slow inner loops for every models. $dictionary = array(); foreach ($results as $result) { $dictionary[$result->pivot->$foreign][] = $result; } return $dictionary; } /** * Touch all of the related models for the relationship. * * E.g.: Touch all roles associated with this user. * * @return void */ public function touch() { $key = $this->getRelated()->getKeyName(); $columns = $this->getRelatedFreshUpdate(); // If we actually have IDs for the relation, we will run the query to update all // the related model's timestamps, to make sure these all reflect the changes // to the parent models. This will help us keep any caching synced up here. $ids = $this->getRelatedIds(); if (count($ids) > 0) { $this->getRelated()->newQuery()->whereIn($key, $ids)->update($columns); } } /** * Get all of the IDs for the related models. * * @return array */ public function getRelatedIds() { $related = $this->getRelated(); $fullKey = $related->getQualifiedKeyName(); return $this->getQuery()->select($fullKey)->lists($related->getKeyName()); } /** * Save a new model and attach it to the parent model. * * @param \Illuminate\Database\Eloquent\Model $model * @param array $joining * @param bool $touch * @return \Illuminate\Database\Eloquent\Model */ public function save(Model $model, array $joining = array(), $touch = true) { $model->save(array('touch' => false)); $this->attach($model->getKey(), $joining, $touch); return $model; } /** * Save an array of new models and attach them to the parent model. * * @param array $models * @param array $joinings * @return array */ public function saveMany(array $models, array $joinings = array()) { foreach ($models as $key => $model) { $this->save($model, (array) array_get($joinings, $key), false); } $this->touchIfTouching(); return $models; } /** * Find a related model by its primary key. * * @param mixed $id * @param array $columns * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null */ public function find($id, $columns = ['*']) { if (is_array($id)) { return $this->findMany($id, $columns); } $this->where($this->getRelated()->getQualifiedKeyName(), '=', $id); return $this->first($columns); } /** * Find multiple related models by their primary keys. * * @param mixed $id * @param array $columns * @return \Illuminate\Database\Eloquent\Collection */ public function findMany($ids, $columns = ['*']) { if (empty($ids)) return $this->getRelated()->newCollection(); $this->whereIn($this->getRelated()->getQualifiedKeyName(), $ids); return $this->get($columns); } /** * Find a related model by its primary key or return new instance of the related model. * * @param mixed $id * @param array $columns * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model */ public function findOrNew($id, $columns = ['*']) { if (is_null($instance = $this->find($id, $columns))) { $instance = $this->getRelated()->newInstance(); } return $instance; } /** * Get the first related model record matching the attributes or instantiate it. * * @param array $attributes * @return \Illuminate\Database\Eloquent\Model */ public function firstOrNew(array $attributes) { if (is_null($instance = $this->where($attributes)->first())) { $instance = $this->related->newInstance(); } return $instance; } /** * Get the first related record matching the attributes or create it. * * @param array $attributes * @return \Illuminate\Database\Eloquent\Model */ public function firstOrCreate(array $attributes, array $joining = [], $touch = true) { if (is_null($instance = $this->where($attributes)->first())) { $instance = $this->create($attributes, $joining, $touch); } return $instance; } /** * Create or update a related record matching the attributes, and fill it with values. * * @param array $attributes * @param array $values * @return \Illuminate\Database\Eloquent\Model */ public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) { if (is_null($instance = $this->where($attributes)->first())) { return $this->create($values, $joining, $touch); } $instance->fill($values); $instance->save(['touch' => false]); return $instance; } /** * Create a new instance of the related model. * * @param array $attributes * @param array $joining * @param bool $touch * @return \Illuminate\Database\Eloquent\Model */ public function create(array $attributes, array $joining = array(), $touch = true) { $instance = $this->related->newInstance($attributes); // Once we save the related model, we need to attach it to the base model via // through intermediate table so we'll use the existing "attach" method to // accomplish this which will insert the record and any more attributes. $instance->save(array('touch' => false)); $this->attach($instance->getKey(), $joining, $touch); return $instance; } /** * Create an array of new instances of the related models. * * @param array $records * @param array $joinings * @return \Illuminate\Database\Eloquent\Model */ public function createMany(array $records, array $joinings = array()) { $instances = array(); foreach ($records as $key => $record) { $instances[] = $this->create($record, (array) array_get($joinings, $key), false); } $this->touchIfTouching(); return $instances; } /** * Sync the intermediate tables with a list of IDs or collection of models. * * @param array $ids * @param bool $detaching * @return array */ public function sync($ids, $detaching = true) { $changes = array( 'attached' => array(), 'detached' => array(), 'updated' => array(), ); 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()->lists($this->otherKey); $records = $this->formatSyncList($ids); $detach = array_diff($current, array_keys($records)); // 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 // the array of the IDs given to the method which will complete the sync. if ($detaching && count($detach) > 0) { $this->detach($detach); $changes['detached'] = (array) array_map(function($v) { return (int) $v; }, $detach); } // Now we are finally ready to attach the new records. Note that we'll disable // 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) ); if (count($changes['attached']) || count($changes['updated'])) { $this->touchIfTouching(); } return $changes; } /** * Format the sync list so that it is keyed by ID. * * @param array $records * @return array */ protected function formatSyncList(array $records) { $results = array(); foreach ($records as $id => $attributes) { if ( ! is_array($attributes)) { list($id, $attributes) = array($attributes, array()); } $results[$id] = $attributes; } return $results; } /** * Attach all of the IDs that aren't in the current array. * * @param array $records * @param array $current * @param bool $touch * @return array */ protected function attachNew(array $records, array $current, $touch = true) { $changes = array('attached' => array(), 'updated' => array()); foreach ($records as $id => $attributes) { // If the ID is not in the list of existing pivot IDs, we will insert a new pivot // record, otherwise, we will just update this existing record on this joining // table, so that the developers will easily update these records pain free. if ( ! in_array($id, $current)) { $this->attach($id, $attributes, $touch); $changes['attached'][] = (int) $id; } // Now we'll try to update an existing pivot record with the attributes that were // given to the method. If the model is actually updated we will add it to the // list of updated pivot records so we return them back out to the consumer. elseif (count($attributes) > 0 && $this->updateExistingPivot($id, $attributes, $touch)) { $changes['updated'][] = (int) $id; } } return $changes; } /** * Update an existing pivot record on the table. * * @param mixed $id * @param array $attributes * @param bool $touch * @return void */ public function updateExistingPivot($id, array $attributes, $touch = true) { if (in_array($this->updatedAt(), $this->pivotColumns)) { $attributes = $this->setTimestampsOnAttach($attributes, true); } $updated = $this->newPivotStatementForId($id)->update($attributes); if ($touch) $this->touchIfTouching(); return $updated; } /** * Attach a model to the parent. * * @param mixed $id * @param array $attributes * @param bool $touch * @return void */ public function attach($id, array $attributes = array(), $touch = true) { if ($id instanceof Model) $id = $id->getKey(); $query = $this->newPivotStatement(); $query->insert($this->createAttachRecords((array) $id, $attributes)); if ($touch) $this->touchIfTouching(); } /** * Create an array of records to insert into the pivot table. * * @param array $ids * @param array $attributes * @return array */ protected function createAttachRecords($ids, array $attributes) { $records = array(); $timed = ($this->hasPivotColumn($this->createdAt()) || $this->hasPivotColumn($this->updatedAt())); // 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) { $records[] = $this->attacher($key, $value, $attributes, $timed); } return $records; } /** * Create a full attachment record payload. * * @param int $key * @param mixed $value * @param array $attributes * @param bool $timed * @return array */ protected function attacher($key, $value, $attributes, $timed) { list($id, $extra) = $this->getAttachId($key, $value, $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. $record = $this->createAttachRecord($id, $timed); return array_merge($record, $extra); } /** * Get the attach record ID and extra attributes. * * @param mixed $key * @param mixed $value * @param array $attributes * @return array */ protected function getAttachId($key, $value, array $attributes) { if (is_array($value)) { return array($key, array_merge($value, $attributes)); } return array($value, $attributes); } /** * Create a new pivot attachment record. * * @param int $id * @param bool $timed * @return array */ protected function createAttachRecord($id, $timed) { $record[$this->foreignKey] = $this->parent->getKey(); $record[$this->otherKey] = $id; // If the record needs to have creation and update timestamps, we will make // them by calling the parent model's "freshTimestamp" method which will // provide us with a fresh timestamp in this model's preferred format. if ($timed) { $record = $this->setTimestampsOnAttach($record); } return $record; } /** * Set the creation and update timestamps on an attach record. * * @param array $record * @param bool $exists * @return array */ protected function setTimestampsOnAttach(array $record, $exists = false) { $fresh = $this->parent->freshTimestamp(); if ( ! $exists && $this->hasPivotColumn($this->createdAt())) { $record[$this->createdAt()] = $fresh; } if ($this->hasPivotColumn($this->updatedAt())) { $record[$this->updatedAt()] = $fresh; } return $record; } /** * Detach models from the relationship. * * @param int|array $ids * @param bool $touch * @return int */ public function detach($ids = array(), $touch = true) { if ($ids instanceof Model) $ids = (array) $ids->getKey(); $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; if (count($ids) > 0) { $query->whereIn($this->otherKey, (array) $ids); } if ($touch) $this->touchIfTouching(); // 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(); return $results; } /** * If we're touching the parent model, touch. * * @return void */ public function touchIfTouching() { if ($this->touchingParent()) $this->getParent()->touch(); if ($this->getParent()->touches($this->relationName)) $this->touch(); } /** * Determine if we should touch the parent on sync. * * @return bool */ protected function touchingParent() { return $this->getRelated()->touches($this->guessInverseRelation()); } /** * Attempt to guess the name of the inverse of the relation. * * @return string */ protected function guessInverseRelation() { return camel_case(str_plural(class_basename($this->getParent()))); } /** * Create a new query builder for the pivot table. * * @return \Illuminate\Database\Query\Builder */ protected function newPivotQuery() { $query = $this->newPivotStatement(); foreach ($this->pivotWheres as $whereArgs) { call_user_func_array([$query, 'where'], $whereArgs); } return $query->where($this->foreignKey, $this->parent->getKey()); } /** * Get a new plain query builder for the pivot table. * * @return \Illuminate\Database\Query\Builder */ public function newPivotStatement() { return $this->query->getQuery()->newQuery()->from($this->table); } /** * Get a new pivot statement for a given "other" ID. * * @param mixed $id * @return \Illuminate\Database\Query\Builder */ public function newPivotStatementForId($id) { return $this->newPivotQuery()->where($this->otherKey, $id); } /** * Create a new pivot model instance. * * @param array $attributes * @param bool $exists * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newPivot(array $attributes = array(), $exists = false) { $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists); return $pivot->setPivotKeys($this->foreignKey, $this->otherKey); } /** * Create a new existing pivot model instance. * * @param array $attributes * @return \Illuminate\Database\Eloquent\Relations\Pivot */ public function newExistingPivot(array $attributes = array()) { return $this->newPivot($attributes, true); } /** * Set the columns on the pivot table to retrieve. * * @param mixed $columns * @return $this */ public function withPivot($columns) { $columns = is_array($columns) ? $columns : func_get_args(); $this->pivotColumns = array_merge($this->pivotColumns, $columns); return $this; } /** * Specify that the pivot table has creation and update timestamps. * * @param mixed $createdAt * @param mixed $updatedAt * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function withTimestamps($createdAt = null, $updatedAt = null) { return $this->withPivot($createdAt ?: $this->createdAt(), $updatedAt ?: $this->updatedAt()); } /** * Get the related model's updated at column name. * * @return string */ public function getRelatedFreshUpdate() { return array($this->related->getUpdatedAtColumn() => $this->related->freshTimestamp()); } /** * Get the key for comparing against the parent key in "has" query. * * @return string */ public function getHasCompareKey() { return $this->getForeignKey(); } /** * Get the fully qualified foreign key for the relation. * * @return string */ public function getForeignKey() { return $this->table.'.'.$this->foreignKey; } /** * Get the fully qualified "other key" for the relation. * * @return string */ public function getOtherKey() { return $this->table.'.'.$this->otherKey; } /** * Get the intermediate table for the relationship. * * @return string */ public function getTable() { return $this->table; } /** * Get the relationship name for the relationship. * * @return string */ public function getRelationName() { return $this->relationName; } }