Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | 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\InteractsWithDictionary;
12
use Illuminate\Database\Eloquent\SoftDeletes;
13
 
14
class HasManyThrough extends Relation
15
{
16
    use InteractsWithDictionary;
17
 
18
    /**
19
     * The "through" parent model instance.
20
     *
21
     * @var \Illuminate\Database\Eloquent\Model
22
     */
23
    protected $throughParent;
24
 
25
    /**
26
     * The far parent model instance.
27
     *
28
     * @var \Illuminate\Database\Eloquent\Model
29
     */
30
    protected $farParent;
31
 
32
    /**
33
     * The near key on the relationship.
34
     *
35
     * @var string
36
     */
37
    protected $firstKey;
38
 
39
    /**
40
     * The far key on the relationship.
41
     *
42
     * @var string
43
     */
44
    protected $secondKey;
45
 
46
    /**
47
     * The local key on the relationship.
48
     *
49
     * @var string
50
     */
51
    protected $localKey;
52
 
53
    /**
54
     * The local key on the intermediary model.
55
     *
56
     * @var string
57
     */
58
    protected $secondLocalKey;
59
 
60
    /**
61
     * Create a new has many through relationship instance.
62
     *
63
     * @param  \Illuminate\Database\Eloquent\Builder  $query
64
     * @param  \Illuminate\Database\Eloquent\Model  $farParent
65
     * @param  \Illuminate\Database\Eloquent\Model  $throughParent
66
     * @param  string  $firstKey
67
     * @param  string  $secondKey
68
     * @param  string  $localKey
69
     * @param  string  $secondLocalKey
70
     * @return void
71
     */
72
    public function __construct(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey)
73
    {
74
        $this->localKey = $localKey;
75
        $this->firstKey = $firstKey;
76
        $this->secondKey = $secondKey;
77
        $this->farParent = $farParent;
78
        $this->throughParent = $throughParent;
79
        $this->secondLocalKey = $secondLocalKey;
80
 
81
        parent::__construct($query, $throughParent);
82
    }
83
 
84
    /**
85
     * Set the base constraints on the relation query.
86
     *
87
     * @return void
88
     */
89
    public function addConstraints()
90
    {
91
        $localValue = $this->farParent[$this->localKey];
92
 
93
        $this->performJoin();
94
 
95
        if (static::$constraints) {
96
            $this->query->where($this->getQualifiedFirstKeyName(), '=', $localValue);
97
        }
98
    }
99
 
100
    /**
101
     * Set the join clause on the query.
102
     *
103
     * @param  \Illuminate\Database\Eloquent\Builder|null  $query
104
     * @return void
105
     */
106
    protected function performJoin(Builder $query = null)
107
    {
108
        $query = $query ?: $this->query;
109
 
110
        $farKey = $this->getQualifiedFarKeyName();
111
 
112
        $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $farKey);
113
 
114
        if ($this->throughParentSoftDeletes()) {
115
            $query->withGlobalScope('SoftDeletableHasManyThrough', function ($query) {
116
                $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
117
            });
118
        }
119
    }
120
 
121
    /**
122
     * Get the fully qualified parent key name.
123
     *
124
     * @return string
125
     */
126
    public function getQualifiedParentKeyName()
127
    {
128
        return $this->parent->qualifyColumn($this->secondLocalKey);
129
    }
130
 
131
    /**
132
     * Determine whether "through" parent of the relation uses Soft Deletes.
133
     *
134
     * @return bool
135
     */
136
    public function throughParentSoftDeletes()
137
    {
138
        return in_array(SoftDeletes::class, class_uses_recursive($this->throughParent));
139
    }
140
 
141
    /**
142
     * Indicate that trashed "through" parents should be included in the query.
143
     *
144
     * @return $this
145
     */
146
    public function withTrashedParents()
147
    {
148
        $this->query->withoutGlobalScope('SoftDeletableHasManyThrough');
149
 
150
        return $this;
151
    }
152
 
153
    /**
154
     * Set the constraints for an eager load of the relation.
155
     *
156
     * @param  array  $models
157
     * @return void
158
     */
159
    public function addEagerConstraints(array $models)
160
    {
161
        $whereIn = $this->whereInMethod($this->farParent, $this->localKey);
162
 
163
        $this->query->{$whereIn}(
164
            $this->getQualifiedFirstKeyName(), $this->getKeys($models, $this->localKey)
165
        );
166
    }
167
 
168
    /**
169
     * Initialize the relation on a set of models.
170
     *
171
     * @param  array  $models
172
     * @param  string  $relation
173
     * @return array
174
     */
