PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php

https://gitlab.com/Pasantias/pasantiasASLG
PHP | 1255 lines | 517 code | 196 blank | 542 comment | 40 complexity | 80c831eda72e39ff02894382db6436c7 MD5 | raw file
  1. <?php
  2. namespace Illuminate\Database\Eloquent\Relations;
  3. use Illuminate\Support\Arr;
  4. use Illuminate\Support\Str;
  5. use Illuminate\Database\Eloquent\Model;
  6. use Illuminate\Database\Eloquent\Builder;
  7. use Illuminate\Database\Query\Expression;
  8. use Illuminate\Database\Eloquent\Collection;
  9. use Illuminate\Database\Eloquent\ModelNotFoundException;
  10. class BelongsToMany extends Relation
  11. {
  12. /**
  13. * The intermediate table for the relation.
  14. *
  15. * @var string
  16. */
  17. protected $table;
  18. /**
  19. * The foreign key of the parent model.
  20. *
  21. * @var string
  22. */
  23. protected $foreignKey;
  24. /**
  25. * The associated key of the relation.
  26. *
  27. * @var string
  28. */
  29. protected $otherKey;
  30. /**
  31. * The "name" of the relationship.
  32. *
  33. * @var string
  34. */
  35. protected $relationName;
  36. /**
  37. * The pivot table columns to retrieve.
  38. *
  39. * @var array
  40. */
  41. protected $pivotColumns = [];
  42. /**
  43. * Any pivot table restrictions.
  44. *
  45. * @var array
  46. */
  47. protected $pivotWheres = [];
  48. /**
  49. * The custom pivot table column for the created_at timestamp.
  50. *
  51. * @var string
  52. */
  53. protected $pivotCreatedAt;
  54. /**
  55. * The custom pivot table column for the updated_at timestamp.
  56. *
  57. * @var string
  58. */
  59. protected $pivotUpdatedAt;
  60. /**
  61. * Create a new belongs to many relationship instance.
  62. *
  63. * @param \Illuminate\Database\Eloquent\Builder $query
  64. * @param \Illuminate\Database\Eloquent\Model $parent
  65. * @param string $table
  66. * @param string $foreignKey
  67. * @param string $otherKey
  68. * @param string $relationName
  69. * @return void
  70. */
  71. public function __construct(Builder $query, Model $parent, $table, $foreignKey, $otherKey, $relationName = null)
  72. {
  73. $this->table = $table;
  74. $this->otherKey = $otherKey;
  75. $this->foreignKey = $foreignKey;
  76. $this->relationName = $relationName;
  77. parent::__construct($query, $parent);
  78. }
  79. /**
  80. * Get the results of the relationship.
  81. *
  82. * @return mixed
  83. */
  84. public function getResults()
  85. {
  86. return $this->get();
  87. }
  88. /**
  89. * Set a where clause for a pivot table column.
  90. *
  91. * @param string $column
  92. * @param string $operator
  93. * @param mixed $value
  94. * @param string $boolean
  95. * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
  96. */
  97. public function wherePivot($column, $operator = null, $value = null, $boolean = 'and')
  98. {
  99. $this->pivotWheres[] = func_get_args();
  100. return $this->where($this->table.'.'.$column, $operator, $value, $boolean);
  101. }
  102. /**
  103. * Set an or where clause for a pivot table column.
  104. *
  105. * @param string $column
  106. * @param string $operator
  107. * @param mixed $value
  108. * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
  109. */
  110. public function orWherePivot($column, $operator = null, $value = null)
  111. {
  112. return $this->wherePivot($column, $operator, $value, 'or');
  113. }
  114. /**
  115. * Execute the query and get the first result.
  116. *
  117. * @param array $columns
  118. * @return mixed
  119. */
  120. public function first($columns = ['*'])
  121. {
  122. $results = $this->take(1)->get($columns);
  123. return count($results) > 0 ? $results->first() : null;
  124. }
  125. /**
  126. * Execute the query and get the first result or throw an exception.
  127. *
  128. * @param array $columns
  129. * @return \Illuminate\Database\Eloquent\Model|static
  130. *
  131. * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
  132. */
  133. public function firstOrFail($columns = ['*'])
  134. {
  135. if (! is_null($model = $this->first($columns))) {
  136. return $model;
  137. }
  138. throw new ModelNotFoundException;
  139. }
  140. /**
  141. * Execute the query as a "select" statement.
  142. *
  143. * @param array $columns
  144. * @return \Illuminate\Database\Eloquent\Collection
  145. */
  146. public function get($columns = ['*'])
  147. {
  148. // First we'll add the proper select columns onto the query so it is run with
  149. // the proper columns. Then, we will get the results and hydrate out pivot
  150. // models with the result of those columns as a separate model relation.
  151. $columns = $this->query->getQuery()->columns ? [] : $columns;
  152. $select = $this->getSelectColumns($columns);
  153. $models = $this->query->addSelect($select)->getModels();
  154. $this->hydratePivotRelation($models);
  155. // If we actually found models we will also eager load any relationships that
  156. // have been specified as needing to be eager loaded. This will solve the
  157. // n + 1 query problem for the developer and also increase performance.
  158. if (count($models) > 0) {
  159. $models = $this->query->eagerLoadRelations($models);
  160. }
  161. return $this->related->newCollection($models);
  162. }
  163. /**
  164. * Get a paginator for the "select" statement.
  165. *
  166. * @param int $perPage
  167. * @param array $columns
  168. * @param string $pageName
  169. * @param int|null $page
  170. * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
  171. */
  172. public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
  173. {
  174. $this->query->addSelect($this->getSelectColumns($columns));
  175. $paginator = $this->query->paginate($perPage, $columns, $pageName, $page);
  176. $this->hydratePivotRelation($paginator->items());
  177. return $paginator;
  178. }
  179. /**
  180. * Paginate the given query into a simple paginator.
  181. *
  182. * @param int $perPage
  183. * @param array $columns
  184. * @param string $pageName
  185. * @return \Illuminate\Contracts\Pagination\Paginator
  186. */
  187. public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page')
  188. {
  189. $this->query->addSelect($this->getSelectColumns($columns));
  190. $paginator = $this->query->simplePaginate($perPage, $columns, $pageName);
  191. $this->hydratePivotRelation($paginator->items());
  192. return $paginator;
  193. }
  194. /**
  195. * Chunk the results of the query.
  196. *
  197. * @param int $count
  198. * @param callable $callback
  199. * @return bool
  200. */
  201. public function chunk($count, callable $callback)
  202. {
  203. $this->query->addSelect($this->getSelectColumns());
  204. return $this->query->chunk($count, function ($results) use ($callback) {
  205. $this->hydratePivotRelation($results->all());
  206. return $callback($results);
  207. });
  208. }
  209. /**
  210. * Hydrate the pivot table relationship on the models.
  211. *
  212. * @param array $models
  213. * @return void
  214. */
  215. protected function hydratePivotRelation(array $models)
  216. {
  217. // To hydrate the pivot relationship, we will just gather the pivot attributes
  218. // and create a new Pivot model, which is basically a dynamic model that we
  219. // will set the attributes, table, and connections on so it they be used.
  220. foreach ($models as $model) {
  221. $pivot = $this->newExistingPivot($this->cleanPivotAttributes($model));
  222. $model->setRelation('pivot', $pivot);
  223. }
  224. }
  225. /**
  226. * Get the pivot attributes from a model.
  227. *
  228. * @param \Illuminate\Database\Eloquent\Model $model
  229. * @return array
  230. */
  231. protected function cleanPivotAttributes(Model $model)
  232. {
  233. $values = [];
  234. foreach ($model->getAttributes() as $key => $value) {
  235. // To get the pivots attributes we will just take any of the attributes which
  236. // begin with "pivot_" and add those to this arrays, as well as unsetting
  237. // them from the parent's models since they exist in a different table.
  238. if (strpos($key, 'pivot_') === 0) {
  239. $values[substr($key, 6)] = $value;
  240. unset($model->$key);
  241. }
  242. }
  243. return $values;
  244. }
  245. /**
  246. * Set the base constraints on the relation query.
  247. *
  248. * @return void
  249. */
  250. public function addConstraints()
  251. {
  252. $this->setJoin();
  253. if (static::$constraints) {
  254. $this->setWhere();
  255. }
  256. }
  257. /**
  258. * Add the constraints for a relationship count query.
  259. *
  260. * @param \Illuminate\Database\Eloquent\Builder $query
  261. * @param \Illuminate\Database\Eloquent\Builder $parent
  262. * @return \Illuminate\Database\Eloquent\Builder
  263. */
  264. public function getRelationCountQuery(Builder $query, Builder $parent)
  265. {
  266. if ($parent->getQuery()->from == $query->getQuery()->from) {
  267. return $this->getRelationCountQueryForSelfJoin($query, $parent);
  268. }
  269. $this->setJoin($query);
  270. return parent::getRelationCountQuery($query, $parent);
  271. }
  272. /**
  273. * Add the constraints for a relationship count query on the same table.
  274. *
  275. * @param \Illuminate\Database\Eloquent\Builder $query
  276. * @param \Illuminate\Database\Eloquent\Builder $parent
  277. * @return \Illuminate\Database\Eloquent\Builder
  278. */
  279. public function getRelationCountQueryForSelfJoin(Builder $query, Builder $parent)
  280. {
  281. $query->select(new Expression('count(*)'));
  282. $query->from($this->related->getTable().' as '.$hash = $this->getRelationCountHash());
  283. $this->related->setTable($hash);
  284. $this->setJoin($query);
  285. return parent::getRelationCountQuery($query, $parent);
  286. }
  287. /**
  288. * Get a relationship join table hash.
  289. *
  290. * @return string
  291. */
  292. public function getRelationCountHash()
  293. {
  294. return 'self_'.md5(microtime(true));
  295. }
  296. /**
  297. * Set the select clause for the relation query.
  298. *
  299. * @param array $columns
  300. * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
  301. */
  302. protected function getSelectColumns(array $columns = ['*'])
  303. {
  304. if ($columns == ['*']) {
  305. $columns = [$this->related->getTable().'.*'];
  306. }
  307. return array_merge($columns, $this->getAliasedPivotColumns());
  308. }
  309. /**
  310. * Get the pivot columns for the relation.
  311. *
  312. * @return array
  313. */
  314. protected function getAliasedPivotColumns()
  315. {
  316. $defaults = [$this->foreignKey, $this->otherKey];
  317. // We need to alias all of the pivot columns with the "pivot_" prefix so we
  318. // can easily extract them out of the models and put them into the pivot
  319. // relationships when they are retrieved and hydrated into the models.
  320. $columns = [];
  321. foreach (array_merge($defaults, $this->pivotColumns) as $column) {
  322. $columns[] = $this->table.'.'.$column.' as pivot_'.$column;
  323. }
  324. return array_unique($columns);
  325. }
  326. /**
  327. * Determine whether the given column is defined as a pivot column.
  328. *
  329. * @param string $column
  330. * @return bool
  331. */
  332. protected function hasPivotColumn($column)
  333. {
  334. return in_array($column, $this->pivotColumns);
  335. }
  336. /**
  337. * Set the join clause for the relation query.
  338. *
  339. * @param \Illuminate\Database\Eloquent\Builder|null $query
  340. * @return $this
  341. */
  342. protected function setJoin($query = null)
  343. {
  344. $query = $query ?: $this->query;
  345. // We need to join to the intermediate table on the related model's primary
  346. // key column with the intermediate table's foreign key for the related
  347. // model instance. Then we can set the "where" for the parent models.
  348. $baseTable = $this->related->getTable();
  349. $key = $baseTable.'.'.$this->related->getKeyName();
  350. $query->join($this->table, $key, '=', $this->getOtherKey());
  351. return $this;
  352. }
  353. /**
  354. * Set the where clause for the relation query.
  355. *
  356. * @return $this
  357. */
  358. protected function setWhere()
  359. {
  360. $foreign = $this->getForeignKey();
  361. $this->query->where($foreign, '=', $this->parent->getKey());
  362. return $this;
  363. }
  364. /**
  365. * Set the constraints for an eager load of the relation.
  366. *
  367. * @param array $models
  368. * @return void
  369. */
  370. public function addEagerConstraints(array $models)
  371. {
  372. $this->query->whereIn($this->getForeignKey(), $this->getKeys($models));
  373. }
  374. /**
  375. * Initialize the relation on a set of models.
  376. *
  377. * @param array $models
  378. * @param string $relation
  379. * @return array
  380. */
  381. public function initRelation(array $models, $relation)
  382. {
  383. foreach ($models as $model) {
  384. $model->setRelation($relation, $this->related->newCollection());
  385. }
  386. return $models;
  387. }
  388. /**
  389. * Match the eagerly loaded results to their parents.
  390. *
  391. * @param array $models
  392. * @param \Illuminate\Database\Eloquent\Collection $results
  393. * @param string $relation
  394. * @return array
  395. */
  396. public function match(array $models, Collection $results, $relation)
  397. {
  398. $dictionary = $this->buildDictionary($results);
  399. // Once we have an array dictionary of child objects we can easily match the
  400. // children back to their parent using the dictionary and the keys on the
  401. // the parent models. Then we will return the hydrated models back out.
  402. foreach ($models as $model) {
  403. if (isset($dictionary[$key = $model->getKey()])) {
  404. $collection = $this->related->newCollection($dictionary[$key]);
  405. $model->setRelation($relation, $collection);
  406. }
  407. }
  408. return $models;
  409. }
  410. /**
  411. * Build model dictionary keyed by the relation's foreign key.
  412. *
  413. * @param \Illuminate\Database\Eloquent\Collection $results
  414. * @return array
  415. */
  416. protected function buildDictionary(Collection $results)
  417. {
  418. $foreign = $this->foreignKey;
  419. // First we will build a dictionary of child models keyed by the foreign key
  420. // of the relation so that we will easily and quickly match them to their
  421. // parents without having a possibly slow inner loops for every models.
  422. $dictionary = [];
  423. foreach ($results as $result) {
  424. $dictionary[$result->pivot->$foreign][] = $result;
  425. }
  426. return $dictionary;
  427. }
  428. /**
  429. * Touch all of the related models for the relationship.
  430. *
  431. * E.g.: Touch all roles associated with this user.
  432. *
  433. * @return void
  434. */
  435. public function touch()
  436. {
  437. $key = $this->getRelated()->getKeyName();
  438. $columns = $this->getRelatedFreshUpdate();
  439. // If we actually have IDs for the relation, we will run the query to update all
  440. // the related model's timestamps, to make sure these all reflect the changes
  441. // to the parent models. This will help us keep any caching synced up here.
  442. $ids = $this->getRelatedIds();
  443. if (count($ids) > 0) {
  444. $this->getRelated()->newQuery()->whereIn($key, $ids)->update($columns);
  445. }
  446. }
  447. /**
  448. * Get all of the IDs for the related models.
  449. *
  450. * @return \Illuminate\Support\Collection
  451. */
  452. public function getRelatedIds()
  453. {
  454. $related = $this->getRelated();
  455. $fullKey = $related->getQualifiedKeyName();
  456. return $this->getQuery()->select($fullKey)->lists($related->getKeyName());
  457. }
  458. /**
  459. * Save a new model and attach it to the parent model.
  460. *
  461. * @param \Illuminate\Database\Eloquent\Model $model
  462. * @param array $joining
  463. * @param bool $touch
  464. * @return \Illuminate\Database\Eloquent\Model
  465. */
  466. public function save(Model $model, array $joining = [], $touch = true)
  467. {
  468. $model->save(['touch' => false]);
  469. $this->attach($model->getKey(), $joining, $touch);
  470. return $model;
  471. }
  472. /**
  473. * Save an array of new models and attach them to the parent model.
  474. *
  475. * @param \Illuminate\Support\Collection|array $models
  476. * @param array $joinings
  477. * @return array
  478. */
  479. public function saveMany($models, array $joinings = [])
  480. {
  481. foreach ($models as $key => $model) {
  482. $this->save($model, (array) Arr::get($joinings, $key), false);
  483. }
  484. $this->touchIfTouching();
  485. return $models;
  486. }
  487. /**
  488. * Find a related model by its primary key.
  489. *
  490. * @param mixed $id
  491. * @param array $columns
  492. * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
  493. */
  494. public function find($id, $columns = ['*'])
  495. {
  496. if (is_array($id)) {
  497. return $this->findMany($id, $columns);
  498. }
  499. $this->where($this->getRelated()->getQualifiedKeyName(), '=', $id);
  500. return $this->first($columns);
  501. }
  502. /**
  503. * Find multiple related models by their primary keys.
  504. *
  505. * @param mixed $ids
  506. * @param array $columns
  507. * @return \Illuminate\Database\Eloquent\Collection
  508. */
  509. public function findMany($ids, $columns = ['*'])
  510. {
  511. if (empty($ids)) {
  512. return $this->getRelated()->newCollection();
  513. }
  514. $this->whereIn($this->getRelated()->getQualifiedKeyName(), $ids);
  515. return $this->get($columns);
  516. }
  517. /**
  518. * Find a related model by its primary key or throw an exception.
  519. *
  520. * @param mixed $id
  521. * @param array $columns
  522. * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
  523. *
  524. * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
  525. */
  526. public function findOrFail($id, $columns = ['*'])
  527. {
  528. $result = $this->find($id, $columns);
  529. if (is_array($id)) {
  530. if (count($result) == count(array_unique($id))) {
  531. return $result;
  532. }
  533. } elseif (! is_null($result)) {
  534. return $result;
  535. }
  536. throw (new ModelNotFoundException)->setModel(get_class($this->parent));
  537. }
  538. /**
  539. * Find a related model by its primary key or return new instance of the related model.
  540. *
  541. * @param mixed $id
  542. * @param array $columns
  543. * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model
  544. */
  545. public function findOrNew($id, $columns = ['*'])
  546. {
  547. if (is_null($instance = $this->find($id, $columns))) {
  548. $instance = $this->getRelated()->newInstance();
  549. }
  550. return $instance;
  551. }
  552. /**
  553. * Get the first related model record matching the attributes or instantiate it.
  554. *
  555. * @param array $attributes
  556. * @return \Illuminate\Database\Eloquent\Model
  557. */
  558. public function firstOrNew(array $attributes)
  559. {
  560. if (is_null($instance = $this->where($attributes)->first())) {
  561. $instance = $this->related->newInstance($attributes);
  562. }
  563. return $instance;
  564. }
  565. /**
  566. * Get the first related record matching the attributes or create it.
  567. *
  568. * @param array $attributes
  569. * @param array $joining
  570. * @param bool $touch
  571. * @return \Illuminate\Database\Eloquent\Model
  572. */
  573. public function firstOrCreate(array $attributes, array $joining = [], $touch = true)
  574. {
  575. if (is_null($instance = $this->where($attributes)->first())) {
  576. $instance = $this->create($attributes, $joining, $touch);
  577. }
  578. return $instance;
  579. }
  580. /**
  581. * Create or update a related record matching the attributes, and fill it with values.
  582. *
  583. * @param array $attributes
  584. * @param array $values
  585. * @param array $joining
  586. * @param bool $touch
  587. * @return \Illuminate\Database\Eloquent\Model
  588. */
  589. public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true)
  590. {
  591. if (is_null($instance = $this->where($attributes)->first())) {
  592. return $this->create($values, $joining, $touch);
  593. }
  594. $instance->fill($values);
  595. $instance->save(['touch' => false]);
  596. return $instance;
  597. }
  598. /**
  599. * Create a new instance of the related model.
  600. *
  601. * @param array $attributes
  602. * @param array $joining
  603. * @param bool $touch
  604. * @return \Illuminate\Database\Eloquent\Model
  605. */
  606. public function create(array $attributes, array $joining = [], $touch = true)
  607. {
  608. $instance = $this->related->newInstance($attributes);
  609. // Once we save the related model, we need to attach it to the base model via
  610. // through intermediate table so we'll use the existing "attach" method to
  611. // accomplish this which will insert the record and any more attributes.
  612. $instance->save(['touch' => false]);
  613. $this->attach($instance->getKey(), $joining, $touch);
  614. return $instance;
  615. }
  616. /**
  617. * Create an array of new instances of the related models.
  618. *
  619. * @param array $records
  620. * @param array $joinings
  621. * @return array
  622. */
  623. public function createMany(array $records, array $joinings = [])
  624. {
  625. $instances = [];
  626. foreach ($records as $key => $record) {
  627. $instances[] = $this->create($record, (array) Arr::get($joinings, $key), false);
  628. }
  629. $this->touchIfTouching();
  630. return $instances;
  631. }
  632. /**
  633. * Sync the intermediate tables with a list of IDs or collection of models.
  634. *
  635. * @param \Illuminate\Database\Eloquent\Collection|array $ids
  636. * @param bool $detaching
  637. * @return array
  638. */
  639. public function sync($ids, $detaching = true)
  640. {
  641. $changes = [
  642. 'attached' => [], 'detached' => [], 'updated' => [],
  643. ];
  644. if ($ids instanceof Collection) {
  645. $ids = $ids->modelKeys();
  646. }
  647. // First we need to attach any of the associated models that are not currently
  648. // in this joining table. We'll spin through the given IDs, checking to see
  649. // if they exist in the array of current ones, and if not we will insert.
  650. $current = $this->newPivotQuery()->lists($this->otherKey);
  651. $records = $this->formatSyncList($ids);
  652. $detach = array_diff($current, array_keys($records));
  653. // Next, we will take the differences of the currents and given IDs and detach
  654. // all of the entities that exist in the "current" array but are not in the
  655. // the array of the IDs given to the method which will complete the sync.
  656. if ($detaching && count($detach) > 0) {
  657. $this->detach($detach);
  658. $changes['detached'] = (array) array_map(function ($v) {
  659. return is_numeric($v) ? (int) $v : (string) $v;
  660. }, $detach);
  661. }
  662. // Now we are finally ready to attach the new records. Note that we'll disable
  663. // touching until after the entire operation is complete so we don't fire a
  664. // ton of touch operations until we are totally done syncing the records.
  665. $changes = array_merge(
  666. $changes, $this->attachNew($records, $current, false)
  667. );
  668. if (count($changes['attached']) || count($changes['updated'])) {
  669. $this->touchIfTouching();
  670. }
  671. return $changes;
  672. }
  673. /**
  674. * Format the sync list so that it is keyed by ID.
  675. *
  676. * @param array $records
  677. * @return array
  678. */
  679. protected function formatSyncList(array $records)
  680. {
  681. $results = [];
  682. foreach ($records as $id => $attributes) {
  683. if (! is_array($attributes)) {
  684. list($id, $attributes) = [$attributes, []];
  685. }
  686. $results[$id] = $attributes;
  687. }
  688. return $results;
  689. }
  690. /**
  691. * Attach all of the IDs that aren't in the current array.
  692. *
  693. * @param array $records
  694. * @param array $current
  695. * @param bool $touch
  696. * @return array
  697. */
  698. protected function attachNew(array $records, array $current, $touch = true)
  699. {
  700. $changes = ['attached' => [], 'updated' => []];
  701. foreach ($records as $id => $attributes) {
  702. // If the ID is not in the list of existing pivot IDs, we will insert a new pivot
  703. // record, otherwise, we will just update this existing record on this joining
  704. // table, so that the developers will easily update these records pain free.
  705. if (! in_array($id, $current)) {
  706. $this->attach($id, $attributes, $touch);
  707. $changes['attached'][] = is_numeric($id) ? (int) $id : (string) $id;
  708. }
  709. // Now we'll try to update an existing pivot record with the attributes that were
  710. // given to the method. If the model is actually updated we will add it to the
  711. // list of updated pivot records so we return them back out to the consumer.
  712. elseif (count($attributes) > 0 &&
  713. $this->updateExistingPivot($id, $attributes, $touch)) {
  714. $changes['updated'][] = is_numeric($id) ? (int) $id : (string) $id;
  715. }
  716. }
  717. return $changes;
  718. }
  719. /**
  720. * Update an existing pivot record on the table.
  721. *
  722. * @param mixed $id
  723. * @param array $attributes
  724. * @param bool $touch
  725. * @return int
  726. */
  727. public function updateExistingPivot($id, array $attributes, $touch = true)
  728. {
  729. if (in_array($this->updatedAt(), $this->pivotColumns)) {
  730. $attributes = $this->setTimestampsOnAttach($attributes, true);
  731. }
  732. $updated = $this->newPivotStatementForId($id)->update($attributes);
  733. if ($touch) {
  734. $this->touchIfTouching();
  735. }
  736. return $updated;
  737. }
  738. /**
  739. * Attach a model to the parent.
  740. *
  741. * @param mixed $id
  742. * @param array $attributes
  743. * @param bool $touch
  744. * @return void
  745. */
  746. public function attach($id, array $attributes = [], $touch = true)
  747. {
  748. if ($id instanceof Model) {
  749. $id = $id->getKey();
  750. }
  751. $query = $this->newPivotStatement();
  752. $query->insert($this->createAttachRecords((array) $id, $attributes));
  753. if ($touch) {
  754. $this->touchIfTouching();
  755. }
  756. }
  757. /**
  758. * Create an array of records to insert into the pivot table.
  759. *
  760. * @param array $ids
  761. * @param array $attributes
  762. * @return array
  763. */
  764. protected function createAttachRecords($ids, array $attributes)
  765. {
  766. $records = [];
  767. $timed = ($this->hasPivotColumn($this->createdAt()) ||
  768. $this->hasPivotColumn($this->updatedAt()));
  769. // To create the attachment records, we will simply spin through the IDs given
  770. // and create a new record to insert for each ID. Each ID may actually be a
  771. // key in the array, with extra attributes to be placed in other columns.
  772. foreach ($ids as $key => $value) {
  773. $records[] = $this->attacher($key, $value, $attributes, $timed);
  774. }
  775. return $records;
  776. }
  777. /**
  778. * Create a full attachment record payload.
  779. *
  780. * @param int $key
  781. * @param mixed $value
  782. * @param array $attributes
  783. * @param bool $timed
  784. * @return array
  785. */
  786. protected function attacher($key, $value, $attributes, $timed)
  787. {
  788. list($id, $extra) = $this->getAttachId($key, $value, $attributes);
  789. // To create the attachment records, we will simply spin through the IDs given
  790. // and create a new record to insert for each ID. Each ID may actually be a
  791. // key in the array, with extra attributes to be placed in other columns.
  792. $record = $this->createAttachRecord($id, $timed);
  793. return array_merge($record, $extra);
  794. }
  795. /**
  796. * Get the attach record ID and extra attributes.
  797. *
  798. * @param mixed $key
  799. * @param mixed $value
  800. * @param array $attributes
  801. * @return array
  802. */
  803. protected function getAttachId($key, $value, array $attributes)
  804. {
  805. if (is_array($value)) {
  806. return [$key, array_merge($value, $attributes)];
  807. }
  808. return [$value, $attributes];
  809. }
  810. /**
  811. * Create a new pivot attachment record.
  812. *
  813. * @param int $id
  814. * @param bool $timed
  815. * @return array
  816. */
  817. protected function createAttachRecord($id, $timed)
  818. {
  819. $record[$this->foreignKey] = $this->parent->getKey();
  820. $record[$this->otherKey] = $id;
  821. // If the record needs to have creation and update timestamps, we will make
  822. // them by calling the parent model's "freshTimestamp" method which will
  823. // provide us with a fresh timestamp in this model's preferred format.
  824. if ($timed) {
  825. $record = $this->setTimestampsOnAttach($record);
  826. }
  827. return $record;
  828. }
  829. /**
  830. * Set the creation and update timestamps on an attach record.
  831. *
  832. * @param array $record
  833. * @param bool $exists
  834. * @return array
  835. */
  836. protected function setTimestampsOnAttach(array $record, $exists = false)
  837. {
  838. $fresh = $this->parent->freshTimestamp();
  839. if (! $exists && $this->hasPivotColumn($this->createdAt())) {
  840. $record[$this->createdAt()] = $fresh;
  841. }
  842. if ($this->hasPivotColumn($this->updatedAt())) {
  843. $record[$this->updatedAt()] = $fresh;
  844. }
  845. return $record;
  846. }
  847. /**
  848. * Detach models from the relationship.
  849. *
  850. * @param int|array $ids
  851. * @param bool $touch
  852. * @return int
  853. */
  854. public function detach($ids = [], $touch = true)
  855. {
  856. if ($ids instanceof Model) {
  857. $ids = (array) $ids->getKey();
  858. }
  859. $query = $this->newPivotQuery();
  860. // If associated IDs were passed to the method we will only delete those
  861. // associations, otherwise all of the association ties will be broken.
  862. // We'll return the numbers of affected rows when we do the deletes.
  863. $ids = (array) $ids;
  864. if (count($ids) > 0) {
  865. $query->whereIn($this->otherKey, (array) $ids);
  866. }
  867. // Once we have all of the conditions set on the statement, we are ready
  868. // to run the delete on the pivot table. Then, if the touch parameter
  869. // is true, we will go ahead and touch all related models to sync.
  870. $results = $query->delete();
  871. if ($touch) {
  872. $this->touchIfTouching();
  873. }
  874. return $results;
  875. }
  876. /**
  877. * If we're touching the parent model, touch.
  878. *
  879. * @return void
  880. */
  881. public function touchIfTouching()
  882. {
  883. if ($this->touchingParent()) {
  884. $this->getParent()->touch();
  885. }
  886. if ($this->getParent()->touches($this->relationName)) {
  887. $this->touch();
  888. }
  889. }
  890. /**
  891. * Determine if we should touch the parent on sync.
  892. *
  893. * @return bool
  894. */
  895. protected function touchingParent()
  896. {
  897. return $this->getRelated()->touches($this->guessInverseRelation());
  898. }
  899. /**
  900. * Attempt to guess the name of the inverse of the relation.
  901. *
  902. * @return string
  903. */
  904. protected function guessInverseRelation()
  905. {
  906. return Str::camel(Str::plural(class_basename($this->getParent())));
  907. }
  908. /**
  909. * Create a new query builder for the pivot table.
  910. *
  911. * @return \Illuminate\Database\Query\Builder
  912. */
  913. protected function newPivotQuery()
  914. {
  915. $query = $this->newPivotStatement();
  916. foreach ($this->pivotWheres as $whereArgs) {
  917. call_user_func_array([$query, 'where'], $whereArgs);
  918. }
  919. return $query->where($this->foreignKey, $this->parent->getKey());
  920. }
  921. /**
  922. * Get a new plain query builder for the pivot table.
  923. *
  924. * @return \Illuminate\Database\Query\Builder
  925. */
  926. public function newPivotStatement()
  927. {
  928. return $this->query->getQuery()->newQuery()->from($this->table);
  929. }
  930. /**
  931. * Get a new pivot statement for a given "other" ID.
  932. *
  933. * @param mixed $id
  934. * @return \Illuminate\Database\Query\Builder
  935. */
  936. public function newPivotStatementForId($id)
  937. {
  938. return $this->newPivotQuery()->where($this->otherKey, $id);
  939. }
  940. /**
  941. * Create a new pivot model instance.
  942. *
  943. * @param array $attributes
  944. * @param bool $exists
  945. * @return \Illuminate\Database\Eloquent\Relations\Pivot
  946. */
  947. public function newPivot(array $attributes = [], $exists = false)
  948. {
  949. $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists);
  950. return $pivot->setPivotKeys($this->foreignKey, $this->otherKey);
  951. }
  952. /**
  953. * Create a new existing pivot model instance.
  954. *
  955. * @param array $attributes
  956. * @return \Illuminate\Database\Eloquent\Relations\Pivot
  957. */
  958. public function newExistingPivot(array $attributes = [])
  959. {
  960. return $this->newPivot($attributes, true);
  961. }
  962. /**
  963. * Set the columns on the pivot table to retrieve.
  964. *
  965. * @param array|mixed $columns
  966. * @return $this
  967. */
  968. public function withPivot($columns)
  969. {
  970. $columns = is_array($columns) ? $columns : func_get_args();
  971. $this->pivotColumns = array_merge($this->pivotColumns, $columns);
  972. return $this;
  973. }
  974. /**
  975. * Specify that the pivot table has creation and update timestamps.
  976. *
  977. * @param mixed $createdAt
  978. * @param mixed $updatedAt
  979. * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
  980. */
  981. public function withTimestamps($createdAt = null, $updatedAt = null)
  982. {
  983. $this->pivotCreatedAt = $createdAt;
  984. $this->pivotUpdatedAt = $updatedAt;
  985. return $this->withPivot($this->createdAt(), $this->updatedAt());
  986. }
  987. /**
  988. * Get the name of the "created at" column.
  989. *
  990. * @return string
  991. */
  992. public function createdAt()
  993. {
  994. return $this->pivotCreatedAt ?: $this->parent->getCreatedAtColumn();
  995. }
  996. /**
  997. * Get the name of the "updated at" column.
  998. *
  999. * @return string
  1000. */
  1001. public function updatedAt()
  1002. {
  1003. return $this->pivotUpdatedAt ?: $this->parent->getUpdatedAtColumn();
  1004. }
  1005. /**
  1006. * Get the related model's updated at column name.
  1007. *
  1008. * @return string
  1009. */
  1010. public function getRelatedFreshUpdate()
  1011. {
  1012. return [$this->related->getUpdatedAtColumn() => $this->related->freshTimestamp()];
  1013. }
  1014. /**
  1015. * Get the key for comparing against the parent key in "has" query.
  1016. *
  1017. * @return string
  1018. */
  1019. public function getHasCompareKey()
  1020. {
  1021. return $this->getForeignKey();
  1022. }
  1023. /**
  1024. * Get the fully qualified foreign key for the relation.
  1025. *
  1026. * @return string
  1027. */
  1028. public function getForeignKey()
  1029. {
  1030. return $this->table.'.'.$this->foreignKey;
  1031. }
  1032. /**
  1033. * Get the fully qualified "other key" for the relation.
  1034. *
  1035. * @return string
  1036. */
  1037. public function getOtherKey()
  1038. {
  1039. return $this->table.'.'.$this->otherKey;
  1040. }
  1041. /**
  1042. * Get the intermediate table for the relationship.
  1043. *
  1044. * @return string
  1045. */
  1046. public function getTable()
  1047. {
  1048. return $this->table;
  1049. }
  1050. /**
  1051. * Get the relationship name for the relationship.
  1052. *
  1053. * @return string
  1054. */
  1055. public function getRelationName()
  1056. {
  1057. return $this->relationName;
  1058. }
  1059. }