Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

SQLConditionalExpression   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 755
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 755
rs 1.263
c 0
b 0
f 0
wmc 85

32 Methods

Rating   Name   Duplication   Size   Complexity  
A toUpdate() 0 5 1
B queriedTables() 0 22 6
A addInnerJoin() 0 13 2
C parsePredicate() 0 39 11
A __construct() 0 4 1
A addFilterToJoin() 0 4 1
A getWhere() 0 3 1
A filtersOnID() 0 11 3
A addWhereAny() 0 8 1
A getFrom() 0 3 1
C mergesort() 0 42 7
A getWhereParameterised() 0 4 1
A useDisjunction() 0 3 1
A setConnective() 0 3 1
A filtersOnFK() 0 12 3
A isJoinedTo() 0 3 1
B getOrderedJoins() 0 18 5
A addFrom() 0 9 3
A useConjunction() 0 3 1
A setWhereAny() 0 6 2
D getJoins() 0 45 9
A addLeftJoin() 0 13 2
A setJoinFilter() 0 4 1
B normalisePredicates() 0 21 7
A setWhere() 0 5 2
B splitQueryParameters() 0 17 5
A isEmpty() 0 3 1
A getConnective() 0 3 1
A addWhere() 0 8 1
A setFrom() 0 4 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
    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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableAlias of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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
                continue;
287
            }
288
289
            if (is_string($join['filter'])) {
290
                $filter = $join['filter'];
291
            } elseif (sizeof($join['filter']) == 1) {
0 ignored issues
show
Bug introduced by
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

291
            } elseif (/** @scrutinizer ignore-call */ sizeof($join['filter']) == 1) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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