Completed
Pull Request — 1.3 (#56)
by David
01:33
created

NodeFactory::escapeDBItem()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
namespace SQLParser\Node;
3
4
use Doctrine\DBAL\Platforms\AbstractPlatform;
5
use Mouf\Database\MagicQueryException;
6
use Mouf\Database\MagicQueryParserException;
7
use SQLParser\SqlRenderInterface;
8
use Mouf\MoufManager;
9
use Mouf\MoufInstanceDescriptor;
10
use SQLParser\Query\StatementFactory;
11
use PHPSQLParser\utils\ExpressionType;
12
13
14
/**
15
 * This class has the ability to create instances implementing NodeInterface based on a descriptive array.
16
 *
17
 * @author David Négrier <[email protected]>
18
 */
19
class NodeFactory
20
{
21
    public static function toObject(array $desc)
22
    {
23
        if (!isset($desc['expr_type'])) {
24
            throw new \Exception('Invalid array. Could not find expression type: '.var_export($desc, true));
25
        }
26
27
        switch ($desc['expr_type']) {
28
            case ExpressionType::CONSTANT:
29
                $const = new ConstNode();
30
                $expr = $desc['base_expr'];
31
                if (strpos($expr, "'") === 0) {
32
                    $expr = substr($expr, 1);
33
                } else {
34
                    $const->setIsString(false);
35
                }
36 View Code Duplication
                if (strrpos($expr, "'") === strlen($expr) - 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
37
                    $expr = substr($expr, 0, -1);
38
                }
39
                $expr = stripslashes($expr);
40
41
                $const->setValue($expr);
42
43
                // If the constant has an alias, it is declared in the columns section.
44
                // If this is the case, let's wrap it in an "expression"
45
                if (isset($desc['alias'])) {
46
                    $expression = new Expression();
47
                    $expression->setBaseExpression($desc['base_expr']);
48
                    $expression->setSubTree($const);
49
                    $expression->setAlias($desc['alias']['name']);
50
                    $expression->setBrackets(false);
51
52
                    $const = $expression;
53
54
                    unset($desc['alias']);
55
                }
56
57
                // Debug:
58
                unset($desc['base_expr']);
59
                unset($desc['expr_type']);
60
                unset($desc['sub_tree']);
61
                unset($desc['delim']);
62
63
                if (!empty($desc)) {
64
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
65
                }
66
67
                return $const;
68
69
            case ExpressionType::OPERATOR:
70
                $operator = new Operator();
71
                $operator->setValue($desc['base_expr']);
72
                // Debug:
73
                unset($desc['base_expr']);
74
                unset($desc['expr_type']);
75 View Code Duplication
                if (!empty($desc['sub_tree'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
76
                    error_log('MagicQuery - NodeFactory: Unexpected operator with subtree: '.var_export($desc['sub_tree'], true));
77
                }
78
                unset($desc['sub_tree']);
79
                if (!empty($desc)) {
80
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
81
                }
82
83
                return $operator;
84
85
            case ExpressionType::COLREF:
86
                if (substr($desc['base_expr'], 0, 1) === ':') {
87
                    $instance = new Parameter();
88
                    $instance->setName(substr($desc['base_expr'], 1));
89
                } else {
90
                    $instance = new ColRef();
91
92
                    if (isset($desc['no_quotes'])) {
93
                        $parts = $desc['no_quotes']['parts'];
94
                    } else {
95
                        $parts = explode('.', str_replace('`', '', $desc['base_expr']));
96
                    }
97
98
                    $columnName = array_pop($parts);
99
                    $instance->setColumn($columnName);
100
101
                    if (!empty($parts)) {
102
                        $tableName = array_pop($parts);
103
                        $instance->setTable($tableName);
104
                    }
105
106
                    if (!empty($parts)) {
107
                        $baseName = array_pop($parts);
108
                        $instance->setDatabase($baseName);
109
                    }
110
111
                    if (!empty($desc['alias'])) {
112
                        $instance->setAlias($desc['alias']['name']);
113
                    }
114
115
                    if (!empty($desc['direction'])) {
116
                        $instance->setDirection($desc['direction']);
117
                    }
118
                }
119
120
                // Debug:
121
                unset($desc['direction']);
122
                unset($desc['base_expr']);
123
                unset($desc['expr_type']);
124 View Code Duplication
                if (!empty($desc['sub_tree'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
125
                    error_log('MagicQuery - NodeFactory: Unexpected operator with subtree: '.var_export($desc['sub_tree'], true));
126
                }
127
                unset($desc['sub_tree']);
128
                unset($desc['alias']);
129
                unset($desc['no_quotes']);
130
                unset($desc['delim']);
131
                if (!empty($desc)) {
132
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
133
                }
134
135
                return $instance;
136
            case ExpressionType::TABLE:
137
                $expr = new Table();
138
139
                if (isset($desc['no_quotes'])) {
140
                    $parts = $desc['no_quotes']['parts'];
141
142
                    $tableName = array_pop($parts);
143
                    $expr->setTable($tableName);
144
145
                    if (!empty($parts)) {
146
                        $baseName = array_pop($parts);
147
                        $expr->setDatabase($baseName);
148
                    }
149
                } else {
150
                    $expr->setTable($desc['table']);
151
                }
152
153
154
155
                $expr->setTable(str_replace('`', '', $desc['table']));
156
                switch ($desc['join_type']) {
157
                    case 'CROSS':
158
                        $joinType = 'CROSS JOIN';
159
                        break;
160
                    case 'JOIN':
161
                        $joinType = 'JOIN';
162
                        break;
163
                    case 'LEFT':
164
                        $joinType = 'LEFT JOIN';
165
                        break;
166
                    case 'RIGHT':
167
                        $joinType = 'RIGHT JOIN';
168
                        break;
169
                    case 'INNER':
170
                        $joinType = 'INNER JOIN';
171
                        break;
172
                    case 'OUTER':
173
                        $joinType = 'OUTER JOIN';
174
                        break;
175
                    case 'NATURAL':
176
                        $joinType = 'NATURAL JOIN';
177
                        break;
178
                    case ',':
179
                        $joinType = ',';
180
                        break;
181
                    default:
182
                        throw new \Exception("Unexpected join type: '".$desc['join_type']."'");
183
                }
184
                $expr->setJoinType($joinType);
185
186
                if (isset($desc['alias'])) {
187
                    $expr->setAlias($desc['alias']['name']);
188
                }
189
                $subTreeNodes = self::buildFromSubtree($desc['ref_clause']);
190
                if ($subTreeNodes) {
191
                    $expr->setRefClause(self::simplify($subTreeNodes));
192
                }
193
194
                // Debug:
195
                unset($desc['base_expr']);
196
                unset($desc['expr_type']);
197 View Code Duplication
                if (!empty($desc['sub_tree'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
                    error_log('MagicQuery - NodeFactory: Unexpected operator with subtree: '.var_export($desc['sub_tree'], true));
199
                }
200
                unset($desc['sub_tree']);
201
                unset($desc['join_type']);
202
                unset($desc['alias']);
203
                unset($desc['table']);
204
                unset($desc['ref_type']);
205
                unset($desc['ref_clause']);
206
                unset($desc['hints']);
207
                unset($desc['no_quotes']);
208
                if (!empty($desc)) {
209
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
210
                }
211
212
                return $expr;
213
            case ExpressionType::SUBQUERY:
214
                $expr = new SubQuery();
215
216
                $expr->setSubQuery(self::buildFromSubtree($desc['sub_tree']));
217
218
                if (isset($desc['join_type'])) {
219
                    $expr->setJoinType($desc['join_type']);
220
                }
221
222
                if (isset($desc['alias'])) {
223
                    $expr->setAlias($desc['alias']['name']);
224
                }
225
226
                if (isset($desc['ref_clause'])) {
227
                    $subTreeNodes = self::buildFromSubtree($desc['ref_clause']);
228
                    if ($subTreeNodes) {
229
                        $expr->setRefClause(self::simplify($subTreeNodes));
230
                    }
231
                }
232
233
                // Debug:
234
                unset($desc['base_expr']);
235
                unset($desc['expr_type']);
236
                unset($desc['sub_tree']);
237
                unset($desc['join_type']);
238
                unset($desc['alias']);
239
                unset($desc['sub_tree']);
240
                unset($desc['ref_type']);
241
                unset($desc['ref_clause']);
242
                unset($desc['hints']);
243
                if (!empty($desc)) {
244
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
245
                }
246
247
                return $expr;
248
            case ExpressionType::AGGREGATE_FUNCTION:
249
                $expr = new AggregateFunction();
250
                $expr->setFunctionName($desc['base_expr']);
251
252
                $expr->setSubTree(self::buildFromSubtree($desc['sub_tree']));
253
254
                if (isset($desc['alias'])) {
255
                    $expr->setAlias($desc['alias']);
256
                }
257
258
                if (isset($desc['direction'])) {
259
                    $expr->setDirection($desc['direction']);
260
                }
261
262
                // Debug:
263
                unset($desc['base_expr']);
264
                unset($desc['expr_type']);
265
                unset($desc['sub_tree']);
266
                unset($desc['alias']);
267
                unset($desc['delim']);
268
                unset($desc['direction']);
269
                if (!empty($desc)) {
270
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in aggregate function: '.var_export($desc, true));
271
                }
272
273
                return $expr;
274
            case ExpressionType::SIMPLE_FUNCTION:
275
                $expr = new SimpleFunction();
276
                $expr->setBaseExpression($desc['base_expr']);
277
278
                if (isset($desc['sub_tree'])) {
279
                    $expr->setSubTree(self::buildFromSubtree($desc['sub_tree']));
280
                }
281
282
                if (isset($desc['alias'])) {
283
                    $expr->setAlias($desc['alias']['name']);
284
                }
285
                if (isset($desc['direction'])) {
286
                    $expr->setDirection($desc['direction']);
287
                }
288
289
                // Debug:
290
                unset($desc['base_expr']);
291
                unset($desc['expr_type']);
292
                unset($desc['sub_tree']);
293
                unset($desc['alias']);
294
                unset($desc['direction']);
295
                unset($desc['delim']);
296
                unset($desc['no_quotes']);
297
                if (!empty($desc)) {
298
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in simple function: '.var_export($desc, true));
299
                }
300
301
                return $expr;
302
            case ExpressionType::RESERVED:
303
                if (in_array(strtoupper($desc['base_expr']), ['CASE', 'WHEN', 'THEN', 'ELSE', 'END'])) {
304
                    $operator = new Operator();
305
                    $operator->setValue($desc['base_expr']);
306
                    // Debug:
307
                    unset($desc['base_expr']);
308
                    unset($desc['expr_type']);
309 View Code Duplication
                    if (!empty($desc['sub_tree'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
310
                        error_log('MagicQuery - NodeFactory: Unexpected operator with subtree: '.var_export($desc['sub_tree'], true));
311
                    }
312
                    unset($desc['sub_tree']);
313
                    unset($desc['delim']);
314
                    if (!empty($desc)) {
315
                        error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
316
                    }
317
318
                    return $operator;
319
                } else {
320
                    $res = new Reserved();
321
                    $res->setBaseExpression($desc['base_expr']);
322
323
                    if ($desc['expr_type'] == ExpressionType::BRACKET_EXPRESSION) {
324
                        $res->setBrackets(true);
325
                    }
326
327
                    // Debug:
328
                    unset($desc['base_expr']);
329
                    unset($desc['expr_type']);
330
                    unset($desc['sub_tree']);
331
                    unset($desc['alias']);
332
                    unset($desc['direction']);
333
                    unset($desc['delim']);
334
                    if (!empty($desc)) {
335
                        error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
336
                    }
337
338
                    return $res;
339
                }
340
            case ExpressionType::USER_VARIABLE:
341
            case ExpressionType::SESSION_VARIABLE:
342
            case ExpressionType::GLOBAL_VARIABLE:
343
            case ExpressionType::LOCAL_VARIABLE:
344
            case ExpressionType::EXPRESSION:
345
            case ExpressionType::BRACKET_EXPRESSION:
346
            case ExpressionType::TABLE_EXPRESSION:
347
348
            case ExpressionType::IN_LIST:
349
350
            case ExpressionType::SIGN:
351
            case ExpressionType::RECORD:
352
353
            case ExpressionType::MATCH_ARGUMENTS:
354
355
            case ExpressionType::ALIAS:
356
            case ExpressionType::POSITION:
357
358
            case ExpressionType::TEMPORARY_TABLE:
359
            case ExpressionType::VIEW:
360
            case ExpressionType::DATABASE:
361
            case ExpressionType::SCHEMA:
362
                $expr = new Expression();
363
                $expr->setBaseExpression($desc['base_expr']);
364
365
                if (isset($desc['sub_tree'])) {
366
                    $expr->setSubTree(self::buildFromSubtree($desc['sub_tree']));
367
                }
368
369
                if (isset($desc['alias'])) {
370
                    $expr->setAlias($desc['alias']['name']);
371
                }
372
                if (isset($desc['direction'])) {
373
                    $expr->setDirection($desc['direction']);
374
                }
375
376
                if ($desc['expr_type'] == ExpressionType::BRACKET_EXPRESSION) {
377
                    $expr->setBrackets(true);
378
                }
379
380
                if ($desc['expr_type'] == ExpressionType::IN_LIST) {
381
                    $expr->setBrackets(true);
382
                    $expr->setDelimiter(',');
383
                }
384
385
                // Debug:
386
                unset($desc['base_expr']);
387
                unset($desc['expr_type']);
388
                unset($desc['sub_tree']);
389
                unset($desc['alias']);
390
                unset($desc['direction']);
391
                unset($desc['delim']);
392
                unset($desc['no_quotes']);
393
                if (!empty($desc)) {
394
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
395
                }
396
397
                return $expr;
398
            case ExpressionType::MATCH_MODE:
399
                $expr = new ConstNode();
400
                $expr->setValue($desc['base_expr']);
401
                $expr->setIsString(false);
402
403
                // Debug:
404
                unset($desc['base_expr']);
405
                unset($desc['expr_type']);
406
                unset($desc['sub_tree']);
407
                unset($desc['alias']);
408
                unset($desc['direction']);
409
                unset($desc['delim']);
410
                if (!empty($desc)) {
411
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
412
                }
413
414
                return $expr;
415
            default:
416
                throw new \Exception('Unknown expression type');
417
        }
418
    }
419
420
421
    /**
422
     * Transforms a limit or offset value/parameter into a node.
423
     *
424
     * @param string $value
425
     * @return NodeInterface
426
     */
427
    public static function toLimitNode($value)
428
    {
429
        if (substr($value, 0, 1) === ':') {
430
            $instance = new UnquotedParameter();
431
            $instance->setName(substr($value, 1));
432
        } else {
433
            $instance = new LimitNode();
434
            $expr = $value;
435
            if (strpos($expr, "'") === 0) {
436
                $expr = substr($expr, 1);
437
            }
438 View Code Duplication
            if (strrpos($expr, "'") === strlen($expr) - 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
439
                $expr = substr($expr, 0, -1);
440
            }
441
            $expr = stripslashes($expr);
442
443
            $instance->setValue($expr);
444
        }
445
        return $instance;
446
    }
447
448
    private static function buildFromSubtree($subTree)
449
    {
450
        if ($subTree && is_array($subTree)) {
451
            //if (isset($subTree['SELECT'])) {
452
            // If the subtree is a map instead of a list, we are likely to be on a SUBSELECT statement.
453
            if (!empty($subTree) && !isset($subTree[0])) {
454
                $subTree = StatementFactory::toObject($subTree);
455
            } else {
456
                $subTree = self::mapArrayToNodeObjectList($subTree);
457
            }
458
        }
459
460
        return $subTree;
461
    }
462
463
    /**
464
     * @param array $items An array of objects represented as SQLParser arrays.
465
     */
466
    public static function mapArrayToNodeObjectList(array $items)
467
    {
468
        $list = [];
469
470
        $nextAndPartOfBetween = false;
471
472
        // Special case, let's replace the AND of a between with a ANDBETWEEN object.
473
        foreach ($items as $item) {
474
            $obj = NodeFactory::toObject($item);
475
            if ($obj instanceof Operator) {
476
                if ($obj->getValue() == 'BETWEEN') {
477
                    $nextAndPartOfBetween = true;
478
                } elseif ($nextAndPartOfBetween && $obj->getValue() == 'AND') {
479
                    $nextAndPartOfBetween = false;
480
                    $obj->setValue('AND_FROM_BETWEEN');
481
                }
482
            }
483
            $list[] = $obj;
484
        }
485
486
        return $list;
487
    }
488
489
    private static $PRECEDENCE = array(
490
            array('INTERVAL'),
491
            array('BINARY', 'COLLATE'),
492
            array('!'),
493
            array(/*'-'*/ /* (unary minus) ,*/ '~' /*(unary bit inversion)*/),
494
            array('^'),
495
            array('*', '/', 'DIV', '%', 'MOD'),
496
            array('-', '+'),
497
            array('<<', '>>'),
498
            array('&'),
499
            array('|'),
500
            array('=' /*(comparison)*/, '<=>', '>=', '>', '<=', '<', '<>', '!=', 'IS', 'LIKE', 'REGEXP', 'IN', 'IS NOT', 'NOT IN'),
501
            array('AND_FROM_BETWEEN'),
502
            array('THEN'),
503
            array('WHEN'),
504
            array('ELSE'),
505
            array('BETWEEN', 'CASE', 'END'),
506
            array('NOT'),
507
            array('&&', 'AND'),
508
            array('XOR'),
509
            array('||', 'OR'), );
510
511
    private static $OPERATOR_TO_CLASS = array(
512
            '=' => 'SQLParser\Node\Equal',
513
            '<' => 'SQLParser\Node\Less',
514
            '>' => 'SQLParser\Node\Greater',
515
            '<=' => 'SQLParser\Node\LessOrEqual',
516
            '>=' => 'SQLParser\Node\GreaterOrEqual',
517
            //'<=>' => '????',
518
            '<>' => 'SQLParser\Node\Different',
519
            '!=' => 'SQLParser\Node\Different',
520
            'IS' => 'SQLParser\Node\Is',
521
            'IS NOT' => 'SQLParser\Node\IsNot',
522
            'LIKE' => 'SQLParser\Node\Like',
523
            'REGEXP' => 'SQLParser\Node\Regexp',
524
            'IN' => 'SQLParser\Node\In',
525
            'NOT IN' => 'SQLParser\Node\NotIn',
526
            '+' => 'SQLParser\Node\Plus',
527
            '-' => 'SQLParser\Node\Minus',
528
            '*' => 'SQLParser\Node\Multiply',
529
            '/' => 'SQLParser\Node\Divide',
530
            '%' => 'SQLParser\Node\Modulo',
531
            'MOD' => 'SQLParser\Node\Modulo',
532
            'DIV' => 'SQLParser\Node\Div',
533
            '&' => 'SQLParser\Node\BitwiseAnd',
534
            '|' => 'SQLParser\Node\BitwiseOr',
535
            '^' => 'SQLParser\Node\BitwiseXor',
536
            '<<' => 'SQLParser\Node\ShiftLeft',
537
            '>>' => 'SQLParser\Node\ShiftRight',
538
            '<=>' => 'SQLParser\Node\NullCompatibleEqual',
539
            'AND' => 'SQLParser\Node\AndOp',
540
            '&&' => 'SQLParser\Node\AndOp',
541
            '||' => 'SQLParser\Node\OrOp',
542
            'OR' => 'SQLParser\Node\OrOp',
543
            'XOR' => 'SQLParser\Node\XorOp',
544
            'THEN' => 'SQLParser\Node\Then',
545
            'ELSE' => 'SQLParser\Node\ElseOperation',
546
    );
547
548
    /**
549
     * Takes an array of nodes (including operators) and try to build a tree from it.
550
     *
551
     * @param NodeInterface[]|NodeInterface $nodes
552
     */
553
    public static function simplify($nodes)
554
    {
555
        if (empty($nodes)) {
556
            $nodes = array();
557
        } elseif (!is_array($nodes)) {
558
            $nodes = array($nodes);
559
        }
560
        $minPriority = -1;
561
        $selectedOperators = array();
562
        $lastSelectedOperator = '';
563
        $differentOperatorWithSamePriority = false;
564
565
        // Let's transform "NOT" + "IN" into "NOT IN"
566
        $newNodes = array();
567
        for ($i = 0; $i < count($nodes); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
568
            $node = $nodes[$i];
569
            if ($node instanceof Operator && isset($nodes[$i + 1]) && $nodes[$i + 1] instanceof Operator
570
                    && strtoupper($node->getValue()) == 'IS' && strtoupper($nodes[$i + 1]->getValue()) == 'NOT') {
571
                $notIn = new Operator();
572
                $notIn->setValue('IS NOT');
573
                $newNodes[] = $notIn;
574
                ++$i;
575
            } elseif ($node instanceof Operator && isset($nodes[$i + 1]) && $nodes[$i + 1] instanceof Operator
576
                    && strtoupper($node->getValue()) == 'NOT' && strtoupper($nodes[$i + 1]->getValue()) == 'IN') {
577
                $notIn = new Operator();
578
                $notIn->setValue('NOT IN');
579
                $newNodes[] = $notIn;
580
                ++$i;
581
            } else {
582
                $newNodes[] = $node;
583
            }
584
        }
585
        $nodes = $newNodes;
586
587
        // Handle AGAINST function. Without this patch params will be placed after AGAINST() and not inside the brackets
588
        $newNodes = array();
589
        for ($i = 0; $i < count($nodes); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
590
            $node = $nodes[$i];
591
            if ($node instanceof SimpleFunction && $node->getBaseExpression() === 'AGAINST' && isset($nodes[$i + 1])) {
592
                $node->setSubTree($nodes[$i + 1]);
593
                $i++;
594
            }
595
            $newNodes[] = $node;
596
        }
597
        $nodes = $newNodes;
598
599
        // Let's find the highest level operator.
600
        for ($i = count($nodes) - 1; $i >= 0; --$i) {
601
            $node = $nodes[$i];
602
            if ($node instanceof Operator) {
603
                $priority = self::getOperatorPrecedence($node);
604
605
                if ($priority == $minPriority && $lastSelectedOperator != strtoupper($node->getValue())) {
606
                    $differentOperatorWithSamePriority = true;
607
                } elseif ($priority > $minPriority) {
608
                    $minPriority = $priority;
609
                    $selectedOperators = array($node);
610
                    $lastSelectedOperator = strtoupper($node->getValue());
611
                } else {
612
                    if (strtoupper($node->getValue()) == $lastSelectedOperator && !$differentOperatorWithSamePriority) {
613
                        $selectedOperators[] = $node;
614
                    }
615
                }
616
            }
617
        }
618
        $selectedOperators = array_reverse($selectedOperators);
619
620
        // At this point, the $selectedOperator list contains a list of operators of the same kind that will apply
621
        // at the same time.
622
        if (empty($selectedOperators)) {
623
            // If we have an Expression with no base expression, let's simply discard it.
624
            // Indeed, the tree will add brackets by itself, and no Expression is needed for that.
625
            $newNodes = array();
626
            /*foreach ($nodes as $key=>$operand) {
627
                if ($operand instanceof Expression) {
628
                    $subTree = $operand->getSubTree();
629
                    if (count($subTree) == 1) {
630
                        $nodes[$key] = self::simplify($subTree);
631
                    }
632
                }
633
            }*/
634
            foreach ($nodes as $operand) {
635
                if ($operand instanceof Expression) {
636
                    if (empty($operand->getBaseExpression())) {
637
                        $subTree = $operand->getSubTree();
638
                        if (count($subTree) == 1) {
639
                            $newNodes = array_merge($newNodes, self::simplify($subTree));
0 ignored issues
show
Bug introduced by
It seems like $subTree defined by $operand->getSubTree() on line 637 can also be of type array; however, SQLParser\Node\NodeFactory::simplify() does only seem to accept array<integer,object<SQL...ser\Node\NodeInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
640
                        } else {
641
                            $newNodes[] = $operand;
642
                        }
643
                    } else {
644
                        $newNodes[] = $operand;
645
                    }
646
                } else {
647
                    $newNodes[] = $operand;
648
                }
649
            }
650
651
            return $newNodes;
652
        }
653
654
        // Let's grab the operands of the operator.
655
        $operands = array();
656
        $operand = array();
657
        $tmpOperators = $selectedOperators;
658
        $nextOperator = array_shift($tmpOperators);
659
660
        $isSelectedOperatorFirst = null;
661
662
        foreach ($nodes as $node) {
663
            if ($node === $nextOperator) {
664
                if ($isSelectedOperatorFirst === null) {
665
                    $isSelectedOperatorFirst = true;
666
                }
667
                // Let's apply the "simplify" method on the operand before storing it.
668
                //$operands[] = self::simplify($operand);
669
                $simple = self::simplify($operand);
670
                if (is_array($simple)) {
671
                    $operands = array_merge($operands, $simple);
672
                } else {
673
                    $operands[] = $simple;
674
                }
675
676
                $operand = array();
677
                $nextOperator = array_shift($tmpOperators);
678
            } else {
679
                if ($isSelectedOperatorFirst === null) {
680
                    $isSelectedOperatorFirst = false;
681
                }
682
                $operand[] = $node;
683
            }
684
        }
685
        //$operands[] = self::simplify($operand);
686
        //$operands = array_merge($operands, self::simplify($operand));
687
        $simple = self::simplify($operand);
688
        if (is_array($simple)) {
689
            $operands = array_merge($operands, $simple);
690
        } else {
691
            $operands[] = $simple;
692
        }
693
694
        // Now, if we have an Expression, let's simply discard it.
695
        // Indeed, the tree will add brackets by itself, and no Expression in needed for that.
696
        /*foreach ($operands as $key=>$operand) {
697
            if ($operand instanceof Expression) {
698
                $subTree = $operand->getSubTree();
699
                if (count($subTree) == 1) {
700
                    $operands[$key] = self::simplify($subTree);
701
                }
702
            }
703
        }*/
704
705
        $operation = strtoupper($selectedOperators[0]->getValue());
706
707
        /* TODO:
708
        Remaining operators to code:
709
        array('INTERVAL'),
710
        array('BINARY', 'COLLATE'),
711
        array('!'),
712
        array('NOT'),
713
        */
714
715
        if (isset(self::$OPERATOR_TO_CLASS[$operation]) && is_subclass_of(self::$OPERATOR_TO_CLASS[$operation], 'SQLParser\Node\AbstractTwoOperandsOperator')) {
716
            if (count($operands) != 2) {
717
                throw new MagicQueryException('An error occured while parsing SQL statement. Invalid character found next to "'.$operation.'"');
718
            }
719
720
            $leftOperand = array_shift($operands);
721
            $rightOperand = array_shift($operands);
722
723
            $instance = new self::$OPERATOR_TO_CLASS[$operation]();
724
            $instance->setLeftOperand($leftOperand);
725
            $instance->setRightOperand($rightOperand);
726
727
            return $instance;
728
        } elseif (isset(self::$OPERATOR_TO_CLASS[$operation]) && is_subclass_of(self::$OPERATOR_TO_CLASS[$operation], 'SQLParser\Node\AbstractManyInstancesOperator')) {
729
            $instance = new self::$OPERATOR_TO_CLASS[$operation]();
730
            $instance->setOperands($operands);
731
732
            return $instance;
733
        } elseif ($operation === 'BETWEEN') {
734
            $leftOperand = array_shift($operands);
735
            $rightOperand = array_shift($operands);
736
            if (!$rightOperand instanceof Operation || $rightOperand->getOperatorSymbol() !== 'AND_FROM_BETWEEN') {
737
                throw new MagicQueryException('Missing AND in BETWEEN filter.');
738
            }
739
740
            $innerOperands = $rightOperand->getOperands();
741
            $minOperand = array_shift($innerOperands);
742
            $maxOperand = array_shift($innerOperands);
743
744
            $instance = new Between();
745
            $instance->setLeftOperand($leftOperand);
746
            $instance->setMinValueOperand($minOperand);
747
            $instance->setMaxValueOperand($maxOperand);
748
749
            return $instance;
750
        } elseif ($operation === 'WHEN') {
751
            $instance = new WhenConditions();
752
753
            if (!$isSelectedOperatorFirst) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isSelectedOperatorFirst of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
754
                $value = array_shift($operands);
755
                $instance->setValue($value);
756
            }
757
            $instance->setOperands($operands);
758
759
            return $instance;
760
        } elseif ($operation === 'CASE') {
761
            $innerOperation = array_shift($operands);
762
763
            if (!empty($operands)) {
764
                throw new MagicQueryException('A CASE statement should contain only a ThenConditions or a ElseOperand object.');
765
            }
766
767
            $instance = new CaseOperation();
768
            $instance->setOperation($innerOperation);
769
770
            return $instance;
771
        } elseif ($operation === 'END') {
772
            // Simply bypass the END operation. We already have a CASE matching node:
773
            $caseOperation = array_shift($operands);
774
775
            return $caseOperation;
776
        } else {
777
            $instance = new Operation();
778
            $instance->setOperatorSymbol($operation);
779
            $instance->setOperands($operands);
780
781
            return $instance;
782
        }
783
    }
784
785
    /**
786
     * Finds the precedence for operator $node (highest number has the least precedence).
787
     *
788
     * @param Operator $node
789
     *
790
     * @throws MagicQueryException
791
     *
792
     * @return int
793
     */
794
    private static function getOperatorPrecedence(Operator $node): int
795
    {
796
        $value = strtoupper($node->getValue());
797
798
        foreach (self::$PRECEDENCE as $priority => $arr) {
799
            foreach ($arr as $op) {
800
                if ($value == $op) {
801
                    return $priority;
802
                }
803
            }
804
        }
805
        throw new MagicQueryException('Unknown operator precedence for operator '.$value);
806
    }
807
808
    /**
809
     * @param mixed       $node        a node of a recursive array of node
810
     * @param MoufManager $moufManager
811
     *
812
     * @return MoufInstanceDescriptor
813
     */
814
    public static function nodeToInstanceDescriptor($node, MoufManager $moufManager)
815
    {
816
        return self::array_map_deep($node, function ($item) use ($moufManager) {
817
            if ($item instanceof NodeInterface) {
818
                return $item->toInstanceDescriptor($moufManager);
819
            } else {
820
                return $item;
821
            }
822
        });
823
    }
824
825
    private static function array_map_deep($array, $callback)
826
    {
827
        $new = array();
828
        if (is_array($array)) {
829
            foreach ($array as $key => $val) {
830
                if (is_array($val)) {
831
                    $new[$key] = self::array_map_deep($val, $callback);
832
                } else {
833
                    $new[$key] = call_user_func($callback, $val);
834
                }
835
            }
836
        } else {
837
            $new = call_user_func($callback, $array);
838
        }
839
840
        return $new;
841
    }
842
843
    /**
844
     * Tansforms the array of nodes (or the node) passed in parameter into a SQL string.
845
     *
846
     * @param mixed       $nodes          Recursive array of node interface
847
     * @param AbstractPlatform  $platform
848
     * @param array       $parameters
849
     * @param string      $delimiter
850
     * @param bool|string $wrapInBrackets
851
     * @param int|number  $indent
852
     * @param int         $conditionsMode
853
     *
854
     * @return null|string
855
     */
856
    public static function toSql($nodes, AbstractPlatform $platform, array $parameters = array(), $delimiter = ',', $wrapInBrackets = true, $indent = 0, $conditionsMode = SqlRenderInterface::CONDITION_APPLY, bool $extrapolateParameters = true)
857
    {
858
        if (is_array($nodes)) {
859
            $elems = array();
860
            array_walk_recursive($nodes, function ($item) use (&$elems, $platform, $indent, $parameters, $conditionsMode, $extrapolateParameters) {
861
                if ($item instanceof SqlRenderInterface) {
862
                    $itemSql = $item->toSql($parameters, $platform, $indent, $conditionsMode, $extrapolateParameters);
863
                    if ($itemSql !== null) {
864
                        $elems[] = str_repeat(' ', $indent).$itemSql;
865
                    }
866
                } else {
867
                    if ($item !== null) {
868
                        $elems[] = str_repeat(' ', $indent).$item;
869
                    }
870
                }
871
            });
872
            $sql = implode($delimiter, $elems);
873
        } else {
874
            $item = $nodes;
875
            if ($item instanceof SqlRenderInterface) {
876
                $itemSql = $item->toSql($parameters, $platform, $indent, $conditionsMode, $extrapolateParameters);
877
                if ($itemSql === null || $itemSql === '') {
878
                    return null;
879
                }
880
                $sql = str_repeat(' ', $indent).$itemSql;
881
            } else {
882
                if ($item === null || $item === '') {
883
                    return null;
884
                }
885
                $sql = str_repeat(' ', $indent).$item;
886
            }
887
        }
888
        if ($wrapInBrackets) {
889
            $sql = '('.$sql.')';
890
        }
891
892
        return $sql;
893
    }
894
}
895