Passed
Push — master ( dd9c2f...5ef467 )
by Ryosuke
02:27
created

adjustArgumentTypeOfOptionalConstraints()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.9332
c 0
b 0
f 0
cc 4
nc 6
nop 2
crap 4
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
9
/**
10
 * Class HasByNonDependentSubqueryMacro
11
 *
12
 * Convert has() and whereHas() constraints to non-dependent subqueries.
13
 */
14
class HasByNonDependentSubqueryMacro
15
{
16
    /**
17
     * @var \Illuminate\Database\Eloquent\Builder
18
     */
19
    protected $query;
20
21
    /**
22
     * HasByNonDependentSubqueryMacro constructor.
23
     *
24
     * @param \Illuminate\Database\Eloquent\Builder $query
25
     */
26 24
    public function __construct(Builder $query)
27
    {
28 24
        $this->query = $query;
29
    }
30
31
    /**
32
     * @param  string|string[]                       $relationMethod
33
     * @param  callable[]|null[]                     $constraints
34
     * @return \Illuminate\Database\Eloquent\Builder
35
     */
36 21
    public function has($relationMethod, ?callable ...$constraints): Builder
37
    {
38 21
        return $this->apply($relationMethod, 'whereIn', ...$constraints);
39
    }
40
41
    /**
42
     * @param  string|string[]                       $relationMethod
43
     * @param  callable[]|null[]                     $constraints
44
     * @return \Illuminate\Database\Eloquent\Builder
45
     */
46 1
    public function orHas($relationMethod, ?callable ...$constraints): Builder
47
    {
48 1
        return $this->apply($relationMethod, 'orWhereIn', ...$constraints);
49
    }
50
51
    /**
52
     * @param  string|string[]                       $relationMethod
53
     * @param  callable[]|null[]                     $constraints
54
     * @return \Illuminate\Database\Eloquent\Builder
55
     */
56 1
    public function doesntHave($relationMethod, ?callable ...$constraints): Builder
57
    {
58 1
        return $this->apply($relationMethod, 'whereNotIn', ...$constraints);
59
    }
60
61
    /**
62
     * @param  string|string[]                       $relationMethod
63
     * @param  callable[]|null[]                     $constraints
64
     * @return \Illuminate\Database\Eloquent\Builder
65
     */
66 1
    public function orDoesntHave($relationMethod, ?callable ...$constraints): Builder
67
    {
68 1
        return $this->apply($relationMethod, 'orWhereNotIn', ...$constraints);
69
    }
70
71
    /**
72
     * Parse nested constraints and iterate them to apply.
73
     *
74
     * @param  string|string[]                       $relationMethod
75
     * @param  string                                $whereInMethod
76
     * @param  callable[]|null[]                     $constraints
77
     * @return \Illuminate\Database\Eloquent\Builder
78
     */
79 24
    protected function apply($relationMethod, string $whereInMethod, ?callable ...$constraints): Builder
80
    {
81
        // Extract dot-chained expressions
82 24
        $relationMethods = \is_string($relationMethod) ? \explode('.', $relationMethod) : \array_values($relationMethod);
83
84
        // Pick the first relation if exists
85 24
        if ($currentRelationMethod = \array_shift($relationMethods)) {
86 24
            $this->applyForCurrentRelation(
87 24
                $currentRelationMethod,
88
                $whereInMethod,
89 24
                function (Relation $query) use ($relationMethods, $whereInMethod, $constraints) {
90
                    // Apply optional constraints
91 21
                    if ($currentConstraints = \array_shift($constraints)) {
92 5
                        $currentConstraints($this->adjustArgumentTypeOfOptionalConstraints($currentConstraints, $query));
93
                    }
94
                    // Apply relations nested under
95 21
                    if ($relationMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relationMethods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
96 3
                        (new static($query->getQuery()))->apply($relationMethods, $whereInMethod, ...$constraints);
97
                    }
98 24
                }
99
            );
100
        }
101
102 21
        return $this->query;
103
    }
104
105
    /**
106
     * Apply the current relation as a non-dependent subquery.
107
     *
108
     * @param string        $relationMethod
109
     * @param string        $whereInMethod
110
     * @param null|callable $constraints
111
     */
112 24
    protected function applyForCurrentRelation(string $relationMethod, string $whereInMethod, callable $constraints): void
113
    {
114
        // Unlike a JOIN-based approach, you don't need give table aliases.
115
        // Table names are never conflicted.
116 24
        if (\preg_match('/\s+as\s+/i', $relationMethod)) {
117 1
            throw new DomainException('Table aliases are not supported.');
118
        }
119
120
        // Create a Relation instance
121 23
        $relation = $this->query->getRelation($relationMethod);
0 ignored issues
show
Bug introduced by
The method getRelation() does not seem to exist on object<Illuminate\Database\Eloquent\Builder>.

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...
122
123
        // Validate the relation and recognize key names
124 23
        $keys = new Keys($relation);
125
126
        // Apply optional constraints and relations nested under
127 21
        $constraints($relation);
128
129
        // Add an "whereIn" constraints for a non-dependent subquery
130 21
        $relation->select($keys->getQualifiedRelatedKeyName());
131 21
        if ($keys->needsPolymorphicRelatedConstraints()) {
132 3
            $relation->where($keys->getQualifiedRelatedMorphType(), $keys->getRelatedMorphClass());
133
        }
134 21
        $this->query->{$whereInMethod}($keys->getQualifiedSourceKeyName(), $relation->getQuery());
135
    }
136
137
    /**
138
     * From v1.1:
139
     *   Relation will be automatically converted to Builder to prevent common mistakes on demand.
140
     *
141
     * @param  callable                                                                               $constraint
142
     * @param  \Illuminate\Database\Eloquent\Relations\Relation                                       $relation
143
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation
144
     */
145 5
    protected function adjustArgumentTypeOfOptionalConstraints(callable $constraint, Relation $relation)
146
    {
147 5
        $reflection = ReflectionCallable::from($constraint);
148
149 5
        return $reflection->getNumberOfParameters() > 0
150 5
            && ($parameter = $reflection->getParameters()[0])->hasType()
151 5
            && \is_a($parameter->getType()->getName(), Builder::class, true)
152 1
            ? $relation->getQuery()
153 5
            : $relation;
154
    }
155
}
156