Completed
Pull Request — 1.3 (#47)
by
unknown
08:10 queued 03:49
created

NodeFactory::mapArrayToNodeObjectList()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.9457
c 0
b 0
f 0
cc 6
nc 5
nop 1
1
<?php
2
namespace SQLParser\Node;
3
4
use Mouf\Database\MagicQueryException;
5
use Mouf\Database\MagicQueryParserException;
6
use SQLParser\SqlRenderInterface;
7
use Doctrine\DBAL\Connection;
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
                if (!empty($desc)) {
393
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
394
                }
395
396
                return $expr;
397
            case ExpressionType::MATCH_MODE:
398
                $expr = new ConstNode();
399
                $expr->setValue($desc['base_expr']);
400
                $expr->setIsString(false);
401
402
                // Debug:
403
                unset($desc['base_expr']);
404
                unset($desc['expr_type']);
405
                unset($desc['sub_tree']);
406
                unset($desc['alias']);
407
                unset($desc['direction']);
408
                unset($desc['delim']);
409
                if (!empty($desc)) {
410
                    error_log('MagicQuery - NodeFactory: Unexpected parameters in exception: '.var_export($desc, true));
411
                }
412
413
                return $expr;
414
            default:
415
                throw new \Exception('Unknown expression type');
416
        }
417
    }
418
419
420
    /**
421
     * Transforms a limit or offset value/parameter into a node.
422
     *
423
     * @param string $value
424
     * @return NodeInterface
425
     */
426
    public static function toLimitNode($value)
427
    {
428
        if (substr($value, 0, 1) === ':') {
429
            $instance = new UnquotedParameter();
430
            $instance->setName(substr($value, 1));
431
        } else {
432
            $instance = new LimitNode();
433
            $expr = $value;
434
            if (strpos($expr, "'") === 0) {
435
                $expr = substr($expr, 1);
436
            }
437 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...
438
                $expr = substr($expr, 0, -1);
439
            }
440
            $expr = stripslashes($expr);
441
442
            $instance->setValue($expr);
443
        }
444
        return $instance;
445
    }
446
447
    private static function buildFromSubtree($subTree)
448
    {
449
        if ($subTree && is_array($subTree)) {
450
            //if (isset($subTree['SELECT'])) {
451
            // If the subtree is a map instead of a list, we are likely to be on a SUBSELECT statement.
452
            if (!empty($subTree) && !isset($subTree[0])) {
453
                $subTree = StatementFactory::toObject($subTree);
454
            } else {
455
                $subTree = self::mapArrayToNodeObjectList($subTree);
456
            }
457
        }
458
459
        return $subTree;
460
    }
461
462
    /**
463
     * @param array $items An array of objects represented as SQLParser arrays.
464
     */
465
    public static function mapArrayToNodeObjectList(array $items)
466
    {
467
        $list = [];
468
469
        $nextAndPartOfBetween = false;
470
471
        // Special case, let's replace the AND of a between with a ANDBETWEEN object.
472
        foreach ($items as $item) {
473
            $obj = NodeFactory::toObject($item);
474
            if ($obj instanceof Operator) {
475
                if ($obj->getValue() == 'BETWEEN') {
476
                    $nextAndPartOfBetween = true;
477
                } elseif ($nextAndPartOfBetween && $obj->getValue() == 'AND') {
478
                    $nextAndPartOfBetween = false;
479
                    $obj->setValue('AND_FROM_BETWEEN');
480
                }
481
            }
482
            $list[] = $obj;
483
        }
484
485
        return $list;
486
    }
487
488
    private static $PRECEDENCE = array(
489
            array('INTERVAL'),
490
            array('BINARY', 'COLLATE'),
491
            array('!'),
492
            array(/*'-'*/ /* (unary minus) ,*/ '~' /*(unary bit inversion)*/),
493
            array('^'),
494
            array('*', '/', 'DIV', '%', 'MOD'),
495
            array('-', '+'),
496
            array('<<', '>>'),
497
            array('&'),
498
            array('|'),
499
            array('=' /*(comparison)*/, '<=>', '>=', '>', '<=', '<', '<>', '!=', 'IS', 'LIKE', 'REGEXP', 'IN', 'IS NOT', 'NOT IN'),
500
            array('AND_FROM_BETWEEN'),
501
            array('THEN'),
502
            array('WHEN'),
503
            array('ELSE'),
504
            array('BETWEEN', 'CASE', 'END'),
505
            array('NOT'),
506
            array('&&', 'AND'),
507
            array('XOR'),
508
            array('||', 'OR'), );
