Passed
Push — 4 ( c63eec...17f4d5 )
by Ingo
06:47
created

SQLConditionalExpression::getJoins()   C

Complexity

Conditions 12
Paths 56

Size

Total Lines 55
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 31
nc 56
nop 1
dl 0
loc 55
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\ORM\Queries;
4
5
use SilverStripe\Dev\Deprecation;
6
7
/**
8
 * Represents a SQL query for an expression which interacts with existing rows
9
 * (SELECT / DELETE / UPDATE) with a WHERE clause
10
 */
11
abstract class SQLConditionalExpression extends SQLExpression
12
{
13
14
    /**
15
     * An array of WHERE clauses.
16
     *
17
     * Each item in this array will be in the form of a single-length array
18
     * in the format array('predicate' => array($parameters))
19
     *
20
     * @var array
21
     */
22
    protected $where = array();
23
24
    /**
25
     * The logical connective used to join WHERE clauses. Defaults to AND.
26
     *
27
     * @var string
28
     */
29
    protected $connective = 'AND';
30
31
    /**
32
     * An array of tables. The first one is just the table name.
33
     * Used as the FROM in DELETE/SELECT statements, the INTO in INSERT statements,
34
     * and the target table in UPDATE statements
35
     *
36
     * The keys of this array are the aliases of the tables (unquoted), where the
37
     * values are either the literal table names, or an array with join details.
38
     *
39
     * @see SQLConditionalExpression::addLeftJoin()
40
     *
41
     * @var array
42
     */
43
    protected $from = array();
44
45
    /**
46
     * Construct a new SQLInteractExpression.
47
     *
48
     * @param array|string $from An array of Tables (FROM clauses). The first one should be just the table name.
49
     * @param array $where An array of WHERE clauses.
50
     */
51
    function __construct($from = array(), $where = array())
52
    {
53
        $this->setFrom($from);
54
        $this->setWhere($where);
55
    }
56
57
    /**
58
     * Sets the list of tables to query from or update
59
     *
60
     * @example $query->setFrom('"MyTable"'); // SELECT * FROM "MyTable"
61
     *
62
     * @param string|array $from Single, or list of, ANSI quoted table names
63
     * @return $this
64
     */
65
    public function setFrom($from)
66
    {
67
        $this->from = array();
68
        return $this->addFrom($from);
69
    }
70
71
    /**
72
     * Add a table to include in the query or update
73
     *
74
     * @example $query->addFrom('"MyTable"'); // SELECT * FROM "MyTable"
75
     *
76
     * @param string|array $from Single, or list of, ANSI quoted table names
77
     * @return $this Self reference
78
     */
79
    public function addFrom($from)
80
    {
81
        if (is_array($from)) {
82
            $this->from = array_merge($this->from, $from);
83
        } elseif (!empty($from)) {
84
            $this->from[str_replace(array('"','`'), '', $from)] = $from;
85
        }
86
87
        return $this;
88
    }
89
90
    /**
91
     * Set the connective property.
92
     *
93
     * @param string $value either 'AND' or 'OR'
94
     */
95
    public function setConnective($value)
96
    {
97
        $this->connective = $value;
98
    }
99
100
    /**
101
     * Get the connective property.
102
     *
103
     * @return string 'AND' or 'OR'
104
     */
105
    public function getConnective()
106
    {
107
        return $this->connective;
108
    }
109
110
    /**
111
     * Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
112
     */
113
    public function useDisjunction()
114
    {
115
        $this->setConnective('OR');
116
    }
117
118
    /**
119
     * Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause.
120
     */
121
    public function useConjunction()
122
    {
123
        $this->setConnective('AND');
124
    }
125
126
    /**
127
     * Add a LEFT JOIN criteria to the tables list.
128
     *
129
     * @param string $table Unquoted table name
130
     * @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement, Needs to be valid
131
     *                            (quoted) SQL.
132
     * @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
133
     * @param int $order A numerical index to control the order that joins are added to the query; lower order values
134
     *                   will cause the query to appear first. The default is 20, and joins created automatically by the
135
     *                   ORM have a value of 10.
136
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
137
     * @return $this Self reference
138
     */
139
    public function addLeftJoin($table, $onPredicate, $tableAlias = '', $order = 20, $parameters = array())
140
    {
141
        if (!$tableAlias) {
142
            $tableAlias = $table;
143
        }
144
        $this->from[$tableAlias] = array(
145
            'type' => 'LEFT',
146
            'table' => $table,
147
            'filter' => array($onPredicate),
148
            'order' => $order,
149
            'parameters' => $parameters
150
        );
151
        return $this;
152
    }
153
154
    /**
155
     * Add an INNER JOIN criteria
156
     *
157
     * @param string $table Unquoted table name
158
     * @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement. Needs to be
159
     * valid (quoted) SQL.
160
     * @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
161
     * @param int $order A numerical index to control the order that joins are added to the query; lower order
162
     * values will cause the query to appear first. The default is 20, and joins created automatically by the
163
     * ORM have a value of 10.
164
     * @param array $parameters Any additional parameters if the join is a parameterised subquery
165
     * @return $this Self reference
166
     */
167
    public function addInnerJoin($table, $onPredicate, $tableAlias = null, $order = 20, $parameters = array())
168
    {
169
        if (!$tableAlias) {
170
            $tableAlias = $table;
171
        }
172
        $this->from[$tableAlias] = array(
173
            'type' => 'INNER',
174
            'table' => $table,
175
            'filter' => array($onPredicate),
176
            'order' => $order,
177
            'parameters' => $parameters
178
        );
179
        return $this;
180
    }
181
182
    /**
183
     * Add an additional filter (part of the ON clause) on a join.
184
     *
185
     * @param string $table Table to join on from the original join (unquoted)
186
     * @param string $filter The "ON" SQL fragment (escaped)
187
     * @return $this Self reference
188
     */
189
    public function addFilterToJoin($table, $filter)
190
    {
191
        $this->from[$table]['filter'][] = $filter;
192
        return $this;
193
    }
194
195
    /**
196
     * Set the filter (part of the ON clause) on a join.
197
     *
198
     * @param string $table Table to join on from the original join (unquoted)
199
     * @param string $filter The "ON" SQL fragment (escaped)
200
     * @return $this Self reference
201
     */
202
    public function setJoinFilter($table, $filter)
203
    {
204
        $this->from[$table]['filter'] = array($filter);
205
        return $this;
206
    }
207
208
    /**
209
     * Returns true if we are already joining to the given table alias
210
     *
211
     * @param string $tableAlias Table name
212
     * @return boolean
213
     */
214
    public function isJoinedTo($tableAlias)
215
    {
216
        return isset($this->from[$tableAlias]);
217
    }
218
219
    /**
220
     * Return a list of tables that this query is selecting from.
221
     *
222
     * @return array Unquoted table names
223
     */
224
    public function queriedTables()
225
    {
226
        $tables = array();
227
228
        foreach ($this->from as $key => $tableClause) {
229
            if (is_array($tableClause)) {
230
                $table = '"' . $tableClause['table'] . '"';
231
            } elseif (is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) {
232
                $table = $matches[1];
233
            } else {
234
                $table = $tableClause;
235
            }
236
237
            // Handle string replacements
238
            if ($this->replacementsOld) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->replacementsOld of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
239
                $table = str_replace($this->replacementsOld, $this->replacementsNew, $table);
240
            }
241
242
            $tables[] = preg_replace('/^"|"$/', '', $table);
243
        }
244
245
        return $tables;
246
    }
