Completed
Pull Request — 2.x (#94)
by Gauthier
02:15
created

AbstractQuery::buildLimit()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6
Metric Value
dl 0
loc 17
ccs 11
cts 11
cp 1
rs 8.8571
cc 6
eloc 11
nc 4
nop 0
crap 6
1
<?php
2
/**
3
 *
4
 * This file is part of Aura for PHP.
5
 *
6
 * @license http://opensource.org/licenses/bsd-license.php BSD
7
 *
8
 */
9
namespace Aura\SqlQuery;
10
11
use Aura\SqlQuery\Common\LimitInterface;
12
use Aura\SqlQuery\Common\LimitOffsetInterface;
13
use Aura\SqlQuery\Common\SubselectInterface;
14
15
/**
16
 *
17
 * Abstract query object.
18
 *
19
 * @package Aura.SqlQuery
20
 *
21
 */
22
abstract class AbstractQuery
23
{
24
25
    /**
26
     *
27
     * Data to be bound to the query.
28
     *
29
     * @var array
30
     *
31
     */
32
    protected $bind_values = array();
33
34
    /**
35
     *
36
     * The list of WHERE conditions.
37
     *
38
     * @var array
39
     *
40
     */
41
    protected $where = array();
42
43
    /**
44
     *
45
     * ORDER BY these columns.
46
     *
47
     * @var array
48
     *
49
     */
50
    protected $order_by = array();
51
52
    /**
53
     *
54
     * The number of rows to select
55
     *
56
     * @var int
57
     *
58
     */
59
    protected $limit = 0;
60
61
    /**
62
     *
63
     * Return rows after this offset.
64
     *
65
     * @var int
66
     *
67
     */
68
    protected $offset = 0;
69
70
    /**
71
     *
72
     * The list of flags.
73
     *
74
     * @var array
75
     *
76
     */
77
    protected $flags = array();
78
79
    /**
80
     *
81
     * A helper for quoting identifier names.
82
     *
83
     * @var Quoter
84
     *
85
     */
86
    protected $quoter;
87
88
    /**
89
     *
90
     * Prefix to use on placeholders for "sequential" bound values; used for
91
     * deconfliction when merging bound values from sub-selects, etc.
92
     *
93
     * @var mixed
94
     *
95
     */
96
    protected $seq_bind_prefix = '';
97
98
    /**
99
     *
100
     * Constructor.
101
     *
102
     * @param Quoter $quoter A helper for quoting identifier names.
103
     *
104
     * @param string $seq_bind_prefix A prefix for rewritten sequential-binding
105
     * placeholders (@see getSeqPlaceholder()).
106
     *
107 387
     */
108
    public function __construct(Quoter $quoter, $seq_bind_prefix = '')
109 387
    {
110 387
        $this->quoter = $quoter;
111 387
        $this->seq_bind_prefix = $seq_bind_prefix;
112
    }
113
114
    /**
115
     *
116
     * Returns the prefix for rewritten sequential-binding placeholders
117
     * (@see getSeqPlaceholder()).
118
     *
119
     * @return string
120
     *
121 1
     */
122
    public function getSeqBindPrefix()
123 1
    {
124
        return $this->seq_bind_prefix;
125
    }
126
127
    /**
128
     *
129
     * Returns this query object as an SQL statement string.
130
     *
131
     * @return string
132
     *
133 232
     */
134
    public function __toString()
135 232
    {
136
        return $this->getStatement();
137
    }
138
139
    /**
140
     *
141
     * Returns this query object as an SQL statement string.
142
     *
143
     * @return string
144
     *
145 68
     */
146
    public function getStatement()
147 68
    {
148
        return $this->build();
149
    }
150
151
    /**
152
     *
153
     * Builds this query object into a string.
154
     *
155
     * @return string
156
     *
157
     */
158
    abstract protected function build();
159
160
    /**
161
     *
162
     * Returns the prefix to use when quoting identifier names.
163
     *
164
     * @return string
165
     *
166 241
     */
167
    public function getQuoteNamePrefix()
168 241
    {
169
        return $this->quoter->getQuoteNamePrefix();
170
    }
171
172
    /**
173
     *
174
     * Returns the suffix to use when quoting identifier names.
175
     *
176
     * @return string
177
     *
178 241
     */
179
    public function getQuoteNameSuffix()
180 241
    {
181
        return $this->quoter->getQuoteNameSuffix();
182
    }
183
184
    /**
185
     *
186
     * Returns an array as an indented comma-separated values string.
187
     *
188
     * @param array $list The values to convert.
189
     *
190
     * @return string
191
     *
192 209
     */
193
    protected function indentCsv(array $list)
194 209
    {
195 209
        return PHP_EOL . '    '
196
             . implode(',' . PHP_EOL . '    ', $list);
197
    }
198
199
    /**
200
     *
201
     * Returns an array as an indented string.
202
     *
203
     * @param array $list The values to convert.
204
     *
205
     * @return string
206
     *
207 71
     */
208
    protected function indent(array $list)
209 71
    {
210 71
        return PHP_EOL . '    '
211
             . implode(PHP_EOL . '    ', $list);
212
    }
213
214
    /**
215
     *
216
     * Binds multiple values to placeholders; merges with existing values.
217
     *
218
     * @param array $bind_values Values to bind to placeholders.
219
     *
220
     * @return $this
221
     *
222 31
     */
223
    public function bindValues(array $bind_values)
224
    {
225
        // array_merge() renumbers integer keys, which is bad for
226 31
        // question-mark placeholders
227 31
        foreach ($bind_values as $key => $val) {
228 31
            $this->bindValue($key, $val);
229 31
        }
230
        return $this;
231
    }
232
233
    /**
234
     *
235
     * Binds a single value to the query.
236
     *
237
     * @param string $name The placeholder name or number.
238
     *
239
     * @param mixed $value The value to bind to the placeholder.
240
     *
241
     * @return $this
242
     *
243 72
     */
244
    public function bindValue($name, $value)
245 72
    {
246 72
        $this->bind_values[$name] = $value;
247
        return $this;
248
    }
249
250
    /**
251
     *
252
     * Gets the values to bind to placeholders.
253
     *
254
     * @return array
255
     *
256 126
     */
257
    public function getBindValues()
258 126
    {
259
        return $this->bind_values;
260
    }
261
262
    /**
263
     *
264
     * Builds the flags as a space-separated string.
265
     *
266
     * @return string
267
     *
268 242
     */
269
    protected function buildFlags()
270 242
    {
271 204
        if (! $this->flags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->flags 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...
272
            return ''; // not applicable
273
        }
274 38
275
        return ' ' . implode(' ', array_keys($this->flags));
276
    }
277
278
    /**
279
     *
280
     * Sets or unsets specified flag.
281
     *
282
     * @param string $flag Flag to set or unset
283
     *
284
     * @param bool $enable Flag status - enabled or not (default true)
285
     *
286
     * @return null
287
     *
288 43
     */
289
    protected function setFlag($flag, $enable = true)
290 43
    {
291 43
        if ($enable) {
292 43
            $this->flags[$flag] = true;
293 5
        } else {
294
            unset($this->flags[$flag]);
295 43
        }
296
    }
297
298
    /**
299
     *
300
     * Reset all query flags.
301
     *
302
     * @return null
303
     *
304 15
     */
305
    protected function resetFlags()
306 15
    {
307 15
        $this->flags = array();
308
    }
309
310
    /**
311
     *
312
     * Adds a WHERE condition to the query by AND or OR. If the condition has
313
     * ?-placeholders, additional arguments to the method will be bound to
314
     * those placeholders sequentially.
315
     *
316
     * @param string $andor Add the condition using this operator, typically
317
     * 'AND' or 'OR'.
318
     *
319
     * @param array $args Arguments for adding the condition.
320
     *
321
     * @return $this
322
     *
323 60
     */
324
    protected function addWhere($andor, $args)
325 60
    {
326 60
        $this->addClauseCondWithBind('where', $andor, $args);
327
        return $this;
328
    }
329
330
    /**
331
     *
332
     * Adds conditions and binds values to a clause.
333
     *
334
     * @param string $clause The clause to work with, typically 'where' or
335
     * 'having'.
336
     *
337
     * @param string $andor Add the condition using this operator, typically
338
     * 'AND' or 'OR'.
339
     *
340
     * @param array $args Arguments for adding the condition.
341
     *
342
     * @return null
343
     *
344 70
     */
345
    protected function addClauseCondWithBind($clause, $andor, $args)
346
    {
347 70
        // remove the condition from the args and quote names in it
348 70
        $cond = array_shift($args);
349
        $cond = $this->rebuildCondAndBindValues($cond, $args);
350
351 70
        // add condition to clause; $this->where
352 70
        $clause =& $this->$clause;
353 49
        if ($clause) {
354 49
            $clause[] = "$andor $cond";
355 70
        } else {
356
            $clause[] = $cond;
357 70
        }
358
    }
359
360
    /**
361
     *
362
     * Rebuilds a condition string, replacing sequential placeholders with
363
     * named placeholders, and binding the sequential values to the named
364
     * placeholders.
365
     *
366
     * @param string $cond The condition with sequential placeholders.
367
     *
368
     * @param array $bind_values The values to bind to the sequential
369
     * placeholders under their named versions.
370
     *
371
     * @return string The rebuilt condition string.
372
     *
373 110
     */
374
    protected function rebuildCondAndBindValues($cond, array $bind_values)
375 110
    {
376
        $cond = $this->quoter->quoteNamesIn($cond);
377
378
        // bind values against ?-mark placeholders, but because PDO is finicky
379
        // about the numbering of sequential placeholders, convert each ?-mark
380 110
        // to a named placeholder
381 110
        $parts = preg_split('/(\?)/', $cond, null, PREG_SPLIT_DELIM_CAPTURE);
382 110
        foreach ($parts as $key => $val) {
383 110
            if ($val != '?') {
384
                continue;
385
            }
386 80
387 80
            $bind_value = array_shift($bind_values);
388 10
            if ($bind_value instanceof SubselectInterface) {
389 10
                $parts[$key] = $bind_value->getStatement();
390 10
                $this->bind_values = array_merge(
391 10
                    $this->bind_values,
392 10
                    $bind_value->getBindValues()
393 10
                );
394
                continue;
395
            }
396 75
397 75
            $placeholder = $this->getSeqPlaceholder();
398 75
            $parts[$key] = ':' . $placeholder;
399 110
            $this->bind_values[$placeholder] = $bind_value;
400
        }
401 110
402 110
        $cond = implode('', $parts);
403
        return $cond;
404
    }
405
406
    /**
407
     *
408
     * Gets the current sequential placeholder name.
409
     *
410
     * @return string
411
     *
412 75
     */
413
    protected function getSeqPlaceholder()
414 75
    {
415 75
        $i = count($this->bind_values) + 1;
416
        return $this->seq_bind_prefix . "_{$i}_";
417
    }
418
419
    /**
420
     *
421
     * Builds the `WHERE` clause of the statement.
422
     *
423
     * @return string
424
     *
425 196
     */
426
    protected function buildWhere()
427 196
    {
428 141
        if (! $this->where) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->where 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...
429
            return ''; // not applicable
430
        }
431 60
432
        return PHP_EOL . 'WHERE' . $this->indent($this->where);
433
    }
434
435
    /**
436
     *
437
     * Adds a column order to the query.
438
     *
439
     * @param array $spec The columns and direction to order by.
440
     *
441
     * @return $this
442
     *
443 9
     */
444
    protected function addOrderBy(array $spec)
445 9
    {
446 9
        foreach ($spec as $col) {
447 9
            $this->order_by[] = $this->quoter->quoteNamesIn($col);
448 9
        }
449
        return $this;
450
    }
451
452
    /**
453
     *
454
     * Builds the `ORDER BY ...` clause of the statement.
455
     *
456
     * @return string
457
     *
458 196
     */
459
    protected function buildOrderBy()
460 196
    {
461 187
        if (! $this->order_by) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->order_by 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...
462
            return ''; // not applicable
463
        }
464 9
465
        return PHP_EOL . 'ORDER BY' . $this->indentCsv($this->order_by);
466
    }
467
468
    /**
469
     *
470
     * Builds the `LIMIT ... OFFSET` clause of the statement.
471
     *
472
     * @return string
473
     *
474 164
     */
475
    protected function buildLimit()
476 164
    {
477 164
        $has_limit = $this instanceof LimitInterface;
478
        $has_offset = $this instanceof LimitOffsetInterface;
479 164
480 15
        if ($has_offset && $this->limit) {
481 15
            $clause = PHP_EOL . "LIMIT {$this->limit}";
482 10
            if ($this->offset) {
483 10
                $clause .= " OFFSET {$this->offset}";
484 15
            }
485 149
            return $clause;
486 4
        } elseif ($has_limit && $this->limit) {
487
            return PHP_EOL . "LIMIT {$this->limit}";
488
        }
489 145
490
        return ''; // not applicable
491
    }
492
}
493