Completed
Pull Request — 1.3 (#47)
by
unknown
04:46
created

NodeFactory::toObject()   F

Complexity

Conditions 80
Paths 1687

Size

Total Lines 397

Duplication

Lines 15
Ratio 3.78 %

Importance

Changes 0
Metric Value
dl 15
loc 397
rs 0
c 0
b 0
f 0
cc 80
nc 1687
nop 1

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