247
248
    /**
249
     * Return a list of tables queried
250
     *
251
     * @return array
252
     */
253
    public function getFrom()
254
    {
255
        return $this->from;
256
    }
257
258
    /**
259
     * Retrieves the finalised list of joins
260
     *
261
     * @todo This part of the code could be simplified
262
     *
263
     * @param array $parameters Out variable for parameters required for this query
264
     * @return array List of joins as a mapping from array('Alias' => 'Join Expression')
265
     */
266
    public function getJoins(&$parameters = array())
267
    {
268
        if (func_num_args() == 0) {
269
            Deprecation::notice(
270
                '4.0',
271
                'SQLConditionalExpression::getJoins() now may produce parameters which are necessary to
272
				execute this query'
273
            );
274
        }
275
276
        // Sort the joins
277
        $parameters = array();
278
        $joins = $this->getOrderedJoins($this->from);
279
280
        // Build from clauses
281
        foreach ($joins as $alias => $join) {
282
            // $join can be something like this array structure
283
            // array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
284
            // "Status = 'approved'", 'order' => 20))
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
285
            if (!is_array($join)) {
286
                if (empty($alias) || is_numeric($alias)) {
287
                    continue;
288
                }
289
290
                $trimmedAlias = trim($alias, '"');
291
292
                if ($trimmedAlias !== trim($join, '"')) {
293
                    $joins[$alias] = "{$join} AS \"{$trimmedAlias}\"";
294
                }
295
296
                continue;
297
            }
298
299
            if (is_string($join['filter'])) {
300
                $filter = $join['filter'];
301
            } elseif (sizeof($join['filter']) == 1) {
302
                $filter = $join['filter'][0];
303
            } else {
304
                $filter = "(" . implode(") AND (", $join['filter']) . ")";
305
            }
306
307
            // Ensure tables are quoted, unless the table is actually a sub-select
308
            $table = preg_match('/\bSELECT\b/i', $join['table'])
309
                ? $join['table']
310
                : "\"{$join['table']}\"";
311
            $aliasClause = ($alias != $join['table'])
312
                ? " AS \"{$alias}\""
313
                : "";
314
            $joins[$alias] = strtoupper($join['type']) . " JOIN " . $table . "$aliasClause ON $filter";
315
            if (!empty($join['parameters'])) {
316
                $parameters = array_merge($parameters, $join['parameters']);
317
            }
318
        }
319
320
        return $joins;
321
    }
