Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Database\Eloquent\Relations;
4
 
5
use Closure;
6
use Illuminate\Contracts\Support\Arrayable;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Collection;
9
use Illuminate\Database\Eloquent\Model;
10
use Illuminate\Database\Eloquent\ModelNotFoundException;
11
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
12
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary;
13
use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable;
14
use Illuminate\Support\Str;
15
use InvalidArgumentException;
16
 
17
class BelongsToMany extends Relation
18
{
19
    use InteractsWithDictionary, InteractsWithPivotTable;
20
 
21
    /**
22
     * The intermediate table for the relation.
23
     *
24
     * @var string
25
     */
26
    protected $table;
27
 
28
    /**
29
     * The foreign key of the parent model.
30
     *
31
     * @var string
32
     */
33
    protected $foreignPivotKey;
34
 
35
    /**
36
     * The associated key of the relation.
37
     *
38
     * @var string
39
     */
40
    protected $relatedPivotKey;
41
 
42
    /**
43
     * The key name of the parent model.
44
     *
45
     * @var string
46
     */
47
    protected $parentKey;
48
 
49
    /**
50
     * The key name of the related model.
51
     *
52
     * @var string
53
     */
54
    protected $relatedKey;
55
 
56
    /**
57
     * The "name" of the relationship.
58
     *
59
     * @var string
60
     */
61
    protected $relationName;
62
 
63
    /**
64
     * The pivot table columns to retrieve.
65
     *
66
     * @var array
67
     */
68
    protected $pivotColumns = [];
69
 
70
    /**
71
     * Any pivot table restrictions for where clauses.
72
     *
73
     * @var array
74
     */
75
    protected $pivotWheres = [];
76
 
77
    /**
78
     * Any pivot table restrictions for whereIn clauses.
79
     *
80
     * @var array
81
     */
82
    protected $pivotWhereIns = [];
83
 
84
    /**
85
     * Any pivot table restrictions for whereNull clauses.
86
     *
87
     * @var array
88
     */
89
    protected $pivotWhereNulls = [];
90
 
91
    /**
92
     * The default values for the pivot columns.
93
     *
94
     * @var array
95
     */
96
    protected $pivotValues = [];
97
 
98
    /**
99
     * Indicates if timestamps are available on the pivot table.
100
     *
101
     * @var bool
102
     */
103
    public $withTimestamps = false;
104
 
105
    /**
106
     * The custom pivot table column for the created_at timestamp.
107
     *
108
     * @var string
109
     */
110
    protected $pivotCreatedAt;
111
 
112
    /**
113
     * The custom pivot table column for the updated_at timestamp.
114
     *
115
     * @var string
116
     */
117
    protected $pivotUpdatedAt;
118
 
119
    /**
120
     * The class name of the custom pivot model to use for the relationship.
121
     *
122
     * @var string
123
     */
124
    protected $using;
125
 
126
    /**
127
     * The name of the accessor to use for the "pivot" relationship.
128
     *
129
     * @var string
130
     */
131
    protected $accessor = 'pivot';
132
 
133
    /**
134
     * Create a new belongs to many relationship instance.
135
     *
136
     * @param  \Illuminate\Database\Eloquent\Builder  $query
137
     * @param  \Illuminate\Database\Eloquent\Model  $parent
138
     * @param  string  $table
139
     * @param  string  $foreignPivotKey
140
     * @param  string  $relatedPivotKey
141
     * @param  string  $parentKey
142
     * @param  string  $relatedKey
143
     * @param  string|null  $relationName
144
     * @return void
145
     */
146
    public function __construct(Builder $query, Model $parent, $table, $foreignPivotKey,
147
                                $relatedPivotKey, $parentKey, $relatedKey, $relationName = null)
148
    {
149
        $this->parentKey = $parentKey;
150
        $this->relatedKey = $relatedKey;
151
        $this->relationName = $relationName;
152
        $this->relatedPivotKey = $relatedPivotKey;
153
        $this->foreignPivotKey = $foreignPivotKey;
154
        $this->table = $this->resolveTableName($table);
155
 
156
        parent::__construct($query, $parent);
157
    }
158
 
159
    /**
160
     * Attempt to resolve the intermediate table name from the given string.
161
     *
162
     * @param  string  $table
163
     * @return string
164
     */
165
    protected function resolveTableName($table)
166
    {
167
        if (! str_contains($table, '\\') || ! class_exists($table)) {
168
            return $table;
169
        }
170
 
171
        $model = new $table;
172
 
173
        if (! $model instanceof Model) {
174
            return $table;
175
        }
176
 
177
        if (in_array(AsPivot::class, class_uses_recursive($model))) {
178
            $this->using($table);
179
        }
180
 
181
        return $model->getTable();
182
    }
183
 
184
    /**
185
     * Set the base constraints on the relation query.
186
     *
187
     * @return void
188
     */
189
    public function addConstraints()
190
    {
191
        $this->performJoin();
192
 
193
        if (static::$constraints) {
194
            $this->addWhereConstraints();
195
        }
196
    }
197
 
198
    /**
199
     * Set the join clause for the relation query.
200
     *
201
     * @param  \Illuminate\Database\Eloquent\Builder|null  $query
202
     * @return $this
203
     */
204
    protected function performJoin($query = null)
205
    {
206
        $query = $query ?: $this->query;
207
 
208
        // We need to join to the intermediate table on the related model's primary
209
        // key column with the intermediate table's foreign key for the related
210
        // model instance. Then we can set the "where" for the parent models.
211
        $query->join(
212
            $this->table,
213
            $this->getQualifiedRelatedKeyName(),
214
            '=',
215
            $this->getQualifiedRelatedPivotKeyName()
216
        );
217
 
218
        return $this;
219
    }
220
 
221
    /**
222
     * Set the where clause for the relation query.
223
     *
224
     * @return $this
225
     */
226
    protected function addWhereConstraints()
227
    {
228
        $this->query->where(
229
            $this->getQualifiedForeignPivotKeyName(), '=', $this->parent->{$this->parentKey}
230
        );
231
 
232
        return $this;
233
    }
234
 
235
    /**
236
     * Set the constraints for an eager load of the relation.
237
     *
238
     * @param  array  $models
239
     * @return void
240
     */
241
    public function addEagerConstraints(array $models)
242
    {
243
        $whereIn = $this->whereInMethod($this->parent, $this->parentKey);
244
 
245
        $this->query->{$whereIn}(
246
            $this->getQualifiedForeignPivotKeyName(),
247
            $this->getKeys($models, $this->parentKey)
248
        );
249
    }
250
 
251
    /**
252
     * Initialize the relation on a set of models.
253
     *
254
     * @param  array  $models
255
     * @param  string  $relation
256
     * @return array
257
     */
258
    public function initRelation(array $models, $relation)
259
    {
260
        foreach ($models as $model) {
261
            $model->setRelation($relation, $this->related->newCollection());
262
        }
263
 
264
        return $models;
265
    }
266
 
267
    /**
268
     * Match the eagerly loaded results to their parents.
269
     *
270
     * @param  array  $models
271
     * @param  \Illuminate\Database\Eloquent\Collection  $results
272
     * @param  string  $relation
273
     * @return array
274
     */
275
    public function match(array $models, Collection $results, $relation)
276
    {
277
        $dictionary = $this->buildDictionary($results);
278
 
279
        // Once we have an array dictionary of child objects we can easily match the
280
        // children back to their parent using the dictionary and the keys on the
281
        // parent models. Then we should return these hydrated models back out.
282
        foreach ($models as $model) {
283
            $key = $this->getDictionaryKey($model->{$this->parentKey});
284
 
285
            if (isset($dictionary[$key])) {
286
                $model->setRelation(
287
                    $relation, $this->related->newCollection($dictionary[$key])
288
                );
289
            }
290
        }
291
 
292
        return $models;
293
    }
294
 
295
    /**
296
     * Build model dictionary keyed by the relation's foreign key.
297
     *
298
     * @param  \Illuminate\Database\Eloquent\Collection  $results
299
     * @return array
300
     */
301
    protected function buildDictionary(Collection $results)
302
    {
303
        // First we'll build a dictionary of child models keyed by the foreign key
304
        // of the relation so that we will easily and quickly match them to the
305
        // parents without having a possibly slow inner loop for every model.
306
        $dictionary = [];
307
 
308
        foreach ($results as $result) {
309
            $value = $this->getDictionaryKey($result->{$this->accessor}->{$this->foreignPivotKey});
310
 
311
            $dictionary[$value][] = $result;
312
        }
313
 
314
        return $dictionary;
315
    }
316
 
317
    /**
318
     * Get the class being used for pivot models.
319
     *
320
     * @return string
321
     */
322
    public function getPivotClass()
323
    {
324
        return $this->using ?? Pivot::class;
325
    }
326
 
327
    /**
328
     * Specify the custom pivot model to use for the relationship.
329
     *
330
     * @param  string  $class
331
     * @return $this
332
     */
333
    public function using($class)
334
    {
335
        $this->using = $class;
336
 
337
        return $this;
338
    }
339
 
340
    /**
341
     * Specify the custom pivot accessor to use for the relationship.
342
     *
343
     * @param  string  $accessor
344
     * @return $this
345
     */
346
    public function as($accessor)
347
    {
348
        $this->accessor = $accessor;
349
 
350
        return $this;
351
    }
352
 
353
    /**
354
     * Set a where clause for a pivot table column.
355
     *
356
     * @param  string  $column
357
     * @param  mixed  $operator
358
     * @param  mixed  $value
359
     * @param  string  $boolean
360
     * @return $this
361
     */
362
    public function wherePivot($column, $operator = null, $value = null, $boolean = 'and')
363
    {
364
        $this->pivotWheres[] = func_get_args();
365
 
366
        return $this->where($this->qualifyPivotColumn($column), $operator, $value, $boolean);
367
    }
368
 
369
    /**
370
     * Set a "where between" clause for a pivot table column.
371
     *
372
     * @param  string  $column
373
     * @param  array  $values
374
     * @param  string  $boolean
375
     * @param  bool  $not
376
     * @return $this
377
     */
378
    public function wherePivotBetween($column, array $values, $boolean = 'and', $not = false)
379
    {
380
        return $this->whereBetween($this->qualifyPivotColumn($column), $values, $boolean, $not);
381
    }
382
 
383
    /**
384
     * Set a "or where between" clause for a pivot table column.
385
     *
386
     * @param  string  $column
387
     * @param  array  $values
388
     * @return $this
389
     */
390
    public function orWherePivotBetween($column, array $values)
391
    {
392
        return $this->wherePivotBetween($column, $values, 'or');
393
    }
394
 
395
    /**
396
     * Set a "where pivot not between" clause for a pivot table column.
397
     *
398
     * @param  string  $column
399
     * @param  array  $values
400
     * @param  string  $boolean
401
     * @return $this
402
     */
403
    public function wherePivotNotBetween($column, array $values, $boolean = 'and')
404
    {
405
        return $this->wherePivotBetween($column, $values, $boolean, true);
406
    }
407
 
408
    /**
409
     * Set a "or where not between" clause for a pivot table column.
410
     *
411
     * @param  string  $column
412
     * @param  array  $values
413
     * @return $this
414
     */
415
    public function orWherePivotNotBetween($column, array $values)
416
    {
417
        return $this->wherePivotBetween($column, $values, 'or', true);
418
    }
419
 
420
    /**
421
     * Set a "where in" clause for a pivot table column.
422
     *
423
     * @param  string  $column
424
     * @param  mixed  $values
425
     * @param  string  $boolean
426
     * @param  bool  $not
427
     * @return $this
428
     */
429
    public function wherePivotIn($column, $values, $boolean = 'and', $not = false)
430
    {
431
        $this->pivotWhereIns[] = func_get_args();
432
 
433
        return $this->whereIn($this->qualifyPivotColumn($column), $values, $boolean, $not);
434
    }
435
 
436
    /**
437
     * Set an "or where" clause for a pivot table column.
438
     *
439
     * @param  string  $column
440
     * @param  mixed  $operator
441
     * @param  mixed  $value
442
     * @return $this
443
     */
444
    public function orWherePivot($column, $operator = null, $value = null)
445
    {
446
        return $this->wherePivot($column, $operator, $value, 'or');
447
    }
448
 
449
    /**
450
     * Set a where clause for a pivot table column.
451
     *
452
     * In addition, new pivot records will receive this value.
453
     *
454
     * @param  string|array  $column
455
     * @param  mixed  $value
456
     * @return $this
457
     *
458
     * @throws \InvalidArgumentException
459
     */
460
    public function withPivotValue($column, $value = null)
461
    {
462
        if (is_array($column)) {
463
            foreach ($column as $name => $value) {
464
                $this->withPivotValue($name, $value);
465
            }
466
 
467
            return $this;
468
        }
469
 
470
        if (is_null($value)) {
471
            throw new InvalidArgumentException('The provided value may not be null.');
472
        }
473
 
474
        $this->pivotValues[] = compact('column', 'value');
475
 
476
        return $this->wherePivot($column, '=', $value);
477
    }
478
 
479
    /**
480
     * Set an "or where in" clause for a pivot table column.
481
     *
482
     * @param  string  $column
483
     * @param  mixed  $values
484
     * @return $this
485
     */
486
    public function orWherePivotIn($column, $values)
487
    {
488
        return $this->wherePivotIn($column, $values, 'or');
489
    }
490
 
491
    /**
492
     * Set a "where not in" clause for a pivot table column.
493
     *
494
     * @param  string  $column
495
     * @param  mixed  $values
496
     * @param  string  $boolean
497
     * @return $this
498
     */
499
    public function wherePivotNotIn($column, $values, $boolean = 'and')
500
    {
501
        return $this->wherePivotIn($column, $values, $boolean, true);
502
    }
503
 
504
    /**
505
     * Set an "or where not in" clause for a pivot table column.
506
     *
507
     * @param  string  $column
508
     * @param  mixed  $values
509
     * @return $this
510
     */
511
    public function orWherePivotNotIn($column, $values)
512
    {
513
        return $this->wherePivotNotIn($column, $values, 'or');
514
    }
515
 
516
    /**
517
     * Set a "where null" clause for a pivot table column.
518
     *
519
     * @param  string  $column
520
     * @param  string  $boolean
521
     * @param  bool  $not
522
     * @return $this
523
     */
524
    public function wherePivotNull($column, $boolean = 'and', $not = false)
525
    {
526
        $this->pivotWhereNulls[] = func_get_args();
527
 
528
        return $this->whereNull($this->qualifyPivotColumn($column), $boolean, $not);
529
    }
530
 
531
    /**
532
     * Set a "where not null" clause for a pivot table column.
533
     *
534
     * @param  string  $column
535
     * @param  string  $boolean
536
     * @return $this
537
     */
538
    public function wherePivotNotNull($column, $boolean = 'and')
539
    {
540
        return $this->wherePivotNull($column, $boolean, true);
541
    }
542
 
543
    /**
544
     * Set a "or where null" clause for a pivot table column.
545
     *
546
     * @param  string  $column
547
     * @param  bool  $not
548
     * @return $this
549
     */
550
    public function orWherePivotNull($column, $not = false)
551
    {
552
        return $this->wherePivotNull($column, 'or', $not);
553
    }
554
 
555
    /**
556
     * Set a "or where not null" clause for a pivot table column.
557
     *
558
     * @param  string  $column
559
     * @return $this
560
     */
561
    public function orWherePivotNotNull($column)
562
    {
563
        return $this->orWherePivotNull($column, true);
564
    }
565
 
566
    /**
567
     * Add an "order by" clause for a pivot table column.
568
     *
569
     * @param  string  $column
570
     * @param  string  $direction
571
     * @return $this
572
     */
573
    public function orderByPivot($column, $direction = 'asc')
574
    {
575
        return $this->orderBy($this->qualifyPivotColumn($column), $direction);
576
    }
577
 
578
    /**
579
     * Find a related model by its primary key or return a new instance of the related model.
580
     *
581
     * @param  mixed  $id
582
     * @param  array  $columns
583
     * @return \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model
584
     */
585
    public function findOrNew($id, $columns = ['*'])
586
    {
587
        if (is_null($instance = $this->find($id, $columns))) {
588
            $instance = $this->related->newInstance();
589
        }
590
 
591
        return $instance;
592
    }
593
 
594
    /**
595
     * Get the first related model record matching the attributes or instantiate it.
596
     *
597
     * @param  array  $attributes
598
     * @param  array  $values
599
     * @return \Illuminate\Database\Eloquent\Model
600
     */
601
    public function firstOrNew(array $attributes = [], array $values = [])
602
    {
603
        if (is_null($instance = $this->related->where($attributes)->first())) {
604
            $instance = $this->related->newInstance(array_merge($attributes, $values));
605
        }
606
 
607
        return $instance;
608
    }
609
 
610
    /**
611
     * Get the first related record matching the attributes or create it.
612
     *
613
     * @param  array  $attributes
614
     * @param  array  $values
615
     * @param  array  $joining
616
     * @param  bool  $touch
617
     * @return \Illuminate\Database\Eloquent\Model
618
     */
619
    public function firstOrCreate(array $attributes = [], array $values = [], array $joining = [], $touch = true)
620
    {
621
        if (is_null($instance = (clone $this)->where($attributes)->first())) {
622
            if (is_null($instance = $this->related->where($attributes)->first())) {
623
                $instance = $this->create(array_merge($attributes, $values), $joining, $touch);
624
            } else {
625
                $this->attach($instance, $joining, $touch);
626
            }
627
        }
628
 
629
        return $instance;
630
    }
631
 
632
    /**
633
     * Create or update a related record matching the attributes, and fill it with values.
634
     *
635
     * @param  array  $attributes
636
     * @param  array  $values
637
     * @param  array  $joining
638
     * @param  bool  $touch
639
     * @return \Illuminate\Database\Eloquent\Model
640
     */
641
    public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true)