175
    public function initRelation(array $models, $relation)
176
    {
177
        foreach ($models as $model) {
178
            $model->setRelation($relation, $this->related->newCollection());
179
        }
180
 
181
        return $models;
182
    }
183
 
184
    /**
185
     * Match the eagerly loaded results to their parents.
186
     *
187
     * @param  array  $models
188
     * @param  \Illuminate\Database\Eloquent\Collection  $results
189
     * @param  string  $relation
190
     * @return array
191
     */
192
    public function match(array $models, Collection $results, $relation)
193
    {
194
        $dictionary = $this->buildDictionary($results);
195
 
196
        // Once we have the dictionary we can simply spin through the parent models to
197
        // link them up with their children using the keyed dictionary to make the
198
        // matching very convenient and easy work. Then we'll just return them.
199
        foreach ($models as $model) {
200
            if (isset($dictionary[$key = $this->getDictionaryKey($model->getAttribute($this->localKey))])) {
201
                $model->setRelation(
202
                    $relation, $this->related->newCollection($dictionary[$key])
203
                );
204
            }
205
        }
206
 
207
        return $models;
208
    }
209
 
210
    /**
211
     * Build model dictionary keyed by the relation's foreign key.
212
     *
213
     * @param  \Illuminate\Database\Eloquent\Collection  $results
214
     * @return array
215
     */
216
    protected function buildDictionary(Collection $results)
217
    {
218
        $dictionary = [];
219
 
220
        // First we will create a dictionary of models keyed by the foreign key of the
221
        // relationship as this will allow us to quickly access all of the related
222
        // models without having to do nested looping which will be quite slow.
223
        foreach ($results as $result) {
224
            $dictionary[$result->laravel_through_key][] = $result;
225
        }
226
 
227
        return $dictionary;
228
    }
229
 
230
    /**
231
     * Get the first related model record matching the attributes or instantiate it.
232
     *
233
     * @param  array  $attributes
234
     * @return \Illuminate\Database\Eloquent\Model
235
     */
236
    public function firstOrNew(array $attributes)
237
    {
238
        if (is_null($instance = $this->where($attributes)->first())) {
239
            $instance = $this->related->newInstance($attributes);
240
        }
241
 
242
        return $instance;
243
    }
244
 
245
    /**
246
     * Create or update a related record matching the attributes, and fill it with values.
247
     *
248
     * @param  array  $attributes
249
     * @param  array  $values
250
     * @return \Illuminate\Database\Eloquent\Model
251
     */
252
    public function updateOrCreate(array $attributes, array $values = [])
253
    {
254
        $instance = $this->firstOrNew($attributes);
255
 
256
        $instance->fill($values)->save();
257
 
258
        return $instance;
259
    }
260
 
261
    /**
262
     * Add a basic where clause to the query, and return the first result.
263
     *
264
     * @param  \Closure|string|array  $column
265
     * @param  mixed  $operator
266
     * @param  mixed  $value
267
     * @param  string  $boolean
268
     * @return \Illuminate\Database\Eloquent\Model|static
269
     */
270
    public function firstWhere($column, $operator = null, $value = null, $boolean = 'and')
271
    {
272
        return $this->where($column, $operator, $value, $boolean)->first();
273
    }
274
 
275
    /**
276
     * Execute the query and get the first related model.
277
     *
278
     * @param  array  $columns
279
     * @return mixed
280
     */
281
    public function first($columns = ['*'])
282
    {
283
        $results = $this->take(1)->get($columns);
284
 
285
        return count($results) > 0 ? $results->first() : null;
286
    }
287
 
288
    /**
289
     * Execute the query and get the first result or throw an exception.
290
     *
291
     * @param  array  $columns
292
     * @return \Illuminate\Database\Eloquent\Model|static
293
     *
294
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
295
     */
296
    public function firstOrFail($columns = ['*'])
297
    {
298
        if (! is_null($model = $this->first($columns))) {
299
            return $model;
300
        }
301
 
302
        throw (new ModelNotFoundException)->setModel(get_class($this->related));
303
    }
304
 
305
    /**
306
     * Execute the query and get the first result or call a callback.
307
     *
308
     * @param  \Closure|array  $columns
309
     * @param  \Closure|null  $callback
310
     * @return \Illuminate\Database\Eloquent\Model|static|mixed
311
     */
312
    public function firstOr($columns = ['*'], Closure $callback = null)
313
    {
314
        if ($columns instanceof Closure) {
315
            $callback = $columns;
316
 
317
            $columns = ['*'];
318
        }
319
 
320
        if (! is_null($model = $this->first($columns))) {
321
            return $model;
322
        }
323
 
324
        return $callback();
325
    }
