Passed
Pull Request — master (#11)
by
unknown
02:07
created

determineArgumentType()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

122
        /** @scrutinizer ignore-call */ 
123
        $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...
123
124 23
        // Validate the relation and recognize key names
125
        $keys = new Keys($relation);
126
127 21
        // Apply optional constraints and relations nested under
128
        $constraints($relation);
129
130 21
        // Add an "whereIn" constraints for a non-dependent subquery
131 21
        $relation->select($keys->getQualifiedRelatedKeyName());
132 3
        if ($keys->needsPolymorphicRelatedConstraints()) {
133
            $relation->where($keys->getQualifiedRelatedMorphType(), $keys->getRelatedMorphClass());
134 21
        }
135
        $this->query->{$whereInMethod}($keys->getQualifiedSourceKeyName(), $relation->getQuery());
136
    }
137
138
    /**
139
     * From v1.1:
140
     *   Relation will be automatically converted to Builder to prevent common mistakes on demand.
141
     *
142
     * @param  callable                                                                               $constraint
143
     * @param  \Illuminate\Database\Eloquent\Relations\Relation                                       $relation
144
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
145 5
     */
146
    protected function adjustArgumentTypeOfOptionalConstraints(callable $constraint, Relation $relation)
147 5
    {
148
        $reflection = ReflectionCallable::from($constraint);
149 5
150 5
        $type = $reflection->getNumberOfParameters() > 0
151 5
            && ($parameter = $reflection->getParameters()[0])->hasType()
152 1
            ? $parameter->getType()
153 5
            : null;
154
155
        if (!$type) {
156
            return $relation;
157
        }
158
159
        return $this->determineArgumentType($type, $relation);
160
    }
161
162
    /**
163
     * @param  \ReflectionType                                                                        $type
164
     * @param  \Illuminate\Database\Eloquent\Relations\Relation                                       $relation
165
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
166
     */
167
    protected function determineArgumentType(ReflectionType $type, Relation $relation)
168
    {
169
        // https://www.php.net/manual/en/class.reflectionnamedtype.php
170
        if (method_exists($type, 'getName')) {
171
            return \is_a($type->getName(), Builder::class, true)
172
                ? $relation->getQuery()
173
                : $relation;
174
        }
175
176
        // https://www.php.net/manual/en/class.reflectionuniontype.php
177
        if (method_exists($type, 'getTypes')) {
178
            /** @var ReflectionType $firstType */
179
            $firstType = $type->getTypes()[0];
180
181
            return $this->determineArgumentType($firstType, $relation);
182
        }
183
184
        return $relation;
185
    }
186
}
187