123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- <?php
-
- namespace Illuminate\Database\Eloquent\Relations\Concerns;
-
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Database\Eloquent\Collection;
- use Illuminate\Support\Collection as BaseCollection;
-
- trait InteractsWithPivotTable
- {
- /**
- * Toggles a model (or models) from the parent.
- *
- * Each existing model is detached, and non existing ones are attached.
- *
- * @param mixed $ids
- * @param bool $touch
- * @return array
- */
- public function toggle($ids, $touch = true)
- {
- $changes = [
- 'attached' => [], 'detached' => [],
- ];
-
- $records = $this->formatRecordsList($this->parseIds($ids));
-
- // 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
- // and removing all of those rows from this "intermediate" joining table.
- $detach = array_values(array_intersect(
- $this->newPivotQuery()->pluck($this->relatedPivotKey)->all(),
- array_keys($records)
- ));
-
- if (count($detach) > 0) {
- $this->detach($detach, false);
-
- $changes['detached'] = $this->castKeys($detach);
- }
-
- // Finally, for all of the records which were not "detached", we'll attach the
- // records into the intermediate table. Then, we will add those attaches to
- // this change list and get ready to return these results to the callers.
- $attach = array_diff_key($records, array_flip($detach));
-
- if (count($attach) > 0) {
- $this->attach($attach, [], false);
-
- $changes['attached'] = array_keys($attach);
- }
-
- // Once we have finished attaching or detaching the records, we will see if we
- // have done any attaching or detaching, and if we have we will touch these
- // relationships if they are configured to touch on any database updates.
- if ($touch && (count($changes['attached']) ||
- count($changes['detached']))) {
- $this->touchIfTouching();
- }
-
- return $changes;
- }
-
- /**
- * Sync the intermediate tables with a list of IDs without detaching.
- *
- * @param \Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection|array $ids
- * @return array
- */
- public function syncWithoutDetaching($ids)
- {
- return $this->sync($ids, false);
- }
-
- /**
- * Sync the intermediate tables with a list of IDs or collection of models.
- *
- * @param \Illuminate\Database\Eloquent\Collection|\Illuminate\Support\Collection|array $ids
- * @param bool $detaching
- * @return array
- */
- public function sync($ids, $detaching = true)
- {
- $changes = [
- 'attached' => [], 'detached' => [], 'updated' => [],
- ];
-
- // 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->relatedPivotKey
- )->all();
-
- $detach = array_diff($current, array_keys(
- $records = $this->formatRecordsList($this->parseIds($ids))
- ));
-
- // 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
- // array of the new IDs given to the method which will complete the sync.
- if ($detaching && count($detach) > 0) {
- $this->detach($detach);
-
- $changes['detached'] = $this->castKeys($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)
- );
-
- // Once we have finished attaching or detaching the records, we will see if we
- // have done any attaching or detaching, and if we have we will touch these
- // relationships if they are configured to touch on any database updates.
- if (count($changes['attached']) ||
- count($changes['updated'])) {
- $this->touchIfTouching();
- }
-
- return $changes;
- }
-
- /**
- * Format the sync / toggle record list so that it is keyed by ID.
- *
- * @param array $records
- * @return array
- */
- protected function formatRecordsList(array $records)
- {
- return collect($records)->mapWithKeys(function ($attributes, $id) {
- if (! is_array($attributes)) {
- list($id, $attributes) = [$attributes, []];
- }
-
- return [$id => $attributes];
- })->all();
- }
-
- /**
- * Attach all of the records that aren't in the given current records.
- *
- * @param array $records
- * @param array $current
- * @param bool $touch
- * @return array
- */
- protected function attachNew(array $records, array $current, $touch = true)
- {
- $changes = ['attached' => [], 'updated' => []];
-
- 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'][] = $this->castKey($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'][] = $this->castKey($id);
- }
- }
-
- return $changes;
- }
-
- /**
- * Update an existing pivot record on the table.
- *
- * @param mixed $id
- * @param array $attributes
- * @param bool $touch
- * @return int
- */
- public function updateExistingPivot($id, array $attributes, $touch = true)
- {
- if (in_array($this->updatedAt(), $this->pivotColumns)) {
- $attributes = $this->addTimestampsToAttachment($attributes, true);
- }
-
- $updated = $this->newPivotStatementForId($this->parseId($id))->update(
- $this->castAttributes($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 = [], $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 ($touch) {
- $this->touchIfTouching();
- }
- }
-
- /**
- * Create an array of records to insert into the pivot table.
- *
- * @param array $ids
- * @param array $attributes
- * @return array
- */
- protected function formatAttachRecords($ids, array $attributes)
- {
- $records = [];
-
- $hasTimestamps = ($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->formatAttachRecord(
- $key, $value, $attributes, $hasTimestamps
- );
- }
-
- return $records;
- }
-
- /**
- * Create a full attachment record payload.
- *
- * @param int $key
- * @param mixed $value
- * @param array $attributes
- * @param bool $hasTimestamps
- * @return array
- */
- protected function formatAttachRecord($key, $value, $attributes, $hasTimestamps)
- {
- list($id, $attributes) = $this->extractAttachIdAndAttributes($key, $value, $attributes);
-
- return array_merge(
- $this->baseAttachRecord($id, $hasTimestamps), $this->castAttributes($attributes)
- );
- }
-
- /**
- * Get the attach record ID and extra attributes.
- *
- * @param mixed $key
- * @param mixed $value
- * @param array $attributes
- * @return array
- */
- protected function extractAttachIdAndAttributes($key, $value, array $attributes)
- {
- return is_array($value)
- ? [$key, array_merge($value, $attributes)]
- : [$value, $attributes];
- }
-
- /**
- * Create a new pivot attachment record.
- *
- * @param int $id
- * @param bool $timed
- * @return array
- */
- protected function baseAttachRecord($id, $timed)
- {
- $record[$this->relatedPivotKey] = $id;
-
- $record[$this->foreignPivotKey] = $this->parent->{$this->parentKey};
-
- // 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->addTimestampsToAttachment($record);
- }
-
- foreach ($this->pivotValues as $value) {
- $record[$value['column']] = $value['value'];
- }
-
- return $record;
- }
-
- /**
- * Set the creation and update timestamps on an attach record.
- *
- * @param array $record
- * @param bool $exists
- * @return array
- */
- protected function addTimestampsToAttachment(array $record, $exists = false)
- {
- $fresh = $this->parent->freshTimestamp();
-
- if ($this->using) {
- $pivotModel = new $this->using;
-
- $fresh = $fresh->format($pivotModel->getDateFormat());
- }
-
- if (! $exists && $this->hasPivotColumn($this->createdAt())) {
- $record[$this->createdAt()] = $fresh;
- }
-
- if ($this->hasPivotColumn($this->updatedAt())) {
- $record[$this->updatedAt()] = $fresh;
- }
-
- return $record;
- }
-
- /**
- * 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);
- }
-
- /**
- * Detach models from the relationship.
- *
- * @param mixed $ids
- * @param bool $touch
- * @return int
- */
- public function detach($ids = null, $touch = true)
- {
- $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 (empty($ids)) {
- return 0;
- }
-
- $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();
-
- if ($touch) {
- $this->touchIfTouching();
- }
-
- return $results;
- }
-
- /**
- * Create a new pivot model instance.
- *
- * @param array $attributes
- * @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, $this->using
- );
-
- return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);
- }
-
- /**
- * Create a new existing pivot model instance.
- *
- * @param array $attributes
- * @return \Illuminate\Database\Eloquent\Relations\Pivot
- */
- public function newExistingPivot(array $attributes = [])
- {
- return $this->newPivot($attributes, true);
- }
-
- /**
- * 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->relatedPivotKey, $id);
- }
-
- /**
- * Create a new query builder for the pivot table.
- *
- * @return \Illuminate\Database\Query\Builder
- */
- protected function newPivotQuery()
- {
- $query = $this->newPivotStatement();
-
- foreach ($this->pivotWheres as $arguments) {
- call_user_func_array([$query, 'where'], $arguments);
- }
-
- foreach ($this->pivotWhereIns as $arguments) {
- call_user_func_array([$query, 'whereIn'], $arguments);
- }
-
- return $query->where($this->foreignPivotKey, $this->parent->{$this->parentKey});
- }
-
- /**
- * Set the columns on the pivot table to retrieve.
- *
- * @param array|mixed $columns
- * @return $this
- */
- public function withPivot($columns)
- {
- $this->pivotColumns = array_merge(
- $this->pivotColumns, is_array($columns) ? $columns : func_get_args()
- );
-
- return $this;
- }
-
- /**
- * Get all of the IDs from the given mixed value.
- *
- * @param mixed $value
- * @return array
- */
- protected function parseIds($value)
- {
- if ($value instanceof Model) {
- return [$value->getKey()];
- }
-
- if ($value instanceof Collection) {
- return $value->modelKeys();
- }
-
- if ($value instanceof BaseCollection) {
- return $value->toArray();
- }
-
- return (array) $value;
- }
-
- /**
- * Get the ID from the given mixed value.
- *
- * @param mixed $value
- * @return mixed
- */
- protected function parseId($value)
- {
- return $value instanceof Model ? $value->getKey() : $value;
- }
-
- /**
- * Cast the given keys to integers if they are numeric and string otherwise.
- *
- * @param array $keys
- * @return array
- */
- protected function castKeys(array $keys)
- {
- return (array) array_map(function ($v) {
- return $this->castKey($v);
- }, $keys);
- }
-
- /**
- * Cast the given key to an integer if it is numeric.
- *
- * @param mixed $key
- * @return mixed
- */
- protected function castKey($key)
- {
- return is_numeric($key) ? (int) $key : (string) $key;
- }
-
- /**
- * Cast the given pivot attributes.
- *
- * @param array $attributes
- * @return array
- */
- protected function castAttributes($attributes)
- {
- return $this->using
- ? $this->newPivot()->fill($attributes)->getAttributes()
- : $attributes;
- }
- }
|