326
 
327
    /**
328
     * Find a related model by its primary key.
329
     *
330
     * @param  mixed  $id
331
     * @param  array  $columns
332
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
333
     */
334
    public function find($id, $columns = ['*'])
335
    {
336
        if (is_array($id) || $id instanceof Arrayable) {
337
            return $this->findMany($id, $columns);
338
        }
339
 
340
        return $this->where(
341
            $this->getRelated()->getQualifiedKeyName(), '=', $id
342
        )->first($columns);
343
    }
344
 
345
    /**
346
     * Find multiple related models by their primary keys.
347
     *
348
     * @param  \Illuminate\Contracts\Support\Arrayable|array  $ids
349
     * @param  array  $columns
350
     * @return \Illuminate\Database\Eloquent\Collection
351
     */
352
    public function findMany($ids, $columns = ['*'])
353
    {
354
        $ids = $ids instanceof Arrayable ? $ids->toArray() : $ids;
355
 
356
        if (empty($ids)) {
357
            return $this->getRelated()->newCollection();
358
        }
359
 
360
        return $this->whereIn(
361
            $this->getRelated()->getQualifiedKeyName(), $ids
362
        )->get($columns);
363
    }
364
 
365
    /**
366
     * Find a related model by its primary key or throw an exception.
367
     *
368
     * @param  mixed  $id
369
     * @param  array  $columns
370
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
371
     *
372
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException<\Illuminate\Database\Eloquent\Model>
373
     */
374
    public function findOrFail($id, $columns = ['*'])
375
    {
376
        $result = $this->find($id, $columns);
377
 
378
        $id = $id instanceof Arrayable ? $id->toArray() : $id;
379
 
380
        if (is_array($id)) {
381
            if (count($result) === count(array_unique($id))) {
382
                return $result;
383
            }
384
        } elseif (! is_null($result)) {
385
            return $result;
386
        }
387
 
388
        throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
389
    }
390
 
391
    /**
392
     * Find a related model by its primary key or call a callback.
393
     *
394
     * @param  mixed  $id
395
     * @param  \Closure|array  $columns
396
     * @param  \Closure|null  $callback
397
     * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|mixed
398
     */
399
    public function findOr($id, $columns = ['*'], Closure $callback = null)
400
    {
401
        if ($columns instanceof Closure) {
402
            $callback = $columns;
403
 
404
            $columns = ['*'];
405
        }
406
 
407
        $result = $this->find($id, $columns);
408
 
409
        $id = $id instanceof Arrayable ? $id->toArray() : $id;
410
 
411
        if (is_array($id)) {
412
            if (count($result) === count(array_unique($id))) {
413
                return $result;
414
            }
415
        } elseif (! is_null($result)) {
416
            return $result;
417
        }
418
 
419
        return $callback();
420
    }
421
 
422
    /**
423
     * Get the results of the relationship.
424
     *
425
     * @return mixed
426
     */
427
    public function getResults()
428
    {
429
        return ! is_null($this->farParent->{$this->localKey})
430
                ? $this->get()
431
                : $this->related->newCollection();
432
    }
433
 
434
    /**
435
     * Execute the query as a "select" statement.
436
     *
437
     * @param  array  $columns
438
     * @return \Illuminate\Database\Eloquent\Collection
439
     */
440
    public function get($columns = ['*'])
441
    {
442
        $builder = $this->prepareQueryBuilder($columns);
443
 
444
        $models = $builder->getModels();
445
 
446
        // If we actually found models we will also eager load any relationships that
447
        // have been specified as needing to be eager loaded. This will solve the
448
        // n + 1 query problem for the developer and also increase performance.
449
        if (count($models) > 0) {
450
            $models = $builder->eagerLoadRelations($models);
451
        }
452
 
453
        return $this->related->newCollection($models);
454
    }
455
 
456
    /**
457
     * Get a paginator for the "select" statement.
458
     *
459
     * @param  int|null  $perPage
460
     * @param  array  $columns
461
     * @param  string  $pageName
462
     * @param  int  $page
463
     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
464
     */
465
    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
466
    {
467
        $this->query->addSelect($this->shouldSelect($columns));
468
 
469
        return $this->query->paginate($perPage, $columns, $pageName, $page);
470
    }
471
 
472
    /**
473
     * Paginate the given query into a simple paginator.
474
     *
475
     * @param  int|null  $perPage
476
     * @param  array  $columns
477
     * @param  string  $pageName
478
     * @param  int|null  $page
479
     * @return \Illuminate\Contracts\Pagination\Paginator
480
     */