322
323
    /**
324
     * Ensure that framework "auto-generated" table JOINs are first in the finalised SQL query.
325
     * This prevents issues where developer-initiated JOINs attempt to JOIN using relations that haven't actually
326
     * yet been scaffolded by the framework. Demonstrated by PostGres in errors like:
327
     *"...ERROR: missing FROM-clause..."
328
     *
329
     * @param $from array - in the format of $this->from
330
     * @return array - and reorderded list of selects
331
     */
332
    protected function getOrderedJoins($from)
333
    {
334
        if (count($from) <= 1) {
335
            return $from;
336
        }
337
338
        // shift the first FROM table out from so we only deal with the JOINs
339
        reset($from);
340
        $baseFromAlias = key($from);
341
        $baseFrom = array_shift($from);
342
343
        $this->mergesort($from, function ($firstJoin, $secondJoin) {
344
            if (!is_array($firstJoin)
345
                || !is_array($secondJoin)
346
                || $firstJoin['order'] == $secondJoin['order']
347
            ) {
348
                return 0;
349
            } else {
350
                return ($firstJoin['order'] < $secondJoin['order']) ?  -1 : 1;
351
            }
352
        });
353
354
        // Put the first FROM table back into the results
355
        if (!empty($baseFromAlias) && !is_numeric($baseFromAlias)) {
356
            $from = array_merge([$baseFromAlias => $baseFrom], $from);
357
        } else {
358
            array_unshift($from, $baseFrom);
359
        }
360
361
        return $from;
362
    }
363
364
    /**
365
     * Since uasort don't preserve the order of an array if the comparison is equal
366
     * we have to resort to a merge sort. It's quick and stable: O(n*log(n)).
367
     *
368
     * @see http://stackoverflow.com/q/4353739/139301
369
     *
370
     * @param array &$array The array to sort
371
     * @param callable|string $cmpFunction The function to use for comparison
372
     */
373
    protected function mergesort(&$array, $cmpFunction = 'strcmp')
374
    {
375
        // Arrays of size < 2 require no action.
376
        if (count($array) < 2) {
377
            return;
378
        }
379
        // Split the array in half
380
        $halfway = count($array) / 2;
381
        $array1 = array_slice($array, 0, $halfway);
382
        $array2 = array_slice($array, $halfway);
383
        // Recurse to sort the two halves
384
        $this->mergesort($array1, $cmpFunction);
385
        $this->mergesort($array2, $cmpFunction);
386
        // If all of $array1 is <= all of $array2, just append them.
387
        if (call_user_func($cmpFunction, end($array1), reset($array2)) < 1) {
388
            $array = array_merge($array1, $array2);
389
            return;
390
        }
391
        // Merge the two sorted arrays into a single sorted array
392
        $array = array();
393
        $val1 = reset($array1);
394
        $val2 = reset($array2);
395
        do {
396
            if (call_user_func($cmpFunction, $val1, $val2) < 1) {
397
                $array[key($array1)] = $val1;
398
                $val1 = next($array1);
399
            } else {
400
                $array[key($array2)] = $val2;
401
                $val2 = next($array2);
402
            }
403
        } while ($val1 && $val2);
404
405
        // Merge the remainder
406
        while ($val1) {
407
            $array[key($array1)] = $val1;
408
            $val1 = next($array1);
409
        }
410
        while ($val2) {
411
            $array[key($array2)] = $val2;
412
            $val2 = next($array2);
413
        }
414
        return;
415
    }
