Passed
Push — feature/improved-search-alias-... ( 5f1fd6...88fb3e )
by Bas
03:32 queued 16s
created

HandlesAqlGrammar::wrap()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 10.5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 9
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 21
ccs 5
cts 10
cp 0.5
crap 10.5
rs 9.2222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace LaravelFreelancerNL\Aranguent\Query\Concerns;
6
7
use Illuminate\Support\Arr;
8
use Illuminate\Database\Query\Expression;
9
10
trait HandlesAqlGrammar
11
{
12
    /**
13
     * Available predicate operators.
14
     *
15
     * @var array<string, int>
16
     */
17
    protected array $comparisonOperators = [
18
        '=='      => 1,
19
        '!='      => 1,
20
        '<'       => 1,
21
        '>'       => 1,
22
        '<='      => 1,
23
        '>='      => 1,
24
        'IN'      => 1,
25
        'NOT IN'  => 1,
26
        'LIKE'    => 1,
27
        '~'       => 1,
28
        '!~'      => 1,
29
        'ALL =='  => 1,
30
        'ALL !='  => 1,
31
        'ALL <'   => 1,
32
        'ALL >'   => 1,
33
        'ALL <='  => 1,
34
        'ALL >='  => 1,
35
        'ALL IN'  => 1,
36
        'ANY =='  => 1,
37
        'ANY !='  => 1,
38
        'ANY <'   => 1,
39
        'ANY >'   => 1,
40
        'ANY <='  => 1,
41
        'ANY >='  => 1,
42
        'ANY IN'  => 1,
43
        'NONE ==' => 1,
44
        'NONE !=' => 1,
45
        'NONE <'  => 1,
46
        'NONE >'  => 1,
47
        'NONE <=' => 1,
48
        'NONE >=' => 1,
49
        'NONE IN' => 1,
50
    ];
51
52
    /**
53
     * @var array<string, int>
54
     */
55
    protected array $arithmeticOperators = [
56
        '+' => 1,
57
        '-' => 1,
58
        '*' => 1,
59
        '/' => 1,
60
        '%' => 1,
61
    ];
62
63
    /**
64
     * @var array|int[]
65
     */
66
    protected array $logicalOperators = [
67
        'AND' => 1,
68
        '&&'  => 1,
69
        'OR'  => 1,
70
        '||'  => 1,
71
        'NOT' => 1,
72
        '!'   => 1,
73
    ];
74
75
    protected string $rangeOperator = '..';
76
77
    /**
78
     * Get the format for database stored dates.
79
     *
80
     * @return string
81
     */
82
    public function getDateFormat(): string
83
    {
84
        return 'Y-m-d\TH:i:s.vp';
85
    }
86
87 161
    public function isBind(mixed $value): bool
88
    {
89 161
        if (is_string($value) && preg_match('/^@?[0-9]{4}_' . json_encode($value) . '_[0-9_]+$/', $value)) {
90
            return true;
91
        }
92
93 161
        return false;
94
    }
95
96
    /**
97
     * Get the appropriate query parameter place-holder for a value.
98
     *
99
     * @param  mixed  $value
100
     */
101
    public function parameter($value): string
102
    {
103
        return $this->isExpression($value) ? $this->getValue($value) : (string) $value;
0 ignored issues
show
Bug introduced by
It seems like isExpression() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

103
        return $this->/** @scrutinizer ignore-call */ isExpression($value) ? $this->getValue($value) : (string) $value;
Loading history...
Bug introduced by
It seems like getValue() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

103
        return $this->isExpression($value) ? $this->/** @scrutinizer ignore-call */ getValue($value) : (string) $value;
Loading history...
104
    }
105
106
107
    /**
108
     * Quote the given string literal.
109
     *
110
     * @param  string|array<string>  $value
111
     * @return string
112
     */
113
    public function quoteString($value)
114
    {
115
        if (is_array($value)) {
116
            return implode(', ', array_map([$this, __FUNCTION__], $value));
117
        }
118
119
        return "`$value`";
120
    }
121
122
123
    /**
124
     * Wrap a value in keyword identifiers.
125
     *
126
     * @param  Array<mixed>|Expression|string  $value
127
     * @param  bool  $prefixAlias
128
     * @return array<mixed>|float|int|string
129
     *
130
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
131
     */
132 226
    public function wrap($value, $prefixAlias = false)
133
    {
134 226
        if ($value instanceof Expression) {
135
            return $value->getValue($this);
0 ignored issues
show
Bug introduced by
$this of type LaravelFreelancerNL\Aran...cerns\HandlesAqlGrammar is incompatible with the type Illuminate\Database\Grammar expected by parameter $grammar of Illuminate\Database\Query\Expression::getValue(). ( Ignorable by Annotation )

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

135
            return $value->getValue(/** @scrutinizer ignore-type */ $this);
Loading history...
136
        }
137
138 226
        if (is_array($value)) {
139
            foreach($value as $key => $subvalue) {
140
                $value[$key] = $this->wrap($subvalue, $prefixAlias);
141
            }
142
            return $value;
143
        }
144
145
        // If the value being wrapped has a column alias we will need to separate out
146
        // the pieces so we can wrap each of the segments of the expression on its
147
        // own, and then join these both back together using the "as" connector.
148 226
        if (is_string($value) && stripos($value, ' as ') !== false) {
149
            return $this->wrapAliasedValue($value, $prefixAlias);
0 ignored issues
show
Bug introduced by
It seems like wrapAliasedValue() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

149
            return $this->/** @scrutinizer ignore-call */ wrapAliasedValue($value, $prefixAlias);
Loading history...
150
        }
151
152 226
        return $this->wrapSegments(explode('.', $value));
0 ignored issues
show
Bug introduced by
It seems like wrapSegments() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

152
        return $this->/** @scrutinizer ignore-call */ wrapSegments(explode('.', $value));
Loading history...
153
    }
154
155
156
    /**
157
     * Wrap a table in keyword identifiers.
158
     *
159
     * @param Expression|string  $table
160
     * @return float|int|string
161
     */
162 224
    public function wrapTable($table)
163
    {
164 224
        if (!$table instanceof Expression) {
165 224
            $wrappedTable = $this->wrap($this->tablePrefix . $table, true);
166
167
            assert(!is_array($wrappedTable));
168
169 224
            return $wrappedTable;
170
        }
171
172
        return $table->getValue($this);
0 ignored issues
show
Bug introduced by
$this of type LaravelFreelancerNL\Aran...cerns\HandlesAqlGrammar is incompatible with the type Illuminate\Database\Grammar expected by parameter $grammar of Illuminate\Database\Query\Expression::getValue(). ( Ignorable by Annotation )

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

172
        return $table->getValue(/** @scrutinizer ignore-type */ $this);
Loading history...
173
    }
174
175
    /**
176
     * Wrap a single string in keyword identifiers.
177
     *
178
     * @param  string  $value
179
     * @return string
180
     */
181 226
    protected function wrapValue($value)
182
    {
183 226
        $postfix = '';
184 226
        if ($value === 'groupsVariable') {
185 1
            $postfix = '[*]';
186
        }
187
188 226
        if ($value === '*') {
189 2
            return $value;
190
        }
191
192 226
        return '`' . str_replace('`', '``', $value) . '`' . $postfix;
193
    }
194
195
    /**
196
     * Wrap a subquery single string in braces.
197
     */
198 23
    public function wrapSubquery(string $subquery): string
199
    {
200 23
        return '(' . $subquery . ')';
201
    }
202
203
    /**
204
     * @param array<mixed> $data
205
     * @return string
206
     */
207 125
    public function generateAqlObject(array $data): string
208
    {
209 125
        $data = Arr::undot($data);
210
211 125
        return $this->generateAqlObjectString($data);
212
    }
213
214
    /**
215
     * @param array<mixed> $data
216
     * @return string
217
     */
218 125
    protected function generateAqlObjectString(array $data): string
219
    {
220 125
        foreach($data as $key => $value) {
221 125
            $prefix = $key . ': ';
222
223 125
            if (is_numeric($key)) {
224 1
                $prefix = '';
225
            }
226
227 125
            if (is_bool($value)) {
228
                $booleanString = ($value) ? 'true' : 'false';
229
                $data[$key] = $prefix . $booleanString;
230
231
                continue;
232
            }
233
234 125
            if (is_array($value)) {
235 9
                $data[$key] = $prefix . $this->generateAqlObjectString($value);
236 9
                continue;
237
            }
238
239 125
            if ($value instanceof Expression) {
240
                $data[$key] = $prefix . $value->getValue($this);
0 ignored issues
show
Bug introduced by
$this of type LaravelFreelancerNL\Aran...cerns\HandlesAqlGrammar is incompatible with the type Illuminate\Database\Grammar expected by parameter $grammar of Illuminate\Database\Query\Expression::getValue(). ( Ignorable by Annotation )

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

240
                $data[$key] = $prefix . $value->getValue(/** @scrutinizer ignore-type */ $this);
Loading history...
241
                continue;
242
            }
243
244 125
            $data[$key] = $prefix . $value;
245
        }
246
247 125
        $returnString = implode(', ', $data);
248
249 125
        if (array_is_list($data)) {
250 1
            return '[' . $returnString . ']';
251
        }
252
253 125
        return '{' . $returnString . '}';
254
    }
255
256
    /**
257
     * Substitute the given bindings into the given raw AQL query.
258
     *
259
     * @param  string  $aql
260
     * @param  array<mixed>  $bindings
261
     * @return string
262
     */
263 4
    public function substituteBindingsIntoRawSql($aql, $bindings)
264
    {
265 4
        $bindings = array_map(fn($value) => $this->escape($value), $bindings);
0 ignored issues
show
Bug introduced by
It seems like escape() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

265
        $bindings = array_map(fn($value) => $this->/** @scrutinizer ignore-call */ escape($value), $bindings);
Loading history...
266
267 4
        $bindings = array_reverse($bindings);
268
269 4
        foreach($bindings as $key => $value) {
270 4
            $pattern = '/(@' . $key . ')(?![^a-zA-Z_ ,\}\]])/';
271 4
            $aql = (string) preg_replace(
272 4
                $pattern,
273 4
                $value,
274 4
                $aql
275 4
            );
276
        }
277
278 4
        return $aql;
279
    }
280
281
    /**
282
     * Determine if the given string is a JSON selector.
283
     *
284
     * @param  string  $value
285
     * @return bool
286
     */
287 256
    public function isJsonSelector($value)
288
    {
289 256
        if(!is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
290 5
            return false;
291
        }
292
293 256
        return str_contains($value, '->');
294
    }
295
296 256
    public function convertJsonFields(mixed $data): mixed
297
    {
298 256
        if (!is_array($data) && !is_string($data)) {
299
            return $data;
300
        }
301
302 256
        if (is_string($data)) {
303 202
            return str_replace('->', '.', $data);
304
        }
305
306 256
        if (array_is_list($data)) {
307 254
            return $this->convertJsonValuesToDotNotation($data);
308
        }
309
310 38
        return $this->convertJsonKeysToDotNotation($data);
311
    }
312
313
    /**
314
     * @param array<string> $fields
315
     * @return array<string>
316
     */
317 254
    public function convertJsonValuesToDotNotation(array $fields): array
318
    {
319 254
        foreach($fields as $key => $value) {
320 254
            if ($this->isJsonSelector($value)) {
321 1
                $fields[$key] = str_replace('->', '.', $value);
322
            }
323
        }
324 254
        return $fields;
325
    }
326
327
    /**
328
     * @param array<string> $fields
329
     * @return array<string>
330
     */
331 38
    public function convertJsonKeysToDotNotation(array $fields): array
332
    {
333 38
        foreach($fields as $key => $value) {
334 38
            if ($this->isJsonSelector($key)) {
335 2
                $fields[str_replace('->', '.', $key)] = $value;
336 2
                unset($fields[$key]);
337
            }
338
        }
339 38
        return $fields;
340
    }
341
342
    /**
343
     * Translate sql operators to their AQL equivalent where possible.
344
     *
345
     * @param string $operator
346
     *
347
     * @return mixed|string
348
     */
349
    protected function translateOperator(string $operator)
350
    {
351
        if (isset($this->operatorTranslations[strtolower($operator)])) {
352
            $operator = $this->operatorTranslations[$operator];
353
        }
354
355
        return $operator;
356
    }
357
358
}
359