481
    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
482
    {
483
        $this->query->addSelect($this->shouldSelect($columns));
484
 
485
        return $this->query->simplePaginate($perPage, $columns, $pageName, $page);
486
    }
487
 
488
    /**
489
     * Paginate the given query into a cursor paginator.
490
     *
491
     * @param  int|null  $perPage
492
     * @param  array  $columns
493
     * @param  string  $cursorName
494
     * @param  string|null  $cursor
495
     * @return \Illuminate\Contracts\Pagination\CursorPaginator
496
     */
497
    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
498
    {
499
        $this->query->addSelect($this->shouldSelect($columns));
500
 
501
        return $this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor);
502
    }
503
 
504
    /**
505
     * Set the select clause for the relation query.
506
     *
507
     * @param  array  $columns
508
     * @return array
509
     */
510
    protected function shouldSelect(array $columns = ['*'])
511
    {
512
        if ($columns == ['*']) {
513
            $columns = [$this->related->getTable().'.*'];
514
        }
515
 
516
        return array_merge($columns, [$this->getQualifiedFirstKeyName().' as laravel_through_key']);
517
    }
518
 
519
    /**
520
     * Chunk the results of the query.
521
     *
522
     * @param  int  $count
523
     * @param  callable  $callback
524
     * @return bool
525
     */
526
    public function chunk($count, callable $callback)
527
    {
528
        return $this->prepareQueryBuilder()->chunk($count, $callback);
529
    }
530
 
531
    /**
532
     * Chunk the results of a query by comparing numeric IDs.
533
     *
534
     * @param  int  $count
535
     * @param  callable  $callback
536
     * @param  string|null  $column
537
     * @param  string|null  $alias
538
     * @return bool
539
     */
540
    public function chunkById($count, callable $callback, $column = null, $alias = null)
541
    {
542
        $column ??= $this->getRelated()->getQualifiedKeyName();
543
 
544
        $alias ??= $this->getRelated()->getKeyName();
545
 
546
        return $this->prepareQueryBuilder()->chunkById($count, $callback, $column, $alias);
547
    }
548
 
549
    /**
550
     * Get a generator for the given query.
551
     *
552
     * @return \Illuminate\Support\LazyCollection
553
     */
554
    public function cursor()
555
    {
556
        return $this->prepareQueryBuilder()->cursor();
557
    }
558
 
559
    /**
560
     * Execute a callback over each item while chunking.
561
     *
562
     * @param  callable  $callback
563
     * @param  int  $count
564
     * @return bool
565
     */
566
    public function each(callable $callback, $count = 1000)
567
    {
568
        return $this->chunk($count, function ($results) use ($callback) {
569
            foreach ($results as $key => $value) {
570
                if ($callback($value, $key) === false) {
571
                    return false;
572
                }
573
            }
574
        });
575
    }
576
 
577
    /**
578
     * Query lazily, by chunks of the given size.
579
     *
580
     * @param  int  $chunkSize
581
     * @return \Illuminate\Support\LazyCollection
582
     */
583
    public function lazy($chunkSize = 1000)
584
    {
585
        return $this->prepareQueryBuilder()->lazy($chunkSize);
586
    }
587
 
588
    /**
589
     * Query lazily, by chunking the results of a query by comparing IDs.
590
     *
591
     * @param  int  $chunkSize
592
     * @param  string|null  $column
593
     * @param  string|null  $alias
594
     * @return \Illuminate\Support\LazyCollection
595
     */
596
    public function lazyById($chunkSize = 1000, $column = null, $alias = null)
597
    {
598
        $column ??= $this->getRelated()->getQualifiedKeyName();
599
 
600
        $alias ??= $this->getRelated()->getKeyName();
601
 
602
        return $this->prepareQueryBuilder()->lazyById($chunkSize, $column, $alias);
603
    }
604
 
605
    /**
606
     * Prepare the query builder for query execution.
607
     *
608
     * @param  array  $columns
609
     * @return \Illuminate\Database\Eloquent\Builder
610
     */
611
    protected function prepareQueryBuilder($columns = ['*'])
612
    {
613
        $builder = $this->query->applyScopes();
614
 
615
        return $builder->addSelect(
616
            $this->shouldSelect($builder->getQuery()->columns ? [] : $columns)
617
        );
618
    }
619
 
620
    /**
621
     * Add the constraints for a relationship query.
622
     *
623
     * @param  \Illuminate\Database\Eloquent\Builder  $query
624
     * @param  \Illuminate\Database\Eloquent\Builder  $parentQuery
625
     * @param  array|mixed  $columns
626
     * @return \Illuminate\Database\Eloquent\Builder
627
     */