642
    {
643
        if (is_null($instance = (clone $this)->where($attributes)->first())) {
644
            if (is_null($instance = $this->related->where($attributes)->first())) {
645
                return $this->create(array_merge($attributes, $values), $joining, $touch);
646
            } else {
647
                $this->attach($instance, $joining, $touch);
648
            }
649
        }
650
 
651
        $instance->fill($values);
652
 
653
        $instance->save(['touch' => false]);
654
 
655
        return $instance;
656
    }
657
 
658
    /**
659
     * Find a related model by its primary key.
660
     *
661
     * @param  mixed  $id
662
     * @param  array  $columns
663
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
664
     */
665
    public function find($id, $columns = ['*'])
666
    {
667
        if (! $id instanceof Model && (is_array($id) || $id instanceof Arrayable)) {
668
            return $this->findMany($id, $columns);
669
        }
670
 
671
        return $this->where(
672
            $this->getRelated()->getQualifiedKeyName(), '=', $this->parseId($id)
673
        )->first($columns);
674
    }
675
 
676
    /**
677
     * Find multiple related models by their primary keys.
678
     *
679
     * @param  \Illuminate\Contracts\Support\Arrayable|array  $ids
680
     * @param  array  $columns
681
     * @return \Illuminate\Database\Eloquent\Collection
682
     */
