PageRenderTime 48ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

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

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