Completed
Push — master ( 26ac29...7345ab )
by Ryosuke
19s queued 11s
created

HasByNonDependentSubqueryMacro::namedTypeIs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
1
<?php
2
3
namespace Mpyw\EloquentHasByNonDependentSubquery;
4
5
use DomainException;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\Relations\Relation;
8
use ReflectionNamedType;
9
use ReflectionType;
10
11
/**
12
 * Class HasByNonDependentSubqueryMacro
13
 *
14
 * Convert has() and whereHas() constraints to non-dependent subqueries.
15
 */
16
class HasByNonDependentSubqueryMacro
17
{
18
    /**
19
     * @var \Illuminate\Database\Eloquent\Builder
20
     */
21
    protected $query;
22
23
    /**
24
     * HasByNonDependentSubqueryMacro constructor.
25
     *
26 24
     * @param \Illuminate\Database\Eloquent\Builder $query
27
     */
28 24
    public function __construct(Builder $query)
29
    {
30
        $this->query = $query;
31
    }
32
33
    /**
34
     * @param  string|string[]                       $relationMethod
35
     * @param  callable[]|null[]                     $constraints
36 21
     * @return \Illuminate\Database\Eloquent\Builder
37
     */
38 21
    public function has($relationMethod, ?callable ...$constraints): Builder
39
    {
40
        return $this->apply($relationMethod, 'whereIn', ...$constraints);
41
    }
42
43
    /**
44
     * @param  string|string[]                       $relationMethod
45
     * @param  callable[]|null[]                     $constraints
46 1
     * @return \Illuminate\Database\Eloquent\Builder
47
     */
48 1
    public function orHas($relationMethod, ?callable ...$constraints): Builder
49
    {
50
        return $this->apply($relationMethod, 'orWhereIn', ...$constraints);
51
    }
52
53
    /**
54
     * @param  string|string[]                       $relationMethod
55
     * @param  callable[]|null[]                     $constraints
56 1
     * @return \Illuminate\Database\Eloquent\Builder
57
     */
58 1
    public function doesntHave($relationMethod, ?callable ...$constraints): Builder
59
    {
60
        return $this->apply($relationMethod, 'whereNotIn', ...$constraints);
61
    }
62
63
    /**
64
     * @param  string|string[]                       $relationMethod
65
     * @param  callable[]|null[]                     $constraints
66 1
     * @return \Illuminate\Database\Eloquent\Builder
67
     */
68 1
    public function orDoesntHave($relationMethod, ?callable ...$constraints): Builder
69
    {
70
        return $this->apply($relationMethod, 'orWhereNotIn', ...$constraints);
71
    }
72
73
    /**
74
     * Parse nested constraints and iterate them to apply.
75
     *
76
     * @param  string|string[]                       $relationMethod
77
     * @param  string                                $whereInMethod
78
     * @param  callable[]|null[]                     $constraints
79 24
     * @return \Illuminate\Database\Eloquent\Builder
80
     */
81
    protected function apply($relationMethod, string $whereInMethod, ?callable ...$constraints): Builder
82 24
    {
83
        // Extract dot-chained expressions
84
        $relationMethods = \is_string($relationMethod) ? \explode('.', $relationMethod) : \array_values($relationMethod);
85 24
86 24
        // Pick the first relation if exists
87 24
        if ($currentRelationMethod = \array_shift($relationMethods)) {
88
            $this->applyForCurrentRelation(
89 24
                $currentRelationMethod,
90
                $whereInMethod,
91 21
                function (Relation $query) use ($relationMethods, $whereInMethod, $constraints) {
92 5
                    // Apply optional constraints
93
                    if ($currentConstraints = \array_shift($constraints)) {
94
                        $currentConstraints($this->adjustArgumentTypeOfOptionalConstraints($currentConstraints, $query));
95 21
                    }
96 3
                    // Apply relations nested under
97
                    if ($relationMethods) {
98 24
                        (new static($query->getQuery()))->apply($relationMethods, $whereInMethod, ...$constraints);
99
                    }
100
                }
101
            );
102 21
        }
103
104
        return $this->query;
105
    }
106
107
    /**
108
     * Apply the current relation as a non-dependent subquery.
109
     *
110
     * @param string        $relationMethod
111
     * @param string        $whereInMethod
112 24
     * @param null|callable $constraints
113
     */
114
    protected function applyForCurrentRelation(string $relationMethod, string $whereInMethod, callable $constraints): void
115
    {
116 24
        // Unlike a JOIN-based approach, you don't need give table aliases.
117 1
        // Table names are never conflicted.
118
        if (\preg_match('/\s+as\s+/i', $relationMethod)) {
119
            throw new DomainException('Table aliases are not supported.');
120
        }
121 23
122
        // Create a Relation instance
123
        $relation = $this->query->getRelation($relationMethod);
0 ignored issues
show
Bug introduced by
The method getRelation() does not exist on Illuminate\Database\Eloquent\Builder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
        /** @scrutinizer ignore-call */ 
124
        $relation = $this->query->getRelation($relationMethod);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
124 23
125
        // Validate the relation and recognize key names
126
        $keys = new Keys($relation);
127 21
128
        // Apply optional constraints and relations nested under
129
        $constraints($relation);
130 21
131 21
        // Add an "whereIn" constraints for a non-dependent subquery
132 3
        $relation->select($keys->getQualifiedRelatedKeyName());
133
        if ($keys->needsPolymorphicRelatedConstraints()) {
134 21
            $relation->where($keys->getQualifiedRelatedMorphType(), $keys->getRelatedMorphClass());
135
        }
136
        $this->query->{$whereInMethod}($keys->getQualifiedSourceKeyName(), $relation->getQuery());
137
    }
138
139
    /**
140
     * From v1.1:
141
     *   Relation will be automatically converted to Builder to prevent common mistakes on demand.
142
     *
143
     * @param  callable                                                                               $constraint
144
     * @param  \Illuminate\Database\Eloquent\Relations\Relation                                       $relation
145 5
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
146
     */
147 5
    protected function adjustArgumentTypeOfOptionalConstraints(callable $constraint, Relation $relation)
148
    {
149 5
        $reflection = ReflectionCallable::from($constraint);
150 5
151 5
        return $reflection->getNumberOfParameters() > 0
152 1
            && ($parameter = $reflection->getParameters()[0])->hasType()
153 5
            && $this->mustExtractEloquentBuilder($parameter->getType())
154
                ? $relation->getQuery()
155
                : $relation;
156
    }
157
158
    /**
159
     * @param  \ReflectionNamedType|\ReflectionType|\ReflectionUnionType $type
160
     * @return bool
161
     */
162
    protected function mustExtractEloquentBuilder(ReflectionType $type): bool
163
    {
164
        /* @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
165
        return $type instanceof \ReflectionUnionType
166
            ? $this->onlyIncludesBuilderType($type)
167
            : $this->namedTypeIs($type, Builder::class);
168
    }
169
170
    /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
171
172
    /**
173
     * @param  \ReflectionUnionType $types
174
     * @return bool
175
     */
176
    protected function onlyIncludesBuilderType(\ReflectionUnionType $types): bool
177
    {
178
        $includesRelationType = false;
179
        $includesBuilderType = false;
180
181
        foreach ($types->getTypes() as $type) {
182
            $includesRelationType = $includesRelationType || $this->namedTypeIs($type, Relation::class);
183
            $includesBuilderType = $includesBuilderType || $this->namedTypeIs($type, Builder::class);
184
        }
185
186
        return !$includesRelationType && $includesBuilderType;
187
    }
188
189
    /**
190
     * @param  \ReflectionNamedType $type
191
     * @param  string               $class
192
     * @return bool
193
     */
194
    protected function namedTypeIs(ReflectionNamedType $type, string $class): bool
195
    {
196
        return \is_a($type->getName(), $class, true);
197
    }
198
}
199