Completed
Push — master ( 7fd4a2...4773f0 )
by Konstantin
14:28 queued 09:30
created

QueryBuilder::getSql()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.0071

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 29
ccs 18
cts 19
cp 0.9474
rs 6.7272
cc 7
eloc 19
nc 7
nop 0
crap 7.0071
1
<?php
2
3
namespace Brouzie\Sphinxy;
4
5
class QueryBuilder
6
{
7
    const TYPE_INSERT = 1;
8
    const TYPE_SELECT = 2;
9
    const TYPE_UPDATE = 3;
10
    const TYPE_DELETE = 4;
11
    const TYPE_REPLACE = 5;
12
13
    /**
14
     * @var Connection
15
     */
16
    private $conn;
17
18
    private $type;
19
20
    private $sqlParts = array(
21
        'select' => array(),
22
        'from' => array(),
23
        'where' => array(),
24
        'groupBy' => array(),
25
        'groupByLimit' => null,
26
        'withinGroupOrderBy' => array(),
27
        'orderBy' => array(),
28
        'facet' => array(),
29
        'resultSetNames' => array(0),
30
        'set' => array(),
31
        'values' => array(),
32
        'options' => array(),
33
        'firstResult' => 0,
34
        'maxResults' => null,
35
    );
36
37
    private static $multipleParts = array(
38
        'select' => true,
39
        'from' => true,
40
        'where' => true,
41
        'groupBy' => true,
42
        'groupByLimit' => false,
43
        'withinGroupOrderBy' => true,
44
        'orderBy' => true,
45
        'facet' => true,
46
        'resultSetNames' => true,
47
        'set' => true,
48
        'values' => true,
49
        'options' => true,
50
        'firstResult' => false,
51
        'maxResults' => false,
52
    );
53
54
    private $isDirty = true;
55
56
    private $sql;
57
58
    private $parameters = array();
59
60
    private $parametersCounter = 0;
61
62 79
    public function __construct(Connection $conn)
63
    {
64 79
        $this->conn = $conn;
65 27
    }
66
67
    public function getEscaper()
68
    {
69
        return $this->conn->getEscaper();
70
    }
71
72 58
    public function select($select = null)
73
    {
74 58
        $this->type = self::TYPE_SELECT;
75 58
        if (null === $select) {
76
            return $this;
77
        }
78
79 20
        return $this->add('select', (array) $select);
80
    }
81
82 9
    public function addSelect($select)
83
    {
84 9
        $this->type = self::TYPE_SELECT;
85
86 3
        return $this->add('select', (array) $select, true);
87
    }
88
89 6
    public function update($index)
90
    {
91 6
        $this->type = self::TYPE_UPDATE;
92
93 2
        return $this->add('from', array('table' => $index));
94
    }
95
96 6
    public function insert($index)
97
    {
98 6
        $this->type = self::TYPE_INSERT;
99
100 2
        return $this->add('from', array('table' => $index));
101
    }
102
103 3
    public function replace($index)
104
    {
105 3
        $this->type = self::TYPE_REPLACE;
106
107 1
        return $this->add('from', array('table' => $index));
108
    }
109
110 3
    public function delete($index)
111
    {
112 3
        $this->type = self::TYPE_DELETE;
113
114 1
        return $this->add('from', array('table' => $index));
115
    }
116
117 2
    public function set($key, $value)
118
    {
119 2
        return $this->add('set', compact('key', 'value'), true);
120
    }
121
122 9
    public function values(array $values)
123
    {
124 3
        return $this->add('values', $values);
125 6
    }
126
127 3
    public function addValues(array $values)
128
    {
129 1
        return $this->add('values', $values, true);
130 2
    }
131
132 20
    public function from($index)
133
    {
134 20
        return $this->add('from', array('table' => $index));
135
    }
136
137 1
    public function addFrom($index)
138
    {
139 1
        return $this->add('from', array('table' => $index), true);
140
    }
141
142 13
    public function where($where)
143
    {
144 13
        return $this->add('where', $where);
145
    }
146
147 3
    public function andWhere($where)
148
    {
149 3
        return $this->add('where', $where, true);
150
    }
151
152 15
    public function groupBy($groupBy, $limit = null)
153
    {
154 10
        return $this
155 15
            ->add('groupBy', $groupBy)
156 5
            ->add('groupByLimit', $limit);
157 8
    }
158
159 1
    public function addGroupBy($groupBy)
160
    {
161 1
        return $this->add('groupBy', $groupBy, true);
162
    }
163
164 2
    public function withinGroupOrderBy($order, $direction = null)
165
    {
166 2
        return $this->add('withinGroupOrderBy', compact('order', 'direction'));
167
    }
168
169 1
    public function addWithinGroupOrderBy($order, $direction = null)
170
    {
171 1
        return $this->add('withinGroupOrderBy', compact('order', 'direction'), true);
172
    }
173
174
    /**
175
     * @param string|array $facet 'brand_id', or array('brand_id', 'year') or array('brand_id' => 'brand', 'year')
176
     * @param string $by
177
     * @param string $order
178
     * @param string $direction
179
     * @param int $limit
180
     * @param int $skip
181
     *
182
     * @return $this
183
     */
184 1
    public function facet($facet, $by = null, $order = null, $direction = null, $limit = null, $skip = 0)
185
    {
186 1
        $facet = (array) $facet;
187
188 1
        return $this->add('facet', compact('facet', 'by', 'order', 'direction', 'limit', 'skip'), true);
189
    }
190
191
    public function nameResultSet($name)
192
    {
193
        return $this->add('resultSetNames', $name, true);
194
    }
195
196 7
    public function orderBy($order, $direction = null)
197
    {
198 3
        return $this->add('orderBy', compact('order', 'direction'));
199 4
    }
200
201 4
    public function addOrderBy($order, $direction = null)
202
    {
203 2
        return $this->add('orderBy', compact('order', 'direction'), true);
204 2
    }
205
206 2
    public function setOption($name, $value)
207
    {
208 2
        return $this->add('options', compact('name', 'value'), true);
209
    }
210
211 2
    public function setMaxResults($limit)
212
    {
213 2
        return $this->add('maxResults', $limit);
214
    }
215
216 1
    public function setFirstResult($skip)
217
    {
218 1
        return $this->add('firstResult', $skip);
219
    }
220
221
    public function merge(self $qb)
0 ignored issues
show
Unused Code introduced by
The parameter $qb is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
222
    {
223
        //TODO: делать или не делать?
224
        // ...
225
    }
226
227 3
    public function setParameter($parameter, $value)
228
    {
229 3
        $this->parameters[$parameter] = $value;
230
231 3
        return $this;
232
    }
233
234
    /**
235
     * Creates a new named parameter and bind the value $value to it.
236
     *
237
     * @param string $value
238
     * @param string $prefix The name to bind with.
239
     *
240
     * @return string the placeholder name used.
241
     */
242 4
    public function createParameter($value, $prefix = 'gen_')
243
    {
244 2
        $prefix = preg_replace('/[^a-z0-9_]/ui', '_', $prefix);
245 2
        $prefix .= ++$this->parametersCounter;
246 2
        $this->setParameter($prefix, $value);
247
248 2
        return ':'.$prefix;
249 2
    }
250
251 7
    public function getParameters()
252
    {
253 7
        return $this->parameters;
254
    }
255
256
    public function execute()
257
    {
258
        return $this->conn->executeUpdate($this->getSql(), $this->parameters);
259
    }
260
261
    public function getResult()
262
    {
263
        return $this->conn->executeQuery($this->getSql(), $this->parameters);
264
    }
265
266
    public function getMultiResult()
267
    {
268
        return $this->conn->executeMultiQuery($this->getSql(), $this->parameters, array(), $this->sqlParts['resultSetNames']);
269
    }
270
271 76
    public function getSql()
272
    {
273 76
        if (!$this->isDirty) {
274
            return $this->sql;
275
        }
276
277 76
        switch ($this->type) {
278 76
            case self::TYPE_SELECT:
279 20
                $this->sql = $this->buildSqlForSelect();
280 58
                break;
281
282 18
            case self::TYPE_INSERT:
283 12
            case self::TYPE_REPLACE:
284 3
                $this->sql = $this->buildSqlForInsert();
285 9
                break;
286
287 9
            case self::TYPE_UPDATE:
288 2
                $this->sql = $this->buildSqlForUpdate();
289 6
                break;
290
291 3
            case self::TYPE_DELETE:
292 1
                $this->sql = $this->buildSqlForDelete();
293 3
                break;
294
        }
295
296 76
        $this->isDirty = false;
297
298 76
        return $this->sql;
299
    }
300
301
    /**
302
     * Either appends to or replaces a single, generic query part.
303
     *
304
     * @param string $sqlPartName
305
     * @param string|array $sqlPart
306
     * @param bool $append
307
     *
308
     * @return $this This QueryBuilder instance.
309
     */
310 44
    protected function add($sqlPartName, $sqlPart, $append = false)
311
    {
312 26
        $this->isDirty = true;
313
314 26
        if (self::$multipleParts[$sqlPartName]) {
315 26
            if ($append) {
316 33
                $this->sqlParts[$sqlPartName][] = $sqlPart;
317
            } else {
318 26
                $this->sqlParts[$sqlPartName] = array($sqlPart);
319 18
            }
320
        } else {
321 19
            $this->sqlParts[$sqlPartName] = $sqlPart;
322
        }
323
324 26
        return $this;
325
    }
326
327 58
    protected function buildSqlForSelect()
328
    {
329 20
        $select = call_user_func_array('array_merge', $this->sqlParts['select']);
330 20
        $query = 'SELECT '.implode(', ', $select).' FROM ';
331
332 58
        $fromParts = array();
333 58
        foreach ($this->sqlParts['from'] as $from) {
334 56
            $table = $from['table'];
335 20
            if ($table instanceof static) {
336 1
                $fromParts[] = '('.$table->getSql().')';
337 3
                foreach ($table->getParameters() as $parameter => $value) {
338 1
                    $this->setParameter($parameter, $value);
339
                }
340
            } else {
341 58
                $fromParts[] = $table;
342 2
            }
343
        }
344
345 58
        $query .= implode(', ', $fromParts)
346 20
            .$this->buildWherePart()
347 20
            .$this->buildGroupByPart()
348 20
            .$this->buildOrderByPart();
349
350
        //TODO: inject limit, skip as parameters for better caching? Or just move caching to upper layer
351 58
        if ($this->sqlParts['maxResults']) {
352 6
            $query .= ' LIMIT '.(int) $this->sqlParts['firstResult'].', '.(int) $this->sqlParts['maxResults'];
353
        }
354
355 58
        $query .= $this->buildOptionsPart()
356 20
            .$this->buildFacetPart();
357
358 58
        return $query;
359
    }
360
361 9
    protected function buildSqlForInsert()
362
    {
363 9
        $columns = array();
364 9
        $valuesParts = array();
365 9
        foreach ($this->sqlParts['values'] as $value) {
366
            //TODO: check columns
367 3
            $columns = array_keys($value);
368 3
            $valuesParts[] = '('.implode(', ', $value).')';
369
        }
370
371 3
        $index = current($this->sqlParts['from'])['table'];
372 9
        $query = ($this->type === self::TYPE_REPLACE ? 'REPLACE' : 'INSERT')
373 3
            .' INTO '.$index
374 3
            .' ('.implode(', ', $columns).') VALUES '.implode(', ', $valuesParts);
375
376 9
        return $query;
377
    }
378
379 6
    protected function buildSqlForUpdate()
380
    {
381 2
        $index = current($this->sqlParts['from'])['table'];
382 6
        $setParts = array();
383 6
        foreach ($this->sqlParts['set'] as $setPart) {
384 4
            $setParts[] = $setPart['key'].' = '.$setPart['value'];
385
        }
386
387 2
        $query = 'UPDATE '.$index.' SET '.implode(', ', $setParts).$this->buildWherePart();
388
389 6
        return $query;
390
    }
391
392 3
    protected function buildSqlForDelete()
393
    {
394 1
        $index = current($this->sqlParts['from'])['table'];
395 1
        $query = 'DELETE FROM '.$index.$this->buildWherePart();
396
397 3
        return $query;
398
    }
399
400 67
    protected function buildWherePart()
401
    {
402 67
        if (!$this->sqlParts['where']) {
403 33
            return '';
404
        }
405
406 13
        return ' WHERE '.implode(' AND ', $this->sqlParts['where']);
407
    }
408
409 58
    protected function buildGroupByPart()
410
    {
411 58
        if (!$this->sqlParts['groupBy']) {
412 43
            return '';
413
        }
414
415 5
        $sql = ' GROUP'.($this->sqlParts['groupByLimit'] ? ' '.$this->sqlParts['groupByLimit'] : '')
416 5
            .' BY '.implode(', ', $this->sqlParts['groupBy']);
417
418 15
        if (!$this->sqlParts['withinGroupOrderBy']) {
419 9
            return $sql;
420
        }
421
422 6
        $orderByParts = array();
423 6 View Code Duplication
        foreach ($this->sqlParts['withinGroupOrderBy'] as $orderBy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
424 2
            $orderByParts[] = $orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
425
        }
426
427 2
        return $sql.' WITHIN GROUP ORDER BY '.implode(', ', $orderByParts);
428
    }
429
430 58
    protected function buildOrderByPart()
431
    {
432 58
        if (!$this->sqlParts['orderBy']) {
433 51
            return '';
434
        }
435
436 7
        $orderByParts = array();
437 7 View Code Duplication
        foreach ($this->sqlParts['orderBy'] as $orderBy) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
438 3
            $orderByParts[] = $orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
439
        }
440
441 3
        return ' ORDER BY '.implode(', ', $orderByParts);
442
    }