683
    public function findMany($ids, $columns = ['*'])
684
    {
685
        $ids = $ids instanceof Arrayable ? $ids->toArray() : $ids;
686
 
687
        if (empty($ids)) {
688
            return $this->getRelated()->newCollection();
689
        }
690
 
399 lars 691
        return $this->whereKey(
692
            $this->parseIds($ids)
148 lars 693
        )->get($columns);
694
    }
695
 
696
    /**
697
     * Find a related model by its primary key or throw an exception.
698
     *
699
     * @param  mixed  $id
700
     * @param  array  $columns
701
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
702
     *
703
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
704
     */
705
    public function findOrFail($id, $columns = ['*'])
706
    {
707
        $result = $this->find($id, $columns);
708
 
709
        $id = $id instanceof Arrayable ? $id->toArray() : $id;
710
 
711
        if (is_array($id)) {
712
            if (count($result) === count(array_unique($id))) {
713
                return $result;
714
            }
715
        } elseif (! is_null($result)) {
716
            return $result;
717
        }
718
 
719
        throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
720
    }
721
 
722
    /**
723
     * Find a related model by its primary key or call a callback.
724
     *
725
     * @param  mixed  $id
726
     * @param  \Closure|array  $columns
727
     * @param  \Closure|null  $callback
728
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|mixed
729
     */