416
417
    /**
418
     * Set a WHERE clause.
419
     *
420
     * @see SQLConditionalExpression::addWhere() for syntax examples
421
     *
422
     * @param mixed $where Predicate(s) to set, as escaped SQL statements or paramaterised queries
423
     * @param mixed $where,... Unlimited additional predicates
424
     * @return $this Self reference
425
     */
426
    public function setWhere($where)
427
    {
428
        $where = func_num_args() > 1 ? func_get_args() : $where;
429
        $this->where = array();
430
        return $this->addWhere($where);
431
    }
432
433
    /**
434
     * Adds a WHERE clause.
435
     *
436
     * Note that the database will execute any parameterised queries using
437
     * prepared statements whenever available.
438
     *
439
     * There are several different ways of doing this.
440
     *
441
     * <code>
442
     *  // the entire predicate as a single string
443
     *  $query->addWhere("\"Column\" = 'Value'");
444
     *
445
     *  // multiple predicates as an array
446
     *  $query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
447
     *
448
     *  // Shorthand for the above using argument expansion
449
     *  $query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
450
     *
451
     *  // multiple predicates with parameters
452
     *  $query->addWhere(array('"Column" = ?' => $column, '"Name" = ?' => $value)));
453
     *
454
     *  // Shorthand for simple column comparison (as above), omitting the '?'
455
     *  $query->addWhere(array('"Column"' => $column, '"Name"' => $value));
456
     *
457
     *  // Multiple predicates, each with multiple parameters.
458
     *  $query->addWhere(array(
459
     *      '"ColumnOne" = ? OR "ColumnTwo" != ?' => array(1, 4),
460
     *      '"ID" != ?' => $value
461
     *  ));
462
     *
463
     *  // Using a dynamically generated condition (any object that implements SQLConditionGroup)
464
     *  $condition = new ObjectThatImplements_SQLConditionGroup();
465
     *  $query->addWhere($condition);
466
     *
467
     * </code>
468
     *
469
     * Note that if giving multiple parameters for a single predicate the array
470
     * of values must be given as an indexed array, not an associative array.
471
     *
472
     * Also should be noted is that any null values for parameters may give unexpected
473
     * behaviour. array('Column' => NULL) is shorthand for array('Column = ?', NULL), and
474
     * will not match null values for that column, as 'Column IS NULL' is the correct syntax.
475
     *
476
     * Additionally, be careful of key conflicts. Adding two predicates with the same
477
     * condition but different parameters can cause a key conflict if added in the same array.
478
     * This can be solved by wrapping each individual condition in an array. E.g.
479
     *
480
     * <code>
481
     * // Multiple predicates with duplicate conditions
482
     *  $query->addWhere(array(
483
     *      array('ID != ?' => 5),
484
     *      array('ID != ?' => 6)
485
     *  ));
486
     *
487
     * // Alternatively this can be added in two separate calls to addWhere
488
     * $query->addWhere(array('ID != ?' => 5));
489
     * $query->addWhere(array('ID != ?' => 6));
490
     *
491
     * // Or simply omit the outer array
492
     * $query->addWhere(array('ID != ?' => 5), array('ID != ?' => 6));
493
     * </code>
494
     *
495
     * If it's necessary to force the parameter to be considered as a specific data type
496
     * by the database connector's prepared query processor any parameter can be cast
497
     * to that type by using the following format.
498
     *
499
     * <code>
500
     *  // Treat this value as a double type, regardless of its type within PHP
501
     *  $query->addWhere(array(
502
     *      'Column' => array(
503
     *          'value' => $variable,
504
     *          'type' => 'double'
505
     *      )
506
     *  ));
507
     * </code>
508
     *
509
     * @param mixed $where Predicate(s) to set, as escaped SQL statements or paramaterised queries
510
     * @param mixed $where,... Unlimited additional predicates
511
     * @return $this Self reference
512
     */
513
    public function addWhere($where)
514
    {
515
        $where = $this->normalisePredicates(func_get_args());
516
517
        // If the function is called with an array of items
518
        $this->where = array_merge($this->where, $where);
519
520
        return $this;
521
    }
522
523
    /**
524
     * @see SQLConditionalExpression::addWhere()
525
     *
526
     * @param mixed $filters Predicate(s) to set, as escaped SQL statements or paramaterised queries
527
     * @param mixed $filters,... Unlimited additional predicates
528
     * @return $this Self reference
529
     */