443
444 58
    protected function buildOptionsPart()
445
    {
446 58
        if (!$this->sqlParts['options']) {
447 52
            return '';
448
        }
449
450 6
        $optionsParts = array();
451 6
        foreach ($this->sqlParts['options'] as $option) {
452 4
            $optionsParts[] = $option['name'].' = '.$option['value'];
453
        }
454
455 2
        return ' OPTION '.implode(', ', $optionsParts);
456
    }
457
458
    /**
459
     * Build FACET {expr_list} [BY {expr_list}] [ORDER BY {expr | FACET()} {ASC | DESC}] [LIMIT [offset,] count].
460
     *
461
     * @return string
462
     */
463 58
    protected function buildFacetPart()
464
    {
465 58
        if (!$this->sqlParts['facet']) {
466 55
            return '';
467
        }
468
469 3
        $facetParts = array();
470 3
        foreach ($this->sqlParts['facet'] as $facet) {
471 1
            $facetExpressions = array();
472 1
            foreach ($facet['facet'] as $key => $facetExpr) {
473 1
                if (is_int($key)) {
474 1
                    $facetExpressions[] = $facetExpr;
475
                } else {
476 3
                    $facetExpressions[] = $key.' AS '.$facetExpr;
477
                }
478
            }
479 1
            $facetPart = 'FACET '.implode(', ', $facetExpressions);
480 1
            if ($facet['by']) {
481 3
                $facetPart .= ' BY '.$facet['by'];
482
            }
483 1
            if ($facet['order']) {
484 1
                $facetPart .= ' ORDER BY '.$facet['order'].$this->getDirection($facet['order'], $facet['direction']);
485
            }
486 1
            if ($facet['limit']) {
487
                $facetPart .= ' LIMIT '.(int) $facet['skip'].', '.(int) $facet['limit'];
488
            }
489
490 1
            $facetParts[] = $facetPart;
491
        }
492
493 1
        return ' '.implode(' ', $facetParts);
494
    }
495
496 18
    protected function getDirection($order, $direction)
497
    {
498 6
        if (strtoupper($direction) === 'DESC') {
499 18
            return ' DESC';
500
        }
501
502 9
        if (null === $direction && strtoupper($order) === 'RAND()') {
503 3
            return '';
504
        }
505
506 15
        return ' ASC';
507
    }
508
}
509