730
    public function findOr($id, $columns = ['*'], Closure $callback = null)
731
    {
732
        if ($columns instanceof Closure) {
733
            $callback = $columns;
734
 
735
            $columns = ['*'];
736
        }
737
 
738
        $result = $this->find($id, $columns);
739
 
740
        $id = $id instanceof Arrayable ? $id->toArray() : $id;
741
 
742
        if (is_array($id)) {
743
            if (count($result) === count(array_unique($id))) {
744
                return $result;
745
            }
746
        } elseif (! is_null($result)) {
747
            return $result;
748
        }
749
 
750
        return $callback();
751
    }
752
 
753
    /**
754
     * Add a basic where clause to the query, and return the first result.
755
     *
756
     * @param  \Closure|string|array  $column
757
     * @param  mixed  $operator
758
     * @param  mixed  $value
759
     * @param  string  $boolean
760
     * @return \Illuminate\Database\Eloquent\Model|static
761
     */
762
    public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
763
    {
764
        return $this->where($column, $operator, $value, $boolean)->first();
765
    }
766
 
767
    /**
768
     * Execute the query and get the first result.
769
     *
770
     * @param  array  $columns
771
     * @return mixed
772
     */
773
    public function first($columns = ['*'])
774
    {
775
        $results = $this->take(1)->get($columns);
776
 
777
        return count($results) > 0 ? $results->first() : null;
778
    }
779
 
780
    /**
781
     * Execute the query and get the first result or throw an exception.
782
     *
783
     * @param  array  $columns
784
     * @return \Illuminate\Database\Eloquent\Model|static
785
     *
786
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
787
     */
