Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.08% covered (warning)
77.08%
37 / 48
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasManySiblings
77.08% covered (warning)
77.08%
37 / 48
75.00% covered (warning)
75.00%
6 / 8
16.36
0.00% covered (danger)
0.00%
0 / 1
 withOrphans
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 withoutOrphans
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addWithoutOrphansGlobalScope
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addConstraints
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addEagerConstraints
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 match
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getRelationExistenceQuery
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
3.37
 getDictionaryKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Baril\Bonsai\Relations;
4
5use Baril\Bonsai\Relations\Concerns\ExcludesSelf;
6use Illuminate\Database\Eloquent\Builder;
7use Illuminate\Database\Eloquent\Collection as EloquentCollection;
8use Illuminate\Database\Eloquent\Relations\HasMany;
9
10/**
11 * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
12 *
13 * @extends \Illuminate\Database\Eloquent\Relations\HasMany<TDeclaringModel, TDeclaringModel>
14 */
15class HasManySiblings extends HasMany
16{
17    use ExcludesSelf;
18
19    protected $withOrphans = false;
20
21    /**
22     * @return $this
23     */
24    public function withOrphans()
25    {
26        $this->withOrphans = true;
27
28        if (static::$constraints) {
29            $this->getRelationQuery()->withoutGlobalScope('withoutOrphans');
30        }
31
32        return $this;
33    }
34
35    /**
36     * @return $this
37     */
38    public function withoutOrphans()
39    {
40        $this->withOrphans = false;
41
42        if (static::$constraints) {
43            $this->addWithoutOrphansGlobalScope();
44        }
45
46        return $this;
47    }
48
49    /**
50     * @return void
51     */
52    protected function addWithoutOrphansGlobalScope()
53    {
54        $this->getRelationQuery()->withGlobalScope(
55            'withoutOrphans',
56            function ($query) {
57                return $query->whereNotNull($this->foreignKey);
58            }
59        );
60    }
61
62    /** @inheritDoc */
63    public function addConstraints()
64    {
65        if (static::$constraints) {
66            $query = $this->getRelationQuery();
67
68            $query->where($this->foreignKey, '=', $this->getParentKey());
69
70            $this->addWithoutOrphansGlobalScope();
71        }
72    }
73
74    /** @inheritDoc */
75    public function addEagerConstraints(array $models)
76    {
77        $this->where(function ($nestedWhere) use ($models) {
78
79            $keys = $this->getKeys($models, $this->localKey);
80
81            $whereIn = $this->whereInMethod($this->parent, $this->localKey);
82
83            $nestedWhere->{$whereIn}(
84                $this->foreignKey,
85                $keys
86            );
87
88            // At this point, the custom constraints that may have been
89            // provided when calling with() have not been applied yet,
90            // thus we can't trust $this->withOrphans. We have to include
91            // the orphans in the query. We can still exclude them when
92            // we do the matching.
93            if (in_array(null, $keys, true)) {
94                $nestedWhere->orWhereNull($this->foreignKey);
95            }
96        });
97    }
98
99    /** @inheritDoc */
100    public function match(array $models, EloquentCollection $results, $relation)
101    {
102        $results = $results->when(! $this->withOrphans, function ($results) {
103            $foreignKey = explode('.', $this->foreignKey);
104            $foreignKey = end($foreignKey);
105            return $results->whereNotNull($foreignKey);
106        });
107
108        return $this->excludeSelfFromMatchesIfExcluded(
109            parent::match($models, $results, $relation),
110            $relation
111        );
112    }
113
114    /** @inheritDoc */
115    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
116    {
117        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
118
119        if (! $this->withOrphans) {
120            return $query;
121        }
122
123        $from = $query->getQuery()->from;
124        $segments = preg_split('/\s+as\s+/i', $from);
125        $as = end($segments);
126
127        return $query->orWhere(function ($nestedWhere) use ($as) {
128            $nestedWhere->whereNull($this->getQualifiedParentKeyName());
129            $nestedWhere->whereNull($as . '.' . $this->getForeignKeyName());
130        });
131    }
132
133    /**
134     * Overridden in order to allow null keys in Laravel 12.44+
135     * (see https://github.com/laravel/framework/commit/095ef3cd5bd0d32492cc918282d98d72e9c59fd5).
136     *
137     * @param  mixed  $attribute
138     * @return mixed
139     *
140     * @throws \InvalidArgumentException
141     */
142    protected function getDictionaryKey($attribute)
143    {
144        return (null === $attribute) ? '' : parent::getDictionaryKey($attribute);
145    }
146}