Passed
Push — 2.x ( 5e81a5...d097d1 )
by Aleksei
14:17
created

QueryBuilder::getQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\ORM\Select;
6
7
use Closure;
8
use Cycle\ORM\Exception\BuilderException;
9
use JetBrains\PhpStorm\ExpectedValues;
10
use Cycle\Database\Driver\Compiler;
11
use Cycle\Database\Query\SelectQuery;
12
13
/**
14
 * Mocks SelectQuery and automatically resolves identifiers for the loaded relations.
15
 *
16
 * @method QueryBuilder distinct()
17
 * @method QueryBuilder where(...$args);
18
 * @method QueryBuilder andWhere(...$args);
19
 * @method QueryBuilder orWhere(...$args);
20
 * @method QueryBuilder having(...$args);
21
 * @method QueryBuilder andHaving(...$args);
22
 * @method QueryBuilder orHaving(...$args);
23
 * @method QueryBuilder orderBy($expression, $direction = 'ASC');
24
 * @method QueryBuilder limit(int $limit)
25
 * @method QueryBuilder offset(int $offset)
26
 * @method int avg($identifier) Perform aggregation (AVG) based on column or expression value.
27
 * @method int min($identifier) Perform aggregation (MIN) based on column or expression value.
28
 * @method int max($identifier) Perform aggregation (MAX) based on column or expression value.
29
 * @method int sum($identifier) Perform aggregation (SUM) based on column or expression value.
30
 */