788
    public function firstOrFail($columns = ['*'])
789
    {
790
        if (! is_null($model = $this->first($columns))) {
791
            return $model;
792
        }
793
 
794
        throw (new ModelNotFoundException)->setModel(get_class($this->related));
795
    }
796
 
797
    /**
798
     * Execute the query and get the first result or call a callback.
799
     *
800
     * @param  \Closure|array  $columns
801
     * @param  \Closure|null  $callback
802
     * @return \Illuminate\Database\Eloquent\Model|static|mixed
803
     */
804
    public function firstOr($columns = ['*'], Closure $callback = null)
805
    {
806
        if ($columns instanceof Closure) {
807
            $callback = $columns;
808
 
809
            $columns = ['*'];
810
        }
811
 
812
        if (! is_null($model = $this->first($columns))) {
813
            return $model;
814
        }
815
 
816
        return $callback();
817
    }
818
 
819
    /**
820
     * Get the results of the relationship.
821
     *
822
     * @return mixed
823
     */
824
    public function getResults()
825
    {
826
        return ! is_null($this->parent->{$this->parentKey})
827
                ? $this->get()
828
                : $this->related->newCollection();
829
    }
830
 
831
    /**
832
     * Execute the query as a "select" statement.
833
     *
834
     * @param  array  $columns
835
     * @return \Illuminate\Database\Eloquent\Collection
836
     */
837
    public function get($columns = ['*'])
838
    {
839
        // First we'll add the proper select columns onto the query so it is run with
840
        // the proper columns. Then, we will get the results and hydrate our pivot
841
        // models with the result of those columns as a separate model relation.
842
        $builder = $this->query->applyScopes();
843
 
844
        $columns = $builder->getQuery()->columns ? [] : $columns;
845
 
846
        $models = $builder->addSelect(
847
            $this->shouldSelect($columns)
848
        )->getModels();
849
 
850
        $this->hydratePivotRelation($models);
851
 
852
        // If we actually found models we will also eager load any relationships that
853
        // have been specified as needing to be eager loaded. This will solve the
854
        // n + 1 query problem for the developer and also increase performance.
855
        if (count($models) > 0) {
856
            $models = $builder->eagerLoadRelations($models);
857
        }
858
 
859
        return $this->related->newCollection($models);
860
    }
861
 
862
    /**
863
     * Get the select columns for the relation query.
864
     *
865
     * @param  array  $columns
866
     * @return array
867
     */
868
    protected function shouldSelect(array $columns = ['*'])
869
    {
870
        if ($columns == ['*']) {
871
            $columns = [$this->related->getTable().'.*'];
872
        }
873
 
874
        return array_merge($columns, $this->aliasedPivotColumns());
875
    }
876
 
877
    /**
878
     * Get the pivot columns for the relation.
879
     *
880
     * "pivot_" is prefixed at each column for easy removal later.
881
     *
882
     * @return array
883
     */
884
    protected function aliasedPivotColumns()
885
    {
886
        $defaults = [$this->foreignPivotKey, $this->relatedPivotKey];
887
 
888
        return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) {
889
            return $this->qualifyPivotColumn($column).' as pivot_'.$column;
890
        })->unique()->all();
891
    }
892
 
893
    /**
894
     * Get a paginator for the "select" statement.
895
     *
896
     * @param  int|null  $perPage
897
     * @param  array  $columns
898
     * @param  string  $pageName
899
     * @param  int|null  $page
900
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
901
     */
902
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
903
    {
904
        $this->query->addSelect($this->shouldSelect($columns));
905
 
906
        return tap($this->query->paginate($perPage, $columns, $pageName, $page), function ($paginator) {
907
            $this->hydratePivotRelation($paginator->items());
908
        });
909
    }
910
 
911
    /**
912
     * Paginate the given query into a simple paginator.
913
     *
914
     * @param  int|null  $perPage
915
     * @param  array  $columns
916
     * @param  string  $pageName
917
     * @param  int|null  $page
918
     * @return \Illuminate\Contracts\Pagination\Paginator
919
     */
920
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
921
    {
922
        $this->query->addSelect($this->shouldSelect($columns));
923
 
924
        return tap($this->query->simplePaginate($perPage, $columns, $pageName, $page), function ($paginator) {
925
            $this->hydratePivotRelation($paginator->items());
926
        });
927
    }
928
 
929
    /**
930
     * Paginate the given query into a cursor paginator.
931
     *
932
     * @param  int|null  $perPage
933
     * @param  array  $columns
934
     * @param  string  $cursorName
935
     * @param  string|null  $cursor
936
     * @return \Illuminate\Contracts\Pagination\CursorPaginator
937
     */
938
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
939
    {
940
        $this->query->addSelect($this->shouldSelect($columns));
941
 
942
        return tap($this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor), function ($paginator) {
943
            $this->hydratePivotRelation($paginator->items());
944
        });
945
    }
946
 
947
    /**
948
     * Chunk the results of the query.
949
     *
950
     * @param  int  $count
951
     * @param  callable  $callback
952
     * @return bool
953
     */
954
    public function chunk($count, callable $callback)
955
    {
956
        return $this->prepareQueryBuilder()->chunk($count, function ($results, $page) use ($callback) {
957
            $this->hydratePivotRelation($results->all());
958
 
959
            return $callback($results, $page);
960
        });
961
    }
962
 
963
    /**
964
     * Chunk the results of a query by comparing numeric IDs.
965
     *
966
     * @param  int  $count
967
     * @param  callable  $callback
968
     * @param  string|null  $column
969
     * @param  string|null  $alias
970
     * @return bool
971
     */
