Collection.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. <?php
  2. namespace Illuminate\Database\Eloquent;
  3. use Illuminate\Contracts\Queue\QueueableCollection;
  4. use Illuminate\Contracts\Queue\QueueableEntity;
  5. use Illuminate\Contracts\Support\Arrayable;
  6. use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
  7. use Illuminate\Support\Arr;
  8. use Illuminate\Support\Collection as BaseCollection;
  9. use LogicException;
  10. /**
  11. * @template TKey of array-key
  12. * @template TModel of \Illuminate\Database\Eloquent\Model
  13. *
  14. * @extends \Illuminate\Support\Collection<TKey, TModel>
  15. */
  16. class Collection extends BaseCollection implements QueueableCollection
  17. {
  18. use InteractsWithDictionary;
  19. /**
  20. * Find a model in the collection by key.
  21. *
  22. * @template TFindDefault
  23. *
  24. * @param mixed $key
  25. * @param TFindDefault $default
  26. * @return static<TKey, TModel>|TModel|TFindDefault
  27. */
  28. public function find($key, $default = null)
  29. {
  30. if ($key instanceof Model) {
  31. $key = $key->getKey();
  32. }
  33. if ($key instanceof Arrayable) {
  34. $key = $key->toArray();
  35. }
  36. if (is_array($key)) {
  37. if ($this->isEmpty()) {
  38. return new static;
  39. }
  40. return $this->whereIn($this->first()->getKeyName(), $key);
  41. }
  42. return Arr::first($this->items, fn ($model) => $model->getKey() == $key, $default);
  43. }
  44. /**
  45. * Load a set of relationships onto the collection.
  46. *
  47. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  48. * @return $this
  49. */
  50. public function load($relations)
  51. {
  52. if ($this->isNotEmpty()) {
  53. if (is_string($relations)) {
  54. $relations = func_get_args();
  55. }
  56. $query = $this->first()->newQueryWithoutRelationships()->with($relations);
  57. $this->items = $query->eagerLoadRelations($this->items);
  58. }
  59. return $this;
  60. }
  61. /**
  62. * Load a set of aggregations over relationship's column onto the collection.
  63. *
  64. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  65. * @param string $column
  66. * @param string|null $function
  67. * @return $this
  68. */
  69. public function loadAggregate($relations, $column, $function = null)
  70. {
  71. if ($this->isEmpty()) {
  72. return $this;
  73. }
  74. $models = $this->first()->newModelQuery()
  75. ->whereKey($this->modelKeys())
  76. ->select($this->first()->getKeyName())
  77. ->withAggregate($relations, $column, $function)
  78. ->get()
  79. ->keyBy($this->first()->getKeyName());
  80. $attributes = Arr::except(
  81. array_keys($models->first()->getAttributes()),
  82. $models->first()->getKeyName()
  83. );
  84. $this->each(function ($model) use ($models, $attributes) {
  85. $extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes);
  86. $model->forceFill($extraAttributes)
  87. ->syncOriginalAttributes($attributes)
  88. ->mergeCasts($models->get($model->getKey())->getCasts());
  89. });
  90. return $this;
  91. }
  92. /**
  93. * Load a set of relationship counts onto the collection.
  94. *
  95. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  96. * @return $this
  97. */
  98. public function loadCount($relations)
  99. {
  100. return $this->loadAggregate($relations, '*', 'count');
  101. }
  102. /**
  103. * Load a set of relationship's max column values onto the collection.
  104. *
  105. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  106. * @param string $column
  107. * @return $this
  108. */
  109. public function loadMax($relations, $column)
  110. {
  111. return $this->loadAggregate($relations, $column, 'max');
  112. }
  113. /**
  114. * Load a set of relationship's min column values onto the collection.
  115. *
  116. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  117. * @param string $column
  118. * @return $this
  119. */
  120. public function loadMin($relations, $column)
  121. {
  122. return $this->loadAggregate($relations, $column, 'min');
  123. }
  124. /**
  125. * Load a set of relationship's column summations onto the collection.
  126. *
  127. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  128. * @param string $column
  129. * @return $this
  130. */
  131. public function loadSum($relations, $column)
  132. {
  133. return $this->loadAggregate($relations, $column, 'sum');
  134. }
  135. /**
  136. * Load a set of relationship's average column values onto the collection.
  137. *
  138. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  139. * @param string $column
  140. * @return $this
  141. */
  142. public function loadAvg($relations, $column)
  143. {
  144. return $this->loadAggregate($relations, $column, 'avg');
  145. }
  146. /**
  147. * Load a set of related existences onto the collection.
  148. *
  149. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  150. * @return $this
  151. */
  152. public function loadExists($relations)
  153. {
  154. return $this->loadAggregate($relations, '*', 'exists');
  155. }
  156. /**
  157. * Load a set of relationships onto the collection if they are not already eager loaded.
  158. *
  159. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string>|string $relations
  160. * @return $this
  161. */
  162. public function loadMissing($relations)
  163. {
  164. if (is_string($relations)) {
  165. $relations = func_get_args();
  166. }
  167. foreach ($relations as $key => $value) {
  168. if (is_numeric($key)) {
  169. $key = $value;
  170. }
  171. $segments = explode('.', explode(':', $key)[0]);
  172. if (str_contains($key, ':')) {
  173. $segments[count($segments) - 1] .= ':'.explode(':', $key)[1];
  174. }
  175. $path = [];
  176. foreach ($segments as $segment) {
  177. $path[] = [$segment => $segment];
  178. }
  179. if (is_callable($value)) {
  180. $path[count($segments) - 1][end($segments)] = $value;
  181. }
  182. $this->loadMissingRelation($this, $path);
  183. }
  184. return $this;
  185. }
  186. /**
  187. * Load a relationship path if it is not already eager loaded.
  188. *
  189. * @param \Illuminate\Database\Eloquent\Collection $models
  190. * @param array $path
  191. * @return void
  192. */
  193. protected function loadMissingRelation(self $models, array $path)
  194. {
  195. $relation = array_shift($path);
  196. $name = explode(':', key($relation))[0];
  197. if (is_string(reset($relation))) {
  198. $relation = reset($relation);
  199. }
  200. $models->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name))->load($relation);
  201. if (empty($path)) {
  202. return;
  203. }
  204. $models = $models->pluck($name)->whereNotNull();
  205. if ($models->first() instanceof BaseCollection) {
  206. $models = $models->collapse();
  207. }
  208. $this->loadMissingRelation(new static($models), $path);
  209. }
  210. /**
  211. * Load a set of relationships onto the mixed relationship collection.
  212. *
  213. * @param string $relation
  214. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string> $relations
  215. * @return $this
  216. */
  217. public function loadMorph($relation, $relations)
  218. {
  219. $this->pluck($relation)
  220. ->filter()
  221. ->groupBy(fn ($model) => get_class($model))
  222. ->each(fn ($models, $className) => static::make($models)->load($relations[$className] ?? []));
  223. return $this;
  224. }
  225. /**
  226. * Load a set of relationship counts onto the mixed relationship collection.
  227. *
  228. * @param string $relation
  229. * @param array<array-key, (callable(\Illuminate\Database\Eloquent\Builder): mixed)|string> $relations
  230. * @return $this
  231. */
  232. public function loadMorphCount($relation, $relations)
  233. {
  234. $this->pluck($relation)
  235. ->filter()
  236. ->groupBy(fn ($model) => get_class($model))
  237. ->each(fn ($models, $className) => static::make($models)->loadCount($relations[$className] ?? []));
  238. return $this;
  239. }
  240. /**
  241. * Determine if a key exists in the collection.
  242. *
  243. * @param (callable(TModel, TKey): bool)|TModel|string|int $key
  244. * @param mixed $operator
  245. * @param mixed $value
  246. * @return bool
  247. */
  248. public function contains($key, $operator = null, $value = null)
  249. {
  250. if (func_num_args() > 1 || $this->useAsCallable($key)) {
  251. return parent::contains(...func_get_args());
  252. }
  253. if ($key instanceof Model) {
  254. return parent::contains(fn ($model) => $model->is($key));
  255. }
  256. return parent::contains(fn ($model) => $model->getKey() == $key);
  257. }
  258. /**
  259. * Get the array of primary keys.
  260. *
  261. * @return array<int, array-key>
  262. */
  263. public function modelKeys()
  264. {
  265. return array_map(fn ($model) => $model->getKey(), $this->items);
  266. }
  267. /**
  268. * Merge the collection with the given items.
  269. *
  270. * @param iterable<array-key, TModel> $items
  271. * @return static
  272. */
  273. public function merge($items)
  274. {
  275. $dictionary = $this->getDictionary();
  276. foreach ($items as $item) {
  277. $dictionary[$this->getDictionaryKey($item->getKey())] = $item;
  278. }
  279. return new static(array_values($dictionary));
  280. }
  281. /**
  282. * Run a map over each of the items.
  283. *
  284. * @template TMapValue
  285. *
  286. * @param callable(TModel, TKey): TMapValue $callback
  287. * @return \Illuminate\Support\Collection<TKey, TMapValue>|static<TKey, TMapValue>
  288. */
  289. public function map(callable $callback)
  290. {
  291. $result = parent::map($callback);
  292. return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
  293. }
  294. /**
  295. * Run an associative map over each of the items.
  296. *
  297. * The callback should return an associative array with a single key / value pair.
  298. *
  299. * @template TMapWithKeysKey of array-key
  300. * @template TMapWithKeysValue
  301. *
  302. * @param callable(TModel, TKey): array<TMapWithKeysKey, TMapWithKeysValue> $callback
  303. * @return \Illuminate\Support\Collection<TMapWithKeysKey, TMapWithKeysValue>|static<TMapWithKeysKey, TMapWithKeysValue>
  304. */
  305. public function mapWithKeys(callable $callback)
  306. {
  307. $result = parent::mapWithKeys($callback);
  308. return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
  309. }
  310. /**
  311. * Reload a fresh model instance from the database for all the entities.
  312. *
  313. * @param array<array-key, string>|string $with
  314. * @return static
  315. */
  316. public function fresh($with = [])
  317. {
  318. if ($this->isEmpty()) {
  319. return new static;
  320. }
  321. $model = $this->first();
  322. $freshModels = $model->newQueryWithoutScopes()
  323. ->with(is_string($with) ? func_get_args() : $with)
  324. ->whereIn($model->getKeyName(), $this->modelKeys())
  325. ->get()
  326. ->getDictionary();
  327. return $this->filter(fn ($model) => $model->exists && isset($freshModels[$model->getKey()]))
  328. ->map(fn ($model) => $freshModels[$model->getKey()]);
  329. }
  330. /**
  331. * Diff the collection with the given items.
  332. *
  333. * @param iterable<array-key, TModel> $items
  334. * @return static
  335. */
  336. public function diff($items)
  337. {
  338. $diff = new static;
  339. $dictionary = $this->getDictionary($items);
  340. foreach ($this->items as $item) {
  341. if (! isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
  342. $diff->add($item);
  343. }
  344. }
  345. return $diff;
  346. }
  347. /**
  348. * Intersect the collection with the given items.
  349. *
  350. * @param iterable<array-key, TModel> $items
  351. * @return static
  352. */
  353. public function intersect($items)
  354. {
  355. $intersect = new static;
  356. if (empty($items)) {
  357. return $intersect;
  358. }
  359. $dictionary = $this->getDictionary($items);
  360. foreach ($this->items as $item) {
  361. if (isset($dictionary[$this->getDictionaryKey($item->getKey())])) {
  362. $intersect->add($item);
  363. }
  364. }
  365. return $intersect;
  366. }
  367. /**
  368. * Return only unique items from the collection.
  369. *
  370. * @param (callable(TModel, TKey): mixed)|string|null $key
  371. * @param bool $strict
  372. * @return static<int, TModel>
  373. */
  374. public function unique($key = null, $strict = false)
  375. {
  376. if (! is_null($key)) {
  377. return parent::unique($key, $strict);
  378. }
  379. return new static(array_values($this->getDictionary()));
  380. }
  381. /**
  382. * Returns only the models from the collection with the specified keys.
  383. *
  384. * @param array<array-key, mixed>|null $keys
  385. * @return static<int, TModel>
  386. */
  387. public function only($keys)
  388. {
  389. if (is_null($keys)) {
  390. return new static($this->items);
  391. }
  392. $dictionary = Arr::only($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
  393. return new static(array_values($dictionary));
  394. }
  395. /**
  396. * Returns all models in the collection except the models with specified keys.
  397. *
  398. * @param array<array-key, mixed>|null $keys
  399. * @return static<int, TModel>
  400. */
  401. public function except($keys)
  402. {
  403. if (is_null($keys)) {
  404. return new static($this->items);
  405. }
  406. $dictionary = Arr::except($this->getDictionary(), array_map($this->getDictionaryKey(...), (array) $keys));
  407. return new static(array_values($dictionary));
  408. }
  409. /**
  410. * Make the given, typically visible, attributes hidden across the entire collection.
  411. *
  412. * @param array<array-key, string>|string $attributes
  413. * @return $this
  414. */
  415. public function makeHidden($attributes)
  416. {
  417. return $this->each->makeHidden($attributes);
  418. }
  419. /**
  420. * Make the given, typically hidden, attributes visible across the entire collection.
  421. *
  422. * @param array<array-key, string>|string $attributes
  423. * @return $this
  424. */
  425. public function makeVisible($attributes)
  426. {
  427. return $this->each->makeVisible($attributes);
  428. }
  429. /**
  430. * Set the visible attributes across the entire collection.
  431. *
  432. * @param array<int, string> $visible
  433. * @return $this
  434. */
  435. public function setVisible($visible)
  436. {
  437. return $this->each->setVisible($visible);
  438. }
  439. /**
  440. * Set the hidden attributes across the entire collection.
  441. *
  442. * @param array<int, string> $hidden
  443. * @return $this
  444. */
  445. public function setHidden($hidden)
  446. {
  447. return $this->each->setHidden($hidden);
  448. }
  449. /**
  450. * Append an attribute across the entire collection.
  451. *
  452. * @param array<array-key, string>|string $attributes
  453. * @return $this
  454. */
  455. public function append($attributes)
  456. {
  457. return $this->each->append($attributes);
  458. }
  459. /**
  460. * Get a dictionary keyed by primary keys.
  461. *
  462. * @param iterable<array-key, TModel>|null $items
  463. * @return array<array-key, TModel>
  464. */
  465. public function getDictionary($items = null)
  466. {
  467. $items = is_null($items) ? $this->items : $items;
  468. $dictionary = [];
  469. foreach ($items as $value) {
  470. $dictionary[$this->getDictionaryKey($value->getKey())] = $value;
  471. }
  472. return $dictionary;
  473. }
  474. /**
  475. * The following methods are intercepted to always return base collections.
  476. */
  477. /**
  478. * Count the number of items in the collection by a field or using a callback.
  479. *
  480. * @param (callable(TModel, TKey): array-key)|string|null $countBy
  481. * @return \Illuminate\Support\Collection<array-key, int>
  482. */
  483. public function countBy($countBy = null)
  484. {
  485. return $this->toBase()->countBy($countBy);
  486. }
  487. /**
  488. * Collapse the collection of items into a single array.
  489. *
  490. * @return \Illuminate\Support\Collection<int, mixed>
  491. */
  492. public function collapse()
  493. {
  494. return $this->toBase()->collapse();
  495. }
  496. /**
  497. * Get a flattened array of the items in the collection.
  498. *
  499. * @param int $depth
  500. * @return \Illuminate\Support\Collection<int, mixed>
  501. */
  502. public function flatten($depth = INF)
  503. {
  504. return $this->toBase()->flatten($depth);
  505. }
  506. /**
  507. * Flip the items in the collection.
  508. *
  509. * @return \Illuminate\Support\Collection<TModel, TKey>
  510. */
  511. public function flip()
  512. {
  513. return $this->toBase()->flip();
  514. }
  515. /**
  516. * Get the keys of the collection items.
  517. *
  518. * @return \Illuminate\Support\Collection<int, TKey>
  519. */
  520. public function keys()
  521. {
  522. return $this->toBase()->keys();
  523. }
  524. /**
  525. * Pad collection to the specified length with a value.
  526. *
  527. * @template TPadValue
  528. *
  529. * @param int $size
  530. * @param TPadValue $value
  531. * @return \Illuminate\Support\Collection<int, TModel|TPadValue>
  532. */
  533. public function pad($size, $value)
  534. {
  535. return $this->toBase()->pad($size, $value);
  536. }
  537. /**
  538. * Get an array with the values of a given key.
  539. *
  540. * @param string|array<array-key, string>|null $value
  541. * @param string|null $key
  542. * @return \Illuminate\Support\Collection<array-key, mixed>
  543. */
  544. public function pluck($value, $key = null)
  545. {
  546. return $this->toBase()->pluck($value, $key);
  547. }
  548. /**
  549. * Zip the collection together with one or more arrays.
  550. *
  551. * @template TZipValue
  552. *
  553. * @param \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue> ...$items
  554. * @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, TModel|TZipValue>>
  555. */
  556. public function zip($items)
  557. {
  558. return $this->toBase()->zip(...func_get_args());
  559. }
  560. /**
  561. * Get the comparison function to detect duplicates.
  562. *
  563. * @param bool $strict
  564. * @return callable(TModel, TModel): bool
  565. */
  566. protected function duplicateComparator($strict)
  567. {
  568. return fn ($a, $b) => $a->is($b);
  569. }
  570. /**
  571. * Get the type of the entities being queued.
  572. *
  573. * @return string|null
  574. *
  575. * @throws \LogicException
  576. */
  577. public function getQueueableClass()
  578. {
  579. if ($this->isEmpty()) {
  580. return;
  581. }
  582. $class = $this->getQueueableModelClass($this->first());
  583. $this->each(function ($model) use ($class) {
  584. if ($this->getQueueableModelClass($model) !== $class) {
  585. throw new LogicException('Queueing collections with multiple model types is not supported.');
  586. }
  587. });
  588. return $class;
  589. }
  590. /**
  591. * Get the queueable class name for the given model.
  592. *
  593. * @param \Illuminate\Database\Eloquent\Model $model
  594. * @return string
  595. */
  596. protected function getQueueableModelClass($model)
  597. {
  598. return method_exists($model, 'getQueueableClassName')
  599. ? $model->getQueueableClassName()
  600. : get_class($model);
  601. }
  602. /**
  603. * Get the identifiers for all of the entities.
  604. *
  605. * @return array<int, mixed>
  606. */
  607. public function getQueueableIds()
  608. {
  609. if ($this->isEmpty()) {
  610. return [];
  611. }
  612. return $this->first() instanceof QueueableEntity
  613. ? $this->map->getQueueableId()->all()
  614. : $this->modelKeys();
  615. }
  616. /**
  617. * Get the relationships of the entities being queued.
  618. *
  619. * @return array<int, string>
  620. */
  621. public function getQueueableRelations()
  622. {
  623. if ($this->isEmpty()) {
  624. return [];
  625. }
  626. $relations = $this->map->getQueueableRelations()->all();
  627. if (count($relations) === 0 || $relations === [[]]) {
  628. return [];
  629. } elseif (count($relations) === 1) {
  630. return reset($relations);
  631. } else {
  632. return array_intersect(...array_values($relations));
  633. }
  634. }
  635. /**
  636. * Get the connection of the entities being queued.
  637. *
  638. * @return string|null
  639. *
  640. * @throws \LogicException
  641. */
  642. public function getQueueableConnection()
  643. {
  644. if ($this->isEmpty()) {
  645. return;
  646. }
  647. $connection = $this->first()->getConnectionName();
  648. $this->each(function ($model) use ($connection) {
  649. if ($model->getConnectionName() !== $connection) {
  650. throw new LogicException('Queueing collections with multiple model connections is not supported.');
  651. }
  652. });
  653. return $connection;
  654. }
  655. /**
  656. * Get the Eloquent query builder from the collection.
  657. *
  658. * @return \Illuminate\Database\Eloquent\Builder
  659. *
  660. * @throws \LogicException
  661. */
  662. public function toQuery()
  663. {
  664. $model = $this->first();
  665. if (! $model) {
  666. throw new LogicException('Unable to create query for empty collection.');
  667. }
  668. $class = get_class($model);
  669. if ($this->filter(fn ($model) => ! $model instanceof $class)->isNotEmpty()) {
  670. throw new LogicException('Unable to create query for collection with mixed types.');
  671. }
  672. return $model->newModelQuery()->whereKey($this->modelKeys());
  673. }
  674. }