530
    public function setWhereAny($filters)
531
    {
532
        $filters = func_num_args() > 1 ? func_get_args() : $filters;
533
        return $this
534
            ->setWhere(array())
535
            ->addWhereAny($filters);
536
    }
537
538
    /**
539
     * @see SQLConditionalExpression::addWhere()
540
     *
541
     * @param mixed $filters Predicate(s) to set, as escaped SQL statements or paramaterised queries
542
     * @param mixed $filters,... Unlimited additional predicates
543
     * @return $this Self reference
544
     */
545
    public function addWhereAny($filters)
546
    {
547
        // Parse and split predicates along with any parameters
548
        $filters = $this->normalisePredicates(func_get_args());
549
        $this->splitQueryParameters($filters, $predicates, $parameters);
550
551
        $clause = "(" . implode(") OR (", $predicates) . ")";
552
        return $this->addWhere(array($clause => $parameters));
553
    }
554
555
    /**
556
     * Return a list of WHERE clauses used internally.
557
     *
558
     * @return array
559
     */
560
    public function getWhere()
561
    {
562
        return $this->where;
563
    }
564
565
    /**
566
     * Return a list of WHERE clauses used internally.
567
     *
568
     * @param array $parameters Out variable for parameters required for this query
569
     * @return array
570
     */
571
    public function getWhereParameterised(&$parameters)
572
    {
573
        $this->splitQueryParameters($this->where, $predicates, $parameters);
574
        return $predicates;
575
    }
576
577
    /**
578
     * Given a key / value pair, extract the predicate and any potential paramaters
579
     * in a format suitable for storing internally as a list of paramaterised conditions.
580
     *
581
     * @param string|integer $key The left hand (key index) of this condition.
582
     * Could be the predicate or an integer index.
583
     * @param mixed $value The The right hand (array value) of this condition.
584
     * Could be the predicate (if non-paramaterised), or the parameter(s). Could also be
585
     * an array containing a nested condition in the similar format this function outputs.
586
     * @return array|SQLConditionGroup A single item array in the format
587
     * array($predicate => array($parameters)), unless it's a SQLConditionGroup
588
     */
589
    protected function parsePredicate($key, $value)
590
    {
591
        // If a string key is given then presume this is a paramaterised condition
592
        if ($value instanceof SQLConditionGroup) {
593
            return $value;
594
        } elseif (is_string($key)) {
595
            // Extract the parameter(s) from the value
596
            if (!is_array($value) || isset($value['type'])) {
597
                $parameters = array($value);
598
            } else {
599
                $parameters = array_values($value);
600
            }
601
602
            // Append '= ?' if not present, parameters are given, and we have exactly one parameter
603
            if (strpos($key, '?') === false) {
604
                $parameterCount = count($parameters);
605
                if ($parameterCount === 1) {
606
                    $key .= " = ?";
607
                } elseif ($parameterCount > 1) {
608
                    user_error(
609
                        "Incorrect number of '?' in predicate $key. Expected $parameterCount but none given.",
610
                        E_USER_ERROR
611
                    );
612
                }
613
            }
614
            return array($key => $parameters);
615
        } elseif (is_array($value)) {
616
            // If predicates are nested one per array (as per the internal format)
617
            // then run a quick check over the contents and recursively parse
618
            if (count($value) != 1) {
619
                user_error('Nested predicates should be given as a single item array in '
620
                        . 'array($predicate => array($prameters)) format)', E_USER_ERROR);
621
            }
622
            foreach ($value as $key => $pairValue) {
0 ignored issues
show
introduced by
$key is overwriting one of the parameters of this function.
Loading history...
623
                return $this->parsePredicate($key, $pairValue);
624
            }
625
        } else {
626
            // Non-paramaterised condition
627
            return array($value => array());
628
        }
629
    }
630
631
    /**
632
     * Given a list of conditions in any user-acceptable format, convert this
633
     * to an array of paramaterised predicates suitable for merging with $this->where.
634
     *
635
     * Normalised predicates are in the below format, in order to avoid key collisions.
636
     *
637
     * <code>
638
     * array(
639
     *  array('Condition != ?' => array('parameter')),
640
     *  array('Condition != ?' => array('otherparameter')),
641
     *  array('Condition = 3' => array()),
642
     *  array('Condition = ? OR Condition = ?' => array('parameter1', 'parameter2))
643
     * )
644
     * </code>
645
     *
646
     * @param array $predicates List of predicates. These should be wrapped in an array
647
     * one level more than for addWhere, as query expansion is not supported here.
648
     * @return array List of normalised predicates
649
     */