31
final class QueryBuilder
32
{
33
    private ?string $forward = null;
34
35 6834
    public function __construct(
36
        private SelectQuery $query,
37
        /** @internal */
38
        private AbstractLoader $loader
39
    ) {
40
    }
41
42
    /**
43
     * Forward call to underlying target.
44
     *
45
     * @return mixed|SelectQuery
46
     */
47 6044
    public function __call(string $func, array $args)
48
    {
49 6044
        $result = \call_user_func_array($this->targetFunc($func), $this->proxyArgs($args));
50 6044
        if ($result === $this->query) {
51 6028
            return $this;
52
        }
53
54 72
        return $result;
55
    }
56
57
    /**
58
     * Get currently associated query. Immutable.
59
     */
60 744
    public function getQuery(): ?SelectQuery
61
    {
62 744
        return clone $this->query;
63
    }
64
65
    /**
66
     * Access to underlying loader. Immutable.
67
     */
68
    public function getLoader(): AbstractLoader
69
    {
70
        return clone $this->loader;
71
    }
72
73
    /**
74
     * Select query method prefix for all "where" queries. Can route "where" to "onWhere".
75
     */
76 1016
    public function withForward(
77
        #[ExpectedValues(values: ['where', 'onWhere'])]
78
        string $forward = null
79
    ): self {
80 1016
        $builder = clone $this;
81 1016
        $builder->forward = $forward;
82
83 1016
        return $builder;
84
    }
85
86 520
    public function withQuery(SelectQuery $query): self
87
    {
88 520
        $builder = clone $this;
89 520
        $builder->query = $query;
90
91 520
        return $builder;
92
    }
93
94
    /**
95
     * Resolve given object.field identifier into proper table alias and column name.
96
     * Attention, calling this method would not affect loaded relations, you must call with/load directly!
97
     *
98
     * Use this method for complex relation queries in combination with Expression()
99
     *
100
     * @param bool $autoload If set to true (default) target relation will be automatically loaded.
101
     *
102
     * @throws BuilderException
103
     */
104 5298
    public function resolve(string $identifier, bool $autoload = true): string
105
    {
106 5298
        if ($identifier === '*') {
107 24
            return '*';
108
        }
109
110 5298
        if (!str_contains($identifier, '.')) {
111
            // parent element
112 2514
            return sprintf(
113
                '%s.%s',
114 2514
                $this->loader->getAlias(),
115 2514
                $this->loader->fieldAlias($identifier)
116
            );
117
        }
118
119 4176
        $split = strrpos($identifier, '.');
120
121 4176
        $loader = $this->findLoader(substr($identifier, 0, $split), $autoload);
122 4176
        if ($loader !== null) {
123 4160
            return sprintf(
124
                '%s.%s',
125 4160
                $loader->getAlias(),
126 4160
                $loader->fieldAlias(substr($identifier, $split + 1))
127
            );
128
        }
129
130 48
        return $identifier;
131
    }
132
133
    /**
134
     * Join relation without loading it's data.
135
     */
136 8
    public function with(string $relation, array $options = []): self
137
    {
138 8
        $this->loader->loadRelation($relation, $options, true, false);
139
140 8
        return $this;
141
    }
142
143
    /**
144
     * Find loader associated with given entity/relation alias.
145
     *
146
     * @param bool $autoload When set to true relation will be automatically loaded.
147
     */
148 4176
    private function findLoader(string $name, bool $autoload = true): ?LoaderInterface
149
    {
150 4176
        if (strpos($name, '(')) {
151
            // expressions are not allowed
152 48
            return null;
153
        }
154
155 4160
        if ($name == '' || $name == '@' || $name == $this->loader->getTarget() || $name == $this->loader->getAlias()) {
156 3824
            return $this->loader;
157
        }
158
159 376
        $loader = $autoload ? $this->loader : clone $this->loader;
160
161 376
        return $loader->loadRelation($name, [], true);
162
    }
163
164
    /**
165
     * Replace target where call with another compatible method (for example join or having).
166
     */
167 6044
    private function targetFunc(string $call): callable
168
    {
169 6044
        if ($this->forward != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $this->forward of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
170 1016
            switch (strtolower($call)) {
171 1016
                case 'where':
172 680
                    $call = $this->forward;
173 680
                    break;
174 640
                case 'orwhere':
175 8
                    $call = 'or' . ucfirst($this->forward);
176 8
                    break;
177 640
                case 'andwhere':
178
                    $call = 'and' . ucfirst($this->forward);
179
                    break;
180
            }
181
        }
182
183 6044
        return [$this->query, $call];
184
    }
185
186
    /**
187
     * Automatically modify all identifiers to mount table prefix. Provide ability to automatically resolve
188
     * relations.
189
     */
190 6044
    private function proxyArgs(array $args): array
191
    {
192 6044
        if (!isset($args[0])) {
193 2612
            return $args;
194
        }
195
196 5896
        if (\is_string($args[0])) {
197 4304
            $args[0] = $this->resolve($args[0]);
198
        }
199
200 5896
        if (\is_array($args[0])) {
201 4016
            $args[0] = $this->walkRecursive($args[0], [$this, 'wrap']);
202
        }
203
204 5896
        if ($args[0] instanceof Closure) {
205 472
            $args[0] = $args[0] = function ($q) use ($args): void {
206 472
                $args[0]($this->withQuery($q));
207
            };
208
        }
209
210 5896
        return $args;
211
    }
212
213
    /**
214
     * Automatically resolve identifier value or wrap the expression.
215
     *
216
     * @param mixed $value
217
     */
218 2306
    private function wrap(int|string &$identifier, &$value): void
219
    {
220 2306
        if (!\is_numeric($identifier)) {
221 2306
            $identifier = $this->resolve($identifier);
222
        }
223
224 2306
        if ($value instanceof Closure) {
225
            $value = function ($q) use ($value): void {
226
                $value($this->withQuery($q));
227
            };
228
        }
229
    }
230
231
    /**
232
     * Walk through method arguments using given function.
233
     */
234 4016
    private function walkRecursive(array $input, callable $func, bool $complex = false): array
235
    {
236 4016
        $result = [];
237 4016
        foreach ($input as $k => $v) {
238 2306
            if (\is_array($v)) {
239 304
                if (!\is_numeric($k) && \in_array(strtoupper($k), [Compiler::TOKEN_AND, Compiler::TOKEN_OR], true)) {
240
                    // complex expression like @OR and @AND
241 16
                    $result[$k] = $this->walkRecursive($v, $func, true);
242 16
                    continue;
243
                }
244 304
                if ($complex) {
245 16
                    $v = $this->walkRecursive($v, $func);
246
                }
247
            }
248
249 2306
            $func(...[&$k, &$v]);
250 2306
            $result[$k] = $v;
251
        }
252
253 4016
        return $result;
254
    }
255
}
256