972
    public function chunkById($count, callable $callback, $column = null, $alias = null)
973
    {
974
        $this->prepareQueryBuilder();
975
 
976
        $column ??= $this->getRelated()->qualifyColumn(
977
            $this->getRelatedKeyName()
978
        );
979
 
980
        $alias ??= $this->getRelatedKeyName();
981
 
982
        return $this->query->chunkById($count, function ($results) use ($callback) {
983
            $this->hydratePivotRelation($results->all());
984
 
985
            return $callback($results);
986
        }, $column, $alias);
987
    }
988
 
989
    /**
990
     * Execute a callback over each item while chunking.
991
     *
992
     * @param  callable  $callback
993
     * @param  int  $count
994
     * @return bool
995
     */
996
    public function each(callable $callback, $count = 1000)
997
    {
998
        return $this->chunk($count, function ($results) use ($callback) {
999
            foreach ($results as $key => $value) {
1000
                if ($callback($value, $key) === false) {
1001
                    return false;
1002
                }
1003
            }
1004
        });
1005
    }
1006
 
1007
    /**
1008
     * Query lazily, by chunks of the given size.
1009
     *
1010
     * @param  int  $chunkSize
1011
     * @return \Illuminate\Support\LazyCollection
1012
     */
1013
    public function lazy($chunkSize = 1000)
1014
    {
1015
        return $this->prepareQueryBuilder()->lazy($chunkSize)->map(function ($model) {
1016
            $this->hydratePivotRelation([$model]);
1017
 
1018
            return $model;
1019
        });
1020
    }
1021
 
1022
    /**
1023
     * Query lazily, by chunking the results of a query by comparing IDs.
1024
     *
1025
     * @param  int  $chunkSize
1026
     * @param  string|null  $column
1027
     * @param  string|null  $alias
1028
     * @return \Illuminate\Support\LazyCollection
1029
     */
1030
    public function lazyById($chunkSize = 1000, $column = null, $alias = null)
1031
    {
1032
        $column ??= $this->getRelated()->qualifyColumn(
1033
            $this->getRelatedKeyName()
1034
        );
1035
 
1036
        $alias ??= $this->getRelatedKeyName();
1037
 
1038
        return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias)->map(function ($model) {
1039
            $this->hydratePivotRelation([$model]);
1040
 
1041
            return $model;
1042
        });
1043
    }
1044
 
1045
    /**
1046
     * Get a lazy collection for the given query.
1047
     *
1048
     * @return \Illuminate\Support\LazyCollection
1049
     */
1050
    public function cursor()
1051
    {
1052
        return $this->prepareQueryBuilder()->cursor()->map(function ($model) {
1053
            $this->hydratePivotRelation([$model]);
1054
 
1055
            return $model;
1056
        });
1057
    }
1058
 
1059
    /**
1060
     * Prepare the query builder for query execution.
1061
     *
1062
     * @return \Illuminate\Database\Eloquent\Builder
1063
     */
1064
    protected function prepareQueryBuilder()
1065
    {
1066
        return $this->query->addSelect($this->shouldSelect());
1067
    }
1068
 
1069
    /**
1070
     * Hydrate the pivot table relationship on the models.
1071
     *
1072
     * @param  array  $models
1073
     * @return void
1074
     */
1075
    protected function hydratePivotRelation(array $models)
1076
    {
1077
        // To hydrate the pivot relationship, we will just gather the pivot attributes
1078
        // and create a new Pivot model, which is basically a dynamic model that we
1079
        // will set the attributes, table, and connections on it so it will work.
1080
        foreach ($models as $model) {
1081
            $model->setRelation($this->accessor, $this->newExistingPivot(
1082
                $this->migratePivotAttributes($model)
1083
            ));
1084
        }
1085
    }
1086
 
1087
    /**
1088
     * Get the pivot attributes from a model.
1089
     *
1090
     * @param  \Illuminate\Database\Eloquent\Model  $model
1091
     * @return array
1092
     */
1093
    protected function migratePivotAttributes(Model $model)
1094
    {
1095
        $values = [];
1096
 
1097
        foreach ($model->getAttributes() as $key => $value) {
1098
            // To get the pivots attributes we will just take any of the attributes which
1099
            // begin with "pivot_" and add those to this arrays, as well as unsetting
1100
            // them from the parent's models since they exist in a different table.
1101
            if (str_starts_with($key, 'pivot_')) {
1102
                $values[substr($key, 6)] = $value;
1103
 
1104
                unset($model->$key);
1105
            }
1106
        }
1107
 
1108
        return $values;
1109
    }
1110
 
1111
    /**
1112
     * If we're touching the parent model, touch.
1113
     *
1114
     * @return void
1115
     */
1116
    public function touchIfTouching()
1117
    {
1118
        if ($this->touchingParent()) {
1119
            $this->getParent()->touch();
1120
        }
1121
 
1122
        if ($this->getParent()->touches($this->relationName)) {
1123
            $this->touch();
1124
        }
1125
    }
1126
 
1127
    /**
1128
     * Determine if we should touch the parent on sync.
1129
     *
1130
     * @return bool
1131
     */
1132
    protected function touchingParent()
1133
    {
1134
        return $this->getRelated()->touches($this->guessInverseRelation());
1135
    }
1136
 
1137
    /**
1138
     * Attempt to guess the name of the inverse of the relation.
1139
     *
1140
     * @return string
1141
     */
1142
    protected function guessInverseRelation()
1143
    {
1144
        return Str::camel(Str::pluralStudly(class_basename($this->getParent())));
1145
    }
1146
 