650
    protected function normalisePredicates(array $predicates)
651
    {
652
        // Since this function is called with func_get_args we should un-nest the single first parameter
653
        if (count($predicates) == 1) {
654
            $predicates = array_shift($predicates);
655
        }
656
657
        // Ensure single predicates are iterable
658
        if (!is_array($predicates)) {
659
            $predicates = array($predicates);
660
        }
661
662
        $normalised = array();
663
        foreach ($predicates as $key => $value) {
664
            if (empty($value) && (empty($key) || is_numeric($key))) {
665
                continue; // Ignore empty conditions
666
            }
667
            $normalised[] = $this->parsePredicate($key, $value);
668
        }
669
670
        return $normalised;
671
    }
672
673
    /**
674
     * Given a list of conditions as per the format of $this->where, split
675
     * this into an array of predicates, and a separate array of ordered parameters
676
     *
677
     * Note, that any SQLConditionGroup objects will be evaluated here.
678
     * @see SQLConditionGroup
679
     *
680
     * @param array $conditions List of Conditions including parameters
681
     * @param array $predicates Out parameter for the list of string predicates
682
     * @param array $parameters Out parameter for the list of parameters
683
     */
684
    public function splitQueryParameters($conditions, &$predicates, &$parameters)
685
    {
686
        // Merge all filters with paramaterised queries
687
        $predicates = array();
688
        $parameters = array();
689
        foreach ($conditions as $condition) {
690
            // Evaluate the result of SQLConditionGroup here
691
            if ($condition instanceof SQLConditionGroup) {
692
                $conditionSQL = $condition->conditionSQL($conditionParameters);
693
                if (!empty($conditionSQL)) {
694
                    $predicates[] = $conditionSQL;
695
                    $parameters = array_merge($parameters, $conditionParameters);
696
                }
697
            } else {
698
                foreach ($condition as $key => $value) {
699
                    $predicates[] = $key;
700
                    $parameters = array_merge($parameters, $value);
701
                }
702
            }
703
        }
704
    }
705
706
    /**
707
     * Checks whether this query is for a specific ID in a table
708
     *
709
     * @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5")
710
     *
711
     * @return boolean
712
     */
713
    public function filtersOnID()
714
    {
715
        $regexp = '/^(.*\.)?("|`)?ID("|`)?\s?(=|IN)/';
716
717
        foreach ($this->getWhereParameterised($parameters) as $predicate) {
718
            if (preg_match($regexp, $predicate)) {
719
                return true;
720
            }
721
        }
722
723
        return false;
724
    }
725
726
    /**
727
     * Checks whether this query is filtering on a foreign key, ie finding a has_many relationship
728
     *
729
     * @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ParentID=5")
730
     *
731
     * @return boolean
732
     */
733
    public function filtersOnFK()
734
    {
735
        $regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?(=|IN)/';
736
737
        // @todo - Test this works with paramaterised queries
738
        foreach ($this->getWhereParameterised($parameters) as $predicate) {
739
            if (preg_match($regexp, $predicate)) {
740
                return true;
741
            }
742
        }
743
744
        return false;
745
    }
746
747
    public function isEmpty()
748
    {
749
        return empty($this->from);
750
    }
751
752
    /**
753
     * Generates an SQLDelete object using the currently specified parameters
754
     *
755
     * @return SQLDelete
756
     */
757
    public function toDelete()
758
    {
759
        $delete = new SQLDelete();
760
        $this->copyTo($delete);
761
        return $delete;
762
    }
763
764
    /**
765
     * Generates an SQLSelect object using the currently specified parameters.
766
     *
767
     * @return SQLSelect
768
     */
769
    public function toSelect()
770
    {
771
        $select = new SQLSelect();
772
        $this->copyTo($select);
773
        return $select;
774
    }
775
776
    /**
777
     * Generates an SQLUpdate object using the currently specified parameters.
778
     * No fields will have any assigned values for the newly generated SQLUpdate
779
     * object.
780
     *
781
     * @return SQLUpdate
782
     */
783
    public function toUpdate()
784
    {
785
        $update = new SQLUpdate();
786
        $this->copyTo($update);
787
        return $update;
788
    }
789
}
790