SQLConditionalExpression   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 777
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 212
dl 0
loc 777
rs 2
c 0
b 0
f 0
wmc 92

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A addFilterToJoin() 0 4 1
A getFrom() 0 3 1
A useDisjunction() 0 3 1
A setConnective() 0 3 1
A isJoinedTo() 0 3 1
A useConjunction() 0 3 1
A setJoinFilter() 0 4 1
A getConnective() 0 3 1
A setFrom() 0 4 1
A queriedTables() 0 22 6
A addInnerJoin() 0 13 2
A addFrom() 0 9 3
A addLeftJoin() 0 13 2
A toUpdate() 0 5 1
B parsePredicate() 0 39 11
A getWhere() 0 3 1
A filtersOnID() 0 11 3
A addWhereAny() 0 8 1
B mergesort() 0 42 8
A getWhereParameterised() 0 4 1
A filtersOnFK() 0 12 3
B getOrderedJoins() 0 30 8
A setWhereAny() 0 6 2
C getJoins() 0 55 12
B normalisePredicates() 0 21 7
A setWhere() 0 5 2
A splitQueryParameters() 0 17 5
A isEmpty() 0 3 1
A addWhere() 0 8 1
A toDelete() 0 5 1
A toSelect() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like SQLConditionalExpression often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SQLConditionalExpression, and based on these observations, apply Extract Interface, too.

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
    public 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",
284
            // "Status = 'approved'", 'order' => 20))
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