Where::sql()   F
last analyzed

Complexity

Conditions 24
Paths 172

Size

Total Lines 60
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 24
eloc 44
nc 172
nop 0
dl 0
loc 60
rs 3.5666
c 2
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of FacturaScripts
4
 * Copyright (C) 2023 Carlos Garcia Gomez <[email protected]>
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18
 */
19
20
namespace FacturaScripts\Core;
21
22
use Exception;
23
use FacturaScripts\Core\Base\DataBase;
24
use FacturaScripts\Core\Base\DataBase\DataBaseWhere;
25
26
/**
27
 * @author Carlos García Gómez <[email protected]>
28
 */
29
final class Where
30
{
31
    const FIELD_SEPARATOR = '|';
32
33
    /** @var DataBase */
34
    private static $db;
35
36
    /** @var string */
37
    public $fields;
38
39
    /** @var string */
40
    public $operator;
41
42
    /** @var string */
43
    public $operation;
44
45
    /** @var Where[] */
46
    public $subWhere;
47
48
    /** @var mixed */
49
    public $value;
50
51
    public function __construct(string $fields, $value, string $operator = '=', string $operation = 'AND')
52
    {
53
        $this->fields = $fields;
54
        $this->value = $value;
55
        $this->operator = $operator;
56
        $this->operation = $operation;
57
    }
58
59
    public static function between(string $fields, $value1, $value2): self
60
    {
61
        return new self($fields, [$value1, $value2], 'BETWEEN');
62
    }
63
64
    public static function column(string $fields, $value, string $operator = '=', string $operation = 'AND'): self
65
    {
66
        return new self($fields, $value, $operator, $operation);
67
    }
68
69
    public static function eq(string $fields, $value): self
70
    {
71
        return new self($fields, $value, '=');
72
    }
73
74
    public static function gt(string $fields, $value): self
75
    {
76
        return new self($fields, $value, '>');
77
    }
78
79
    public static function gte(string $fields, $value): self
80
    {
81
        return new self($fields, $value, '>=');
82
    }
83
84
    public static function in(string $fields, $values): self
85
    {
86
        return new self($fields, $values, 'IN');
87
    }
88
89
    public static function isNotNull(string $fields): self
90
    {
91
        return new self($fields, null, 'IS NOT');
92
    }
93
94
    public static function isNull(string $fields): self
95
    {
96
        return new self($fields, null, 'IS');
97
    }
98
99
    public static function like(string $fields, string $value): self
100
    {
101
        return new self($fields, $value, 'LIKE');
102
    }
103
104
    public static function lt(string $fields, $value): self
105
    {
106
        return new self($fields, $value, '<');
107
    }
108
109
    public static function lte(string $fields, $value): self
110
    {
111
        return new self($fields, $value, '<=');
112
    }
113
114
    public static function multiSql(array $where): string
115
    {
116
        $sql = '';
117
        foreach ($where as $item) {
118
            // si no es una instancia de Where, lanzamos una excepción
119
            if (!($item instanceof self)) {
120
                throw new Exception('Invalid where clause ' . print_r($item, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($item, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

120
                throw new Exception('Invalid where clause ' . /** @scrutinizer ignore-type */ print_r($item, true));
Loading history...
121
            }
122
123
            if (!empty($sql)) {
124
                $sql .= ' ' . $item->operation . ' ';
125
            }
126
127
            if ($item->operator === '(') {
128
                $sql .= '(' . self::multiSql($item->subWhere) . ')';
129
                continue;
130
            }
131
132
            $sql .= $item->sql();
133
        }
134
135
        return $sql;
136
    }
137
138
    public static function multiSqlLegacy(array $where): string
139
    {
140
        $sql = '';
141
        $group = false;
142
143
        foreach ($where as $key => $item) {
144
            // si es una instancia de DataBaseWhere, lo convertimos a sql
145
            if ($item instanceof DataBaseWhere) {
146
                $dbWhere = new self($item->fields, $item->value, $item->operator, $item->operation);
147
148
                if (!empty($sql)) {
149
                    $sql .= ' ' . $item->operation . ' ';
150
                }
151
152
                // si el siguiente elemento es un OR, lo agrupamos
153
                if (!$group && isset($where[$key + 1]) && $where[$key + 1] instanceof DataBaseWhere && $where[$key + 1]->operation === 'OR') {
154
                    $sql .= '(';
155
                    $group = true;
156
                }
157
158
                $sql .= $dbWhere->sql();
159
160
                // si estamos agrupando y el siguiente elemento no es un OR, cerramos el grupo
161
                if ($group && (!isset($where[$key + 1]) || !($where[$key + 1] instanceof DataBaseWhere) || $where[$key + 1]->operation !== 'OR')) {
162
                    $sql .= ')';
163
                    $group = false;
164
                }
165
                continue;
166
            }
167
168
            // si no es una instancia de Where, lanzamos una excepción
169
            if (!($item instanceof self)) {
170
                throw new Exception('Invalid where clause ' . print_r($item, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($item, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

170
                throw new Exception('Invalid where clause ' . /** @scrutinizer ignore-type */ print_r($item, true));
Loading history...
171
            }
172
173
            if (!empty($sql)) {
174
                $sql .= ' ' . $item->operation . ' ';
175
            }
176
177
            if ($item->operator === '(') {
178
                $sql .= '(' . self::multiSql($item->subWhere) . ')';
179
                continue;
180
            }
181
182
            $sql .= $item->sql();
183
        }
184
185
        return empty($sql) ? '' : ' WHERE ' . $sql;
186
    }
187
188
    public static function notBetween(string $fields, $value1, $value2): self
189
    {
190
        return new self($fields, [$value1, $value2], 'NOT BETWEEN');
191
    }
192
193
    public static function notEq(string $fields, $value): self
194
    {
195
        return new self($fields, $value, '!=');
196
    }
197
198
    public static function notIn(string $fields, $values): self
199
    {
200
        return new self($fields, $values, 'NOT IN');
201
    }
202
203
    public static function notLike(string $fields, string $value): self
204
    {
205
        return new self($fields, $value, 'NOT LIKE');
206
    }
207
208
    public static function or(string $fields, $value, string $operator = '='): self
209
    {
210
        return new self($fields, $value, $operator, 'OR');
211
    }
212
213
    public static function orBetween(string $fields, $value1, $value2): self
214
    {
215
        return new self($fields, [$value1, $value2], 'BETWEEN', 'OR');
216
    }
217
218
    public static function orEq(string $fields, $value): self
219
    {
220
        return new self($fields, $value, '=', 'OR');
221
    }
222
223
    public static function orGt(string $fields, $value): self
224
    {
225
        return new self($fields, $value, '>', 'OR');
226
    }
227
228
    public static function orGte(string $fields, $value): self
229
    {
230
        return new self($fields, $value, '>=', 'OR');
231
    }
232
233
    public static function orIn(string $fields, $values): self
234
    {
235
        return new self($fields, $values, 'IN', 'OR');
236
    }
237
238
    public static function orIsNotNull(string $fields): self
239
    {
240
        return new self($fields, null, 'IS NOT', 'OR');
241
    }
242
243
    public static function orIsNull(string $fields): self
244
    {
245
        return new self($fields, null, 'IS', 'OR');
246
    }
247
248
    public static function orLike(string $fields, string $value): self
249
    {
250
        return new self($fields, $value, 'LIKE', 'OR');
251
    }
252
253
    public static function orLt(string $fields, $value): self
254
    {
255
        return new self($fields, $value, '<', 'OR');
256
    }
257
258
    public static function orLte(string $fields, $value): self
259
    {
260
        return new self($fields, $value, '<=', 'OR');
261
    }
262
263
    public static function orNotBetween(string $fields, $value1, $value2): self
264
    {
265
        return new self($fields, [$value1, $value2], 'NOT BETWEEN', 'OR');
266
    }
267
268
    public static function orNotEq(string $fields, $value): self
269
    {
270
        return new self($fields, $value, '!=', 'OR');
271
    }
272
273
    public static function orNotIn(string $fields, $values): self
274
    {
275
        return new self($fields, $values, 'NOT IN', 'OR');
276
    }
277
278
    public static function orNotLike(string $fields, string $value): self
279
    {
280
        return new self($fields, $value, 'NOT LIKE', 'OR');
281
    }
282
283
    public static function orRegexp(string $fields, string $value): self
284
    {
285
        return new self($fields, $value, 'REGEXP', 'OR');
286
    }
287
288
    public static function orSub(array $where): self
289
    {
290
        return self::sub($where, 'OR');
291
    }
292
293
    public static function orXlike(string $fields, string $value): self
294
    {
295
        return new self($fields, $value, 'XLIKE', 'OR');
296
    }
297
298
    public static function regexp(string $fields, string $value): self
299
    {
300
        return new self($fields, $value, 'REGEXP');
301
    }
302
303
    public function sql(): string
304
    {
305
        $fields = explode(self::FIELD_SEPARATOR, $this->fields);
306
307
        $sql = count($fields) > 1 ? '(' : '';
308
309
        foreach ($fields as $key => $field) {
310
            if ($key > 0) {
311
                $sql .= ' OR ';
312
            }
313
314
            switch ($this->operator) {
315
                case '=':
316
                    $sql .= is_null($this->value) ?
317
                        self::sqlColumn($field) . ' IS NULL' :
318
                        self::sqlColumn($field) . ' = ' . self::sqlValue($this->value);
319
                    break;
320
321
                case '!=':
322
                case '<>':
323
                    $sql .= is_null($this->value) ?
324
                        self::sqlColumn($field) . ' IS NOT NULL' :
325
                        self::sqlColumn($field) . ' ' . $this->operator . ' ' . self::sqlValue($this->value);
326
                    break;
327
328
                case '>':
329
                case '<':
330
                case '>=':
331
                case '<=':
332
                case 'REGEXP':
333
                    $sql .= self::sqlColumn($field) . ' ' . $this->operator . ' ' . self::sqlValue($this->value);
334
                    break;
335
336
                case 'IS':
337
                case 'IS NOT':
338
                    $sql .= self::sqlColumn($field) . ' ' . $this->operator . ' NULL';
339
                    break;
340
341
                case 'IN':
342
                case 'NOT IN':
343
                    $sql .= self::sqlOperatorIn($field, $this->value, $this->operator);
344
                    break;
345
346
                case 'BETWEEN':
347
                case 'NOT BETWEEN':
348
                    $sql .= self::sqlOperatorBetween($field, $this->value, $this->operator);
349
                    break;
350
351
                case 'LIKE':
352
                case 'NOT LIKE':
353
                    $sql .= self::sqlOperatorLike($field, $this->value, $this->operator);
354
                    break;
355
356
                case 'XLIKE':
357
                    $sql .= self::sqlOperatorXLike($field, $this->value);
358
                    break;
359
            }
360
        }
361
362
        return count($fields) > 1 ? $sql . ')' : $sql;
363
    }
364
365
    public static function sub(array $where, string $operation = 'AND'): self
366
    {
367
        // comprobamos si el $where es un array de Where
368
        foreach ($where as $item) {
369
            // si no es una instancia de Where, lanzamos una excepción
370
            if (!($item instanceof self)) {
371
                throw new Exception('Invalid where clause ' . print_r($item, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($item, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

371
                throw new Exception('Invalid where clause ' . /** @scrutinizer ignore-type */ print_r($item, true));
Loading history...
372
            }
373
        }
374
375
        $item = new self('', '', '(', $operation);
376
        $item->subWhere = $where;
377
        return $item;
378
    }
379
380
    public static function xlike(string $fields, string $value): self
381
    {
382
        return new self($fields, $value, 'XLIKE');
383
    }
384
385
    private static function db(): DataBase
386
    {
387
        if (empty(self::$db)) {
388
            self::$db = new DataBase();
389
        }
390
391
        return self::$db;
392
    }
393
394
    private static function sqlColumn(string $field): string
395
    {
396
        // si empieza por integer: hacemos el cast
397
        if (substr($field, 0, 8) === 'integer:') {
398
            return self::db()->castInteger(substr($field, 8));
399
        }
400
401
        return self::db()->escapeColumn($field);
402
    }
403
404
    private static function sqlOperatorBetween(string $field, $values, string $operator): string
405
    {
406
        // si no es un array, lanzamos una excepción
407
        if (!is_array($values)) {
408
            throw new Exception('Invalid values in where clause ' . print_r($values, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($values, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

408
            throw new Exception('Invalid values in where clause ' . /** @scrutinizer ignore-type */ print_r($values, true));
Loading history...
409
        }
410
411
        // si no tiene 2 elementos, lanzamos una excepción
412
        if (count($values) !== 2) {
413
            throw new Exception('Invalid values in where clause ' . print_r($values, true));
414
        }
415
416
        return self::sqlColumn($field) . ' ' . $operator . ' ' . self::sqlValue($values[0])
417
            . ' AND ' . self::sqlValue($values[1]);
418
    }
419
420
    private static function sqlOperatorIn(string $field, $values, string $operator): string
421
    {
422
        if (is_array($values)) {
423
            $items = [];
424
            foreach ($values as $val) {
425
                $items[] = self::db()->var2str($val);
426
            }
427
428
            return self::sqlColumn($field) . ' ' . $operator . ' (' . implode(',', $items) . ')';
429
        }
430
431
        // si comienza por SELECT, lo tratamos como una subconsulta
432
        if (substr(strtoupper($values), 0, 6) === 'SELECT') {
433
            return self::sqlColumn($field) . ' ' . $operator . ' (' . $values . ')';
434
        }
435
436
        // es un string, separamos los valores por coma
437
        $items = [];
438
        foreach (explode(',', $values) as $val) {
439
            $items[] = self::db()->var2str(trim($val));
440
        }
441
442
        return self::sqlColumn($field) . ' ' . $operator . ' (' . implode(',', $items) . ')';
443
    }
444
445
    private static function sqlOperatorLike(string $field, string $value, string $operator): string
446
    {
447
        // si no contiene %, se los añadimos
448
        if (strpos($value, '%') === false) {
449
            return 'LOWER(' . self::sqlColumn($field) . ') ' . $operator
450
                . " LOWER('%" . self::db()->escapeString($value) . "%')";
451
        }
452
453
        // contiene algún comodín
454
        return 'LOWER(' . self::sqlColumn($field) . ') ' . $operator
455
            . " LOWER('" . self::db()->escapeString($value) . "')";
456
    }
457
458
    private static function sqlOperatorXLike(string $field, string $value): string
459
    {
460
        // separamos las palabras en $value
461
        $words = explode(' ', $value);
462
463
        // si solamente hay una palabra, la tratamos como un like
464
        if (count($words) === 1) {
465
            return '(' . self::sqlOperatorLike($field, $value, 'LIKE') . ')';
466
        }
467
468
        // si hay más de una palabra, las tratamos como un like con OR
469
        $sql = '';
470
        foreach ($words as $word) {
471
            if (!empty($sql)) {
472
                $sql .= ' AND ';
473
            }
474
            $sql .= self::sqlOperatorLike($field, trim($word), 'LIKE');
475
        }
476
477
        return '(' . $sql . ')';
478
    }
479
480
    private static function sqlValue($value): string
481
    {
482
        // si empieza por field: lo tratamos como un campo
483
        if (substr($value, 0, 6) === 'field:') {
484
            return self::sqlColumn(substr($value, 6));
485
        }
486
487
        // si no, lo tratamos como un valor
488
        return self::db()->var2str($value);
489
    }
490
}
491