1147
    /**
1148
     * Touch all of the related models for the relationship.
1149
     *
1150
     * E.g.: Touch all roles associated with this user.
1151
     *
1152
     * @return void
1153
     */
1154
    public function touch()
1155
    {
1156
        $columns = [
1157
            $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(),
1158
        ];
1159
 
1160
        // If we actually have IDs for the relation, we will run the query to update all
1161
        // the related model's timestamps, to make sure these all reflect the changes
1162
        // to the parent models. This will help us keep any caching synced up here.
1163
        if (count($ids = $this->allRelatedIds()) > 0) {
399 lars 1164
            $this->getRelated()->newQueryWithoutRelationships()->whereKey($ids)->update($columns);
148 lars 1165
        }
1166
    }
1167
 
1168
    /**
1169
     * Get all of the IDs for the related models.
1170
     *
1171
     * @return \Illuminate\Support\Collection
1172
     */
1173
    public function allRelatedIds()
1174
    {
1175
        return $this->newPivotQuery()->pluck($this->relatedPivotKey);
1176
    }
1177
 
1178
    /**
1179
     * Save a new model and attach it to the parent model.
1180
     *
1181
     * @param  \Illuminate\Database\Eloquent\Model  $model
1182
     * @param  array  $pivotAttributes
1183
     * @param  bool  $touch
1184
     * @return \Illuminate\Database\Eloquent\Model
1185
     */
1186
    public function save(Model $model, array $pivotAttributes = [], $touch = true)
1187
    {
1188
        $model->save(['touch' => false]);
1189
 
1190
        $this->attach($model, $pivotAttributes, $touch);
1191
 
1192
        return $model;
1193
    }
1194
 
1195
    /**
1196
     * Save a new model without raising any events and attach it to the parent model.
1197
     *
1198
     * @param  \Illuminate\Database\Eloquent\Model  $model
1199
     * @param  array  $pivotAttributes
1200
     * @param  bool  $touch
1201
     * @return \Illuminate\Database\Eloquent\Model
1202
     */
1203
    public function saveQuietly(Model $model, array $pivotAttributes = [], $touch = true)
1204
    {
1205
        return Model::withoutEvents(function () use ($model, $pivotAttributes, $touch) {
1206
            return $this->save($model, $pivotAttributes, $touch);
1207
        });
1208
    }
1209
 
1210
    /**
1211
     * Save an array of new models and attach them to the parent model.
1212
     *
1213
     * @param  \Illuminate\Support\Collection|array  $models
1214
     * @param  array  $pivotAttributes
1215
     * @return array
1216
     */
1217
    public function saveMany($models, array $pivotAttributes = [])
1218
    {
1219
        foreach ($models as $key => $model) {
1220
            $this->save($model, (array) ($pivotAttributes[$key] ?? []), false);
1221
        }
1222
 
1223
        $this->touchIfTouching();
1224
 
1225
        return $models;
1226
    }
1227
 
1228
    /**
1229
     * Save an array of new models without raising any events and attach them to the parent model.
1230
     *
1231
     * @param  \Illuminate\Support\Collection|array  $models
1232
     * @param  array  $pivotAttributes
1233
     * @return array
1234
     */
1235
    public function saveManyQuietly($models, array $pivotAttributes = [])
1236
    {
1237
        return Model::withoutEvents(function () use ($models, $pivotAttributes) {
1238
            return $this->saveMany($models, $pivotAttributes);
1239
        });
1240
    }
1241
 
1242
    /**
1243
     * Create a new instance of the related model.
1244
     *
1245
     * @param  array  $attributes
1246
     * @param  array  $joining
1247
     * @param  bool  $touch
1248
     * @return \Illuminate\Database\Eloquent\Model
1249
     */
1250
    public function create(array $attributes = [], array $joining = [], $touch = true)
1251
    {
1252
        $instance = $this->related->newInstance($attributes);
1253
 
1254
        // Once we save the related model, we need to attach it to the base model via
1255
        // through intermediate table so we'll use the existing "attach" method to
1256
        // accomplish this which will insert the record and any more attributes.
1257
        $instance->save(['touch' => false]);
1258
 
1259
        $this->attach($instance, $joining, $touch);
1260
 
1261
        return $instance;
1262
    }
1263
 
1264
    /**
1265
     * Create an array of new instances of the related models.
1266
     *
1267
     * @param  iterable  $records
1268
     * @param  array  $joinings
1269
     * @return array
1270
     */
1271
    public function createMany(iterable $records, array $joinings = [])
1272
    {
1273
        $instances = [];
1274
 
1275
        foreach ($records as $key => $record) {
1276
            $instances[] = $this->create($record, (array) ($joinings[$key] ?? []), false);
1277
        }
1278
 
1279
        $this->touchIfTouching();
1280
 
1281
        return $instances;
1282
    }
1283
 
1284
    /**
1285
     * Add the constraints for a relationship query.
1286
     *
1287
     * @param  \Illuminate\Database\Eloquent\Builder  $query
1288
     * @param  \Illuminate\Database\Eloquent\Builder  $parentQuery
1289
     * @param  array|mixed  $columns
1290
     * @return \Illuminate\Database\Eloquent\Builder
1291
     */
1292
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
1293
    {
1294
        if ($parentQuery->getQuery()->from == $query->getQuery()->from) {
1295
            return $this->getRelationExistenceQueryForSelfJoin($query, $parentQuery, $columns);
1296
        }
1297
 
1298
        $this->performJoin($query);
1299
 
1300
        return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
1301
    }
1302
 