628
    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
629
    {
630
        if ($parentQuery->getQuery()->from === $query->getQuery()->from) {
631
            return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns);
632
        }
633
 
634
        if ($parentQuery->getQuery()->from === $this->throughParent->getTable()) {
635
            return $this->getRelationExistenceQueryForThroughSelfRelation($query, $parentQuery, $columns);
636
        }
637
 
638
        $this->performJoin($query);
639
 
640
        return $query->select($columns)->whereColumn(
641
            $this->getQualifiedLocalKeyName(), '=', $this->getQualifiedFirstKeyName()
642
        );
643
    }
644
 
645
    /**
646
     * Add the constraints for a relationship query on the same table.
647
     *
648
     * @param  \Illuminate\Database\Eloquent\Builder  $query
649
     * @param  \Illuminate\Database\Eloquent\Builder  $parentQuery
650
     * @param  array|mixed  $columns
651
     * @return \Illuminate\Database\Eloquent\Builder
652
     */
653
    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
654
    {
655
        $query->from($query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash());
656
 
657
        $query->join($this->throughParent->getTable(), $this->getQualifiedParentKeyName(), '=', $hash.'.'.$this->secondKey);
658
 
659
        if ($this->throughParentSoftDeletes()) {
660
            $query->whereNull($this->throughParent->getQualifiedDeletedAtColumn());
661
        }
662
 
663
        $query->getModel()->setTable($hash);
664
 
665
        return $query->select($columns)->whereColumn(
666
            $parentQuery->getQuery()->from.'.'.$this->localKey, '=', $this->getQualifiedFirstKeyName()
667
        );
668
    }
669
 
670
    /**
671
     * Add the constraints for a relationship query on the same table as the through parent.
672
     *
673
     * @param  \Illuminate\Database\Eloquent\Builder  $query
674
     * @param  \Illuminate\Database\Eloquent\Builder  $parentQuery
675
     * @param  array|mixed  $columns
676
     * @return \Illuminate\Database\Eloquent\Builder
677
     */
678
    public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
679
    {
680
        $table = $this->throughParent->getTable().' as '.$hash = $this->getRelationCountHash();
681
 
682
        $query->join($table, $hash.'.'.$this->secondLocalKey, '=', $this->getQualifiedFarKeyName());
683
 
684
        if ($this->throughParentSoftDeletes()) {
685
            $query->whereNull($hash.'.'.$this->throughParent->getDeletedAtColumn());
686
        }
687
 
688
        return $query->select($columns)->whereColumn(
689
            $parentQuery->getQuery()->from.'.'.$this->localKey, '=', $hash.'.'.$this->firstKey
690
        );
691
    }
692
 
693
    /**
694
     * Get the qualified foreign key on the related model.
695
     *
696
     * @return string
697
     */
698
    public function getQualifiedFarKeyName()
699
    {
700
        return $this->getQualifiedForeignKeyName();
701
    }
702
 
703
    /**
704
     * Get the foreign key on the "through" model.
705
     *
706
     * @return string
707
     */
708
    public function getFirstKeyName()
709
    {
710
        return $this->firstKey;
711
    }
712
 
713
    /**
714
     * Get the qualified foreign key on the "through" model.
715
     *
716
     * @return string
717
     */
718
    public function getQualifiedFirstKeyName()
719
    {
720
        return $this->throughParent->qualifyColumn($this->firstKey);
721
    }
722
 
723
    /**
724
     * Get the foreign key on the related model.
725
     *
726
     * @return string
727
     */
728
    public function getForeignKeyName()
729
    {
730
        return $this->secondKey;
731
    }
732
 
733
    /**
734
     * Get the qualified foreign key on the related model.
735
     *
736
     * @return string
737
     */
738
    public function getQualifiedForeignKeyName()
739
    {
740
        return $this->related->qualifyColumn($this->secondKey);
741
    }
742
 
743
    /**
744
     * Get the local key on the far parent model.
745
     *
746
     * @return string
747
     */
748
    public function getLocalKeyName()
749
    {
750
        return $this->localKey;
751
    }
752
 
753
    /**
754
     * Get the qualified local key on the far parent model.
755
     *
756
     * @return string
757
     */
758
    public function getQualifiedLocalKeyName()
759
    {
760
        return $this->farParent->qualifyColumn($this->localKey);
761
    }
762
 
763
    /**
764
     * Get the local key on the intermediary model.
765
     *
766
     * @return string
767
     */
768
    public function getSecondLocalKeyName()
769
    {
770
        return $this->secondLocalKey;
771
    }
772
}