509
510
    private static $OPERATOR_TO_CLASS = array(
511
            '=' => 'SQLParser\Node\Equal',
512
            '<' => 'SQLParser\Node\Less',
513
            '>' => 'SQLParser\Node\Greater',
514
            '<=' => 'SQLParser\Node\LessOrEqual',
515
            '>=' => 'SQLParser\Node\GreaterOrEqual',
516
            //'<=>' => '????',
517
            '<>' => 'SQLParser\Node\Different',
518
            '!=' => 'SQLParser\Node\Different',
519
            'IS' => 'SQLParser\Node\Is',
520
            'IS NOT' => 'SQLParser\Node\IsNot',
521
            'LIKE' => 'SQLParser\Node\Like',
522
            'REGEXP' => 'SQLParser\Node\Regexp',
523
            'IN' => 'SQLParser\Node\In',
524
            'NOT IN' => 'SQLParser\Node\NotIn',
525
            '+' => 'SQLParser\Node\Plus',
526
            '-' => 'SQLParser\Node\Minus',
527
            '*' => 'SQLParser\Node\Multiply',
528
            '/' => 'SQLParser\Node\Divide',
529
            '%' => 'SQLParser\Node\Modulo',
530
            'MOD' => 'SQLParser\Node\Modulo',
531
            'DIV' => 'SQLParser\Node\Div',
532
            '&' => 'SQLParser\Node\BitwiseAnd',
533
            '|' => 'SQLParser\Node\BitwiseOr',
534
            '^' => 'SQLParser\Node\BitwiseXor',
535
            '<<' => 'SQLParser\Node\ShiftLeft',
536
            '>>' => 'SQLParser\Node\ShiftRight',
537
            '<=>' => 'SQLParser\Node\NullCompatibleEqual',
538
            'AND' => 'SQLParser\Node\AndOp',
539
            '&&' => 'SQLParser\Node\AndOp',
540
            '||' => 'SQLParser\Node\OrOp',
541
            'OR' => 'SQLParser\Node\OrOp',
542
            'XOR' => 'SQLParser\Node\XorOp',
543
            'THEN' => 'SQLParser\Node\Then',
544
            'ELSE' => 'SQLParser\Node\ElseOperation',
545
    );
546
547
    /**
548
     * Takes an array of nodes (including operators) and try to build a tree from it.
549
     *
550
     * @param NodeInterface[]|NodeInterface $nodes
551
     */
552
    public static function simplify($nodes)
553
    {
554
        if (empty($nodes)) {
555
            $nodes = array();
556
        } elseif (!is_array($nodes)) {
557
            $nodes = array($nodes);
558
        }
559
        $minPriority = -1;
560
        $selectedOperators = array();
561
        $lastSelectedOperator = '';
562
        $differentOperatorWithSamePriority = false;
563
564
        // Let's transform "NOT" + "IN" into "NOT IN"
565
        $newNodes = array();
566
        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...
567
            $node = $nodes[$i];
568
            if ($node instanceof Operator && isset($nodes[$i + 1]) && $nodes[$i + 1] instanceof Operator
569
                    && strtoupper($node->getValue()) == 'IS' && strtoupper($nodes[$i + 1]->getValue()) == 'NOT') {
570
                $notIn = new Operator();
571
                $notIn->setValue('IS NOT');
572
                $newNodes[] = $notIn;
573
                ++$i;
574
            } elseif ($node instanceof Operator && isset($nodes[$i + 1]) && $nodes[$i + 1] instanceof Operator
575
                    && strtoupper($node->getValue()) == 'NOT' && strtoupper($nodes[$i + 1]->getValue()) == 'IN') {
576
                $notIn = new Operator();
577
                $notIn->setValue('NOT IN');
578
                $newNodes[] = $notIn;
579
                ++$i;
580
            } else {
581
                $newNodes[] = $node;
582
            }
583
        }