1303
    /**
1304
     * Add the constraints for a relationship query on the same table.
1305
     *
1306
     * @param  \Illuminate\Database\Eloquent\Builder  $query
1307
     * @param  \Illuminate\Database\Eloquent\Builder  $parentQuery
1308
     * @param  array|mixed  $columns
1309
     * @return \Illuminate\Database\Eloquent\Builder
1310
     */
1311
    public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $parentQuery, $columns = ['*'])
1312
    {
1313
        $query->select($columns);
1314
 
1315
        $query->from($this->related->getTable().' as '.$hash = $this->getRelationCountHash());
1316
 
1317
        $this->related->setTable($hash);
1318
 
1319
        $this->performJoin($query);
1320
 
1321
        return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
1322
    }
1323
 
1324
    /**
1325
     * Get the key for comparing against the parent key in "has" query.
1326
     *
1327
     * @return string
1328
     */
1329
    public function getExistenceCompareKey()
1330
    {
1331
        return $this->getQualifiedForeignPivotKeyName();
1332
    }
1333
 
1334
    /**
1335
     * Specify that the pivot table has creation and update timestamps.
1336
     *
1337
     * @param  mixed  $createdAt
1338
     * @param  mixed  $updatedAt
1339
     * @return $this
1340
     */
1341
    public function withTimestamps($createdAt = null, $updatedAt = null)
1342
    {
1343
        $this->withTimestamps = true;
1344
 
1345
        $this->pivotCreatedAt = $createdAt;
1346
        $this->pivotUpdatedAt = $updatedAt;
1347
 
1348
        return $this->withPivot($this->createdAt(), $this->updatedAt());
1349
    }
1350
 
1351
    /**
1352
     * Get the name of the "created at" column.
1353
     *
1354
     * @return string
1355
     */
1356
    public function createdAt()
1357
    {
1358
        return $this->pivotCreatedAt ?: $this->parent->getCreatedAtColumn();
1359
    }
1360
 
1361
    /**
1362
     * Get the name of the "updated at" column.
1363
     *
1364
     * @return string
1365
     */
1366
    public function updatedAt()
1367
    {
1368
        return $this->pivotUpdatedAt ?: $this->parent->getUpdatedAtColumn();
1369
    }
1370
 
1371
    /**
1372
     * Get the foreign key for the relation.
1373
     *
1374
     * @return string
1375
     */
1376
    public function getForeignPivotKeyName()
1377
    {
1378
        return $this->foreignPivotKey;
1379
    }
1380
 
1381
    /**
1382
     * Get the fully qualified foreign key for the relation.
1383
     *
1384
     * @return string
1385
     */
1386
    public function getQualifiedForeignPivotKeyName()
1387
    {
1388
        return $this->qualifyPivotColumn($this->foreignPivotKey);
1389
    }
1390
 
1391
    /**
1392
     * Get the "related key" for the relation.
1393
     *
1394
     * @return string
1395
     */
1396
    public function getRelatedPivotKeyName()
1397
    {
1398
        return $this->relatedPivotKey;
1399
    }
1400
 
1401
    /**
1402
     * Get the fully qualified "related key" for the relation.
1403
     *
1404
     * @return string
1405
     */
1406
    public function getQualifiedRelatedPivotKeyName()
1407
    {
1408
        return $this->qualifyPivotColumn($this->relatedPivotKey);
1409
    }
1410
 
1411
    /**
1412
     * Get the parent key for the relationship.
1413
     *
1414
     * @return string
1415
     */
1416
    public function getParentKeyName()
1417
    {
1418
        return $this->parentKey;
1419
    }
1420
 
1421
    /**
1422
     * Get the fully qualified parent key name for the relation.
1423
     *
1424
     * @return string
1425
     */
1426
    public function getQualifiedParentKeyName()
1427
    {
1428
        return $this->parent->qualifyColumn($this->parentKey);
1429
    }
1430
 
1431
    /**
1432
     * Get the related key for the relationship.
1433
     *
1434
     * @return string
1435
     */
1436
    public function getRelatedKeyName()
1437
    {
1438
        return $this->relatedKey;
1439
    }
1440
 
1441
    /**
1442
     * Get the fully qualified related key name for the relation.
1443
     *
1444
     * @return string
1445
     */
1446
    public function getQualifiedRelatedKeyName()
1447
    {
1448
        return $this->related->qualifyColumn($this->relatedKey);
1449
    }
1450
 
1451
    /**
1452
     * Get the intermediate table for the relationship.
1453
     *
1454
     * @return string
1455
     */
1456
    public function getTable()
1457
    {
1458
        return $this->table;
1459
    }
1460
 
1461
    /**
1462
     * Get the relationship name for the relationship.
1463
     *
1464
     * @return string
1465
     */
1466
    public function getRelationName()
1467
    {
1468
        return $this->relationName;
1469
    }
1470
 
1471
    /**
1472
     * Get the name of the pivot accessor for this relationship.
1473
     *
1474
     * @return string
1475
     */
1476
    public function getPivotAccessor()
1477
    {
1478
        return $this->accessor;
1479
    }
1480
 
1481
    /**
1482
     * Get the pivot columns for this relationship.
1483
     *
1484
     * @return array
1485
     */
1486
    public function getPivotColumns()
1487
    {
1488
        return $this->pivotColumns;
1489
    }
1490
 
1491
    /**
1492
     * Qualify the given column name by the pivot table.
1493
     *
1494
     * @param  string  $column
1495
     * @return string
1496
     */
1497
    public function qualifyPivotColumn($column)
1498
    {
1499
        return str_contains($column, '.')
1500
                    ? $column
1501
                    : $this->table.'.'.$column;
1502
    }
1503
}