584
        $nodes = $newNodes;
585
586
        // Handle AGAINST function. Without this patch params will be placed after AGAINST() and not inside the brackets
587
        $newNodes = array();
588
        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...
589
            $node = $nodes[$i];
590
            if ($node instanceof SimpleFunction && $node->getBaseExpression() === 'AGAINST' && isset($nodes[$i + 1])) {
591
                $node->setSubTree($nodes[$i + 1]);
592
                $i++;
593
            }
594
            $newNodes[] = $node;
595
        }
596
        $nodes = $newNodes;
597
598
        // Let's find the highest level operator.
599
        for ($i = count($nodes) - 1; $i >= 0; --$i) {
600
            $node = $nodes[$i];
601
            if ($node instanceof Operator) {
602
                $priority = self::getOperatorPrecedence($node);
603
604
                if ($priority == $minPriority && $lastSelectedOperator != strtoupper($node->getValue())) {
605
                    $differentOperatorWithSamePriority = true;
606
                } elseif ($priority > $minPriority) {
607
                    $minPriority = $priority;
608
                    $selectedOperators = array($node);
609
                    $lastSelectedOperator = strtoupper($node->getValue());
610
                } else {
611
                    if (strtoupper($node->getValue()) == $lastSelectedOperator && !$differentOperatorWithSamePriority) {
612
                        $selectedOperators[] = $node;
613
                    }
614
                }
615
            }
616
        }
617
        $selectedOperators = array_reverse($selectedOperators);
618
619
        // At this point, the $selectedOperator list contains a list of operators of the same kind that will apply
620
        // at the same time.
621
        if (empty($selectedOperators)) {
622
            // If we have an Expression with no base expression, let's simply discard it.
623
            // Indeed, the tree will add brackets by itself, and no Expression is needed for that.
624
            $newNodes = array();
625
            /*foreach ($nodes as $key=>$operand) {
626
                if ($operand instanceof Expression) {
627
                    $subTree = $operand->getSubTree();
628
                    if (count($subTree) == 1) {
629
                        $nodes[$key] = self::simplify($subTree);
630
                    }
631
                }
632
            }*/
633
            foreach ($nodes as $operand) {
634
                if ($operand instanceof Expression) {
635
                    if (empty($operand->getBaseExpression())) {
636
                        $subTree = $operand->getSubTree();
637
                        if (count($subTree) == 1) {
638
                            $newNodes = array_merge($newNodes, self::simplify($subTree));
0 ignored issues
show
Bug introduced by
It seems like $subTree defined by $operand->getSubTree() on line 636 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...
639
                        } else {
640
                            $newNodes[] = $operand;
641
                        }
642
                    } else {
643
                        $newNodes[] = $operand;
644
                    }
645
                } else {
646
                    $newNodes[] = $operand;
647
                }
648
            }
649
650
            return $newNodes;
651
        }
652
653
        // Let's grab the operands of the operator.
654
        $operands = array();
655
        $operand = array();
656
        $tmpOperators = $selectedOperators;
657
        $nextOperator = array_shift($tmpOperators);
658
659
        $isSelectedOperatorFirst = null;
660
661
        foreach ($nodes as $node) {
662
            if ($node === $nextOperator) {
663
                if ($isSelectedOperatorFirst === null) {
664
                    $isSelectedOperatorFirst = true;
665
                }
666
                // Let's apply the "simplify" method on the operand before storing it.
667
                //$operands[] = self::simplify($operand);
668
                $simple = self::simplify($operand);
669
                if (is_array($simple)) {
670
                    $operands = array_merge($operands, $simple);
671
                } else {
672
                    $operands[] = $simple;
673
                }
674
675
                $operand = array();
676
                $nextOperator = array_shift($tmpOperators);
677
            } else {
678
                if ($isSelectedOperatorFirst === null) {
679
                    $isSelectedOperatorFirst = false;
680
                }
681
                $operand[] = $node;
682
            }
683
        }
684
        //$operands[] = self::simplify($operand);
685
        //$operands = array_merge($operands, self::simplify($operand));
686
        $simple = self::simplify($operand);
687
        if (is_array($simple)) {
688
            $operands = array_merge($operands, $simple);
689
        } else {
690
            $operands[] = $simple;
691
        }
692
693
        // Now, if we have an Expression, let's simply discard it.
694
        // Indeed, the tree will add brackets by itself, and no Expression in needed for that.
695
        /*foreach ($operands as $key=>$operand) {
696
            if ($operand instanceof Expression) {
697
                $subTree = $operand->getSubTree();
698
                if (count($subTree) == 1) {
699
                    $operands[$key] = self::simplify($subTree);
700
                }
701
            }
702
        }*/
703
704
        $operation = strtoupper($selectedOperators[0]->getValue());
705
706
        /* TODO:
707
        Remaining operators to code:
708
        array('INTERVAL'),
709
        array('BINARY', 'COLLATE'),
710
        array('!'),
711
        array('NOT'),
712
        */
713
714
        if (isset(self::$OPERATOR_TO_CLASS[$operation]) && is_subclass_of(self::$OPERATOR_TO_CLASS[$operation], 'SQLParser\Node\AbstractTwoOperandsOperator')) {
715
            if (count($operands) != 2) {
716
                throw new MagicQueryException('An error occured while parsing SQL statement. Invalid character found next to "'.$operation.'"');
717
            }
718
719
            $leftOperand = array_shift($operands);
720
            $rightOperand = array_shift($operands);
721
722
            $instance = new self::$OPERATOR_TO_CLASS[$operation]();
723
            $instance->setLeftOperand($leftOperand);
724
            $instance->setRightOperand($rightOperand);
725
726
            return $instance;
727
        } elseif (isset(self::$OPERATOR_TO_CLASS[$operation]) && is_subclass_of(self::$OPERATOR_TO_CLASS[$operation], 'SQLParser\Node\AbstractManyInstancesOperator')) {
728
            $instance = new self::$OPERATOR_TO_CLASS[$operation]();
729
            $instance->setOperands($operands);
730
731
            return $instance;
732
        } elseif ($operation === 'BETWEEN') {
733
            $leftOperand = array_shift($operands);
734
            $rightOperand = array_shift($operands);
735
            if (!$rightOperand instanceof Operation || $rightOperand->getOperatorSymbol() !== 'AND_FROM_BETWEEN') {
736
                throw new MagicQueryException('Missing AND in BETWEEN filter.');
737
            }
738
739
            $innerOperands = $rightOperand->getOperands();
740
            $minOperand = array_shift($innerOperands);
741
            $maxOperand = array_shift($innerOperands);
742
743
            $instance = new Between();
744
            $instance->setLeftOperand($leftOperand);
745
            $instance->setMinValueOperand($minOperand);
746
            $instance->setMaxValueOperand($maxOperand);
747
748
            return $instance;
749
        } elseif ($operation === 'WHEN') {
750
            $instance = new WhenConditions();
751
752
            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...
753
                $value = array_shift($operands);
754
                $instance->setValue($value);
755
            }
756
            $instance->setOperands($operands);
757
758
            return $instance;
759
        } elseif ($operation === 'CASE') {
760
            $innerOperation = array_shift($operands);
761
762
            if (!empty($operands)) {
763
                throw new MagicQueryException('A CASE statement should contain only a ThenConditions or a ElseOperand object.');
764
            }
765
766
            $instance = new CaseOperation();
767
            $instance->setOperation($innerOperation);
768
769
            return $instance;
770
        } elseif ($operation === 'END') {
771
            // Simply bypass the END operation. We already have a CASE matching node:
772
            $caseOperation = array_shift($operands);
773
774
            return $caseOperation;
775
        } else {
776
            $instance = new Operation();
777
            $instance->setOperatorSymbol($operation);
778
            $instance->setOperands($operands);
779
780
            return $instance;
781
        }
782
    }
783
784
    /**
785
     * Finds the precedence for operator $node (highest number has the least precedence).
786
     *
787
     * @param Operator $node
788
     *
789
     * @throws MagicQueryException
790
     *
791
     * @return int
792
     */
793
    private static function getOperatorPrecedence(Operator $node): int
794
    {
795
        $value = strtoupper($node->getValue());
796
797
        foreach (self::$PRECEDENCE as $priority => $arr) {
798
            foreach ($arr as $op) {
799
                if ($value == $op) {
800
                    return $priority;
801
                }
802
            }
803
        }
804
        throw new MagicQueryException('Unknown operator precedence for operator '.$value);
805
    }
806
807
    /**
808
     * @param mixed       $node        a node of a recursive array of node
809
     * @param MoufManager $moufManager
810
     *
811
     * @return MoufInstanceDescriptor
812
     */
813
    public static function nodeToInstanceDescriptor($node, MoufManager $moufManager)
814
    {
815
        return self::array_map_deep($node, function ($item) use ($moufManager) {
816
            if ($item instanceof NodeInterface) {
817
                return $item->toInstanceDescriptor($moufManager);
818
            } else {
819
                return $item;
820
            }
821
        });
822
    }
823
824
    private static function array_map_deep($array, $callback)
825
    {
826
        $new = array();
827
        if (is_array($array)) {
828
            foreach ($array as $key => $val) {
829
                if (is_array($val)) {
830
                    $new[$key] = self::array_map_deep($val, $callback);
831
                } else {
832
                    $new[$key] = call_user_func($callback, $val);
833
                }
834
            }
835
        } else {
836
            $new = call_user_func($callback, $array);
837
        }
838
839
        return $new;
840
    }
841
842
    /**
843
     * Tansforms the array of nodes (or the node) passed in parameter into a SQL string.
844
     *
845
     * @param mixed       $nodes          Recursive array of node interface
846
     * @param Connection  $dbConnection
847
     * @param array       $parameters
848
     * @param string      $delimiter
849
     * @param bool|string $wrapInBrackets
850
     * @param int|number  $indent
851
     * @param int         $conditionsMode
852
     *
853
     * @return null|string
854
     */
855
    public static function toSql($nodes, Connection $dbConnection = null, array $parameters = array(), $delimiter = ',', $wrapInBrackets = true, $indent = 0, $conditionsMode = SqlRenderInterface::CONDITION_APPLY, bool $extrapolateParameters = true)
856
    {
857
        if (is_array($nodes)) {
858
            $elems = array();
859
            array_walk_recursive($nodes, function ($item) use (&$elems, $dbConnection, $indent, $parameters, $conditionsMode, $extrapolateParameters) {
860
                if ($item instanceof SqlRenderInterface) {
861
                    $itemSql = $item->toSql($parameters, $dbConnection, $indent, $conditionsMode, $extrapolateParameters);
862
                    if ($itemSql !== null) {
863
                        $elems[] = str_repeat(' ', $indent).$itemSql;
864
                    }
865
                } else {
866
                    if ($item !== null) {
867
                        $elems[] = str_repeat(' ', $indent).$item;
868
                    }
869
                }
870
            });
871
            $sql = implode($delimiter, $elems);
872
        } else {
873
            $item = $nodes;
874
            if ($item instanceof SqlRenderInterface) {
875
                $itemSql = $item->toSql($parameters, $dbConnection, $indent, $conditionsMode, $extrapolateParameters);
876
                if ($itemSql === null || $itemSql === '') {
877
                    return null;
878
                }
879
                $sql = str_repeat(' ', $indent).$itemSql;
880
            } else {
881
                if ($item === null || $item === '') {
882
                    return null;
883
                }
884
                $sql = str_repeat(' ', $indent).$item;
885
            }
886
        }
887
        if ($wrapInBrackets) {
888
            $sql = '('.$sql.')';
889
        }
890
891
        return $sql;
892
    }
893
894
    /**
895
     * Escapes a DB item (should not be used. Only used if no DBConnection is passed).
896
     *
897
     * @return string
898
     *
899
     * @param string $str
900
     */
901
    public static function escapeDBItem($str, Connection $dbConnection = null)
902
    {
903
        if ($dbConnection) {
904
            return $dbConnection->quoteIdentifier($str);
905
        } else {
906
            return '`'.$str.'`';
907
        }
908
    }
909
}
910