Code

< 40 %
40-60 %
> 60 %
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 81
    public function __construct(Connection $conn)
63
    {
64 81
        $this->conn = $conn;
65 81
    }
66
67
    public function getEscaper()
68
    {
69
        return $this->conn->getEscaper();
70
    }
71
72 60
    public function select($select = null)
73
    {
74 60
        $this->type = self::TYPE_SELECT;
75 60
        if (null === $select) {
76
            return $this;
77
        }
78
79 60
        return $this->add('select', (array) $select);
80
    }
81
82 9
    public function addSelect($select)
83
    {
84 9
        $this->type = self::TYPE_SELECT;
85
86 9
        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 6
        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 6
        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 3
        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 3
        return $this->add('from', array('table' => $index));
115
    }
116
117 6
    public function set($key, $value)
118
    {
119 6
        return $this->add('set', compact('key', 'value'), true);
120
    }
121
122 9
    public function values(array $values)
123
    {
124 9
        return $this->add('values', $values);
125
    }
126
127 3
    public function addValues(array $values)
128
    {
129 3
        return $this->add('values', $values, true);
130
    }
131
132 60
    public function from($index)
133
    {
134 60
        return $this->add('from', array('table' => $index));
135
    }
136
137 3
    public function addFrom($index)
138
    {
139 3
        return $this->add('from', array('table' => $index), true);
140
    }
141
142 39
    public function where($where)
143
    {
144 39
        return $this->add('where', $where);
145
    }
146
147 9
    public function andWhere($where)
148
    {
149 9
        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 15
            ->add('groupByLimit', $limit);
157
    }
158
159 3
    public function addGroupBy($groupBy)
160
    {
161 3
        return $this->add('groupBy', $groupBy, true);
162
    }
163
164 6
    public function withinGroupOrderBy($order, $direction = null)
165
    {
166 6
        return $this->add('withinGroupOrderBy', compact('order', 'direction'));
167
    }
168
169 3
    public function addWithinGroupOrderBy($order, $direction = null)
170
    {
171 3
        return $this->add('withinGroupOrderBy', compact('order', 'direction'), true);
172
    }
173
174
    /**
175
     * @param string|array $facet 'column1', or array('column1', 'column1') or array('column1' => 'column_alias', 'column2')
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 3
    public function facet($facet, $by = null, $order = null, $direction = null, $limit = null, $skip = 0)
185
    {
186 3
        $facet = (array) $facet;
187
188 3
        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 9
    public function orderBy($order, $direction = null)
197
    {
198 9
        return $this->add('orderBy', compact('order', 'direction'));
199
    }
200
201 6
    public function addOrderBy($order, $direction = null)
202
    {
203 6
        return $this->add('orderBy', compact('order', 'direction'), true);
204
    }
205
206 6
    public function setOption($name, $value)
207
    {
208 6
        return $this->add('options', compact('name', 'value'), true);
209
    }
210
211 6
    public function setMaxResults($limit)
212
    {
213 6
        return $this->add('maxResults', $limit);
214
    }
215
216 3
    public function setFirstResult($skip)
217
    {
218 3
        return $this->add('firstResult', $skip);
219
    }
220
221
    public function merge(self $qb)
222
    {
223
        //TODO: делать или не делать?
224
        // ...
225
    }
226
227 9
    public function setParameter($parameter, $value)
228
    {
229 9
        $this->parameters[$parameter] = $value;
230
231 9
        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 6
    public function createParameter($value, $prefix = 'gen_')
243
    {
244 6
        $prefix = preg_replace('/[^a-z0-9_]/ui', '_', $prefix);
245 6
        $prefix .= ++$this->parametersCounter;
246 6
        $this->setParameter($prefix, $value);
247
248 6
        return ':'.$prefix;
249
    }
250
251 9
    public function getParameters()
252
    {
253 9
        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 78
    public function getSql()
272
    {
273 78
        if (!$this->isDirty) {
274
            return $this->sql;
275
        }
276
277 78
        switch ($this->type) {
278 78
            case self::TYPE_SELECT:
279 60
                $this->sql = $this->buildSqlForSelect();
280 60
                break;
281
282 18
            case self::TYPE_INSERT:
283 16
            case self::TYPE_REPLACE:
284 9
                $this->sql = $this->buildSqlForInsert();
285 9
                break;
286
287 9
            case self::TYPE_UPDATE:
288 6
                $this->sql = $this->buildSqlForUpdate();
289 6
                break;
290
291 3
            case self::TYPE_DELETE:
292 3
                $this->sql = $this->buildSqlForDelete();
293 3
                break;
294 52
        }
295
296 78
        $this->isDirty = false;
297
298 78
        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 78
    protected function add($sqlPartName, $sqlPart, $append = false)
311
    {
312 78
        $this->isDirty = true;
313
314 78
        if (self::$multipleParts[$sqlPartName]) {
315 78
            if ($append) {
316 45
                $this->sqlParts[$sqlPartName][] = $sqlPart;
317 30
            } else {
318 78
                $this->sqlParts[$sqlPartName] = array($sqlPart);
319
            }
320 52
        } else {
321 21
            $this->sqlParts[$sqlPartName] = $sqlPart;
322
        }
323
324 78
        return $this;
325
    }
326
327 60
    protected function buildSqlForSelect()
328
    {
329 60
        $select = call_user_func_array('array_merge', $this->sqlParts['select']);
330 60
        $query = 'SELECT '.implode(', ', $select).' FROM ';
331
332 60
        $fromParts = array();
333 60
        foreach ($this->sqlParts['from'] as $from) {
334 60
            $table = $from['table'];
335 60
            if ($table instanceof static) {
336 3
                $fromParts[] = '('.$table->getSql().')';
337 3
                foreach ($table->getParameters() as $parameter => $value) {
338 3
                    $this->setParameter($parameter, $value);
339 2
                }
340 2
            } else {
341 60
                $fromParts[] = $table;
342
            }
343 40
        }
344
345 60
        $query .= implode(', ', $fromParts)
346 60
            .$this->buildWherePart()
347 60
            .$this->buildGroupByPart()
348 60
            .$this->buildOrderByPart();
349
350
        //TODO: inject limit, skip as parameters for better caching? Or just move caching to upper layer
351 60
        if ($this->sqlParts['maxResults']) {
352 6
            $query .= ' LIMIT '.(int) $this->sqlParts['firstResult'].', '.(int) $this->sqlParts['maxResults'];
353 4
        }
354
355 60
        $query .= $this->buildOptionsPart()
356 60
            .$this->buildFacetPart();
357
358 60
        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 9
            $columns = array_keys($value);
368 9
            $valuesParts[] = '('.implode(', ', $value).')';
369 6
        }
370
371 9
        $index = current($this->sqlParts['from'])['table'];
372 9
        $query = ($this->type === self::TYPE_REPLACE ? 'REPLACE' : 'INSERT')
373 9
            .' INTO '.$index
374 9
            .' ('.implode(', ', $columns).') VALUES '.implode(', ', $valuesParts);
375
376 9
        return $query;
377
    }
378
379 6
    protected function buildSqlForUpdate()
380
    {
381 6
        $index = current($this->sqlParts['from'])['table'];
382 6
        $setParts = array();
383 6
        foreach ($this->sqlParts['set'] as $setPart) {
384 6
            $setParts[] = $setPart['key'].' = '.$setPart['value'];
385 4
        }
386
387 6
        $query = 'UPDATE '.$index.' SET '.implode(', ', $setParts).$this->buildWherePart();
388
389 6
        return $query;
390
    }
391
392 3
    protected function buildSqlForDelete()
393
    {
394 3
        $index = current($this->sqlParts['from'])['table'];
395 3
        $query = 'DELETE FROM '.$index.$this->buildWherePart();
396
397 3
        return $query;
398
    }
399
400 69
    protected function buildWherePart()
401
    {
402 69
        if (!$this->sqlParts['where']) {
403 33
            return '';
404
        }
405
406 39
        return ' WHERE '.implode(' AND ', $this->sqlParts['where']);
407
    }
408
409 60
    protected function buildGroupByPart()
410
    {
411 60
        if (!$this->sqlParts['groupBy']) {
412 45
            return '';
413
        }
414
415 15
        $sql = ' GROUP'.($this->sqlParts['groupByLimit'] ? ' '.$this->sqlParts['groupByLimit'] : '')
416 15
            .' 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) {
424 6
            $orderByParts[] = $orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
425 4
        }
426
427 6
        return $sql.' WITHIN GROUP ORDER BY '.implode(', ', $orderByParts);
428
    }
429
430 60
    protected function buildOrderByPart()
431
    {
432 60
        if (!$this->sqlParts['orderBy']) {
433 51
            return '';
434
        }
435
436 9
        $orderByParts = array();
437 9 View Code Duplication
        foreach ($this->sqlParts['orderBy'] as $orderBy) {
438 9
            $orderByParts[] = $orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
439 6
        }
440
441 9
        return ' ORDER BY '.implode(', ', $orderByParts);
442
    }
443
444 60
    protected function buildOptionsPart()
445
    {
446 60
        if (!$this->sqlParts['options']) {
447 54
            return '';
448
        }
449
450 6
        $optionsParts = array();
451 6
        foreach ($this->sqlParts['options'] as $option) {
452 6
            $optionsParts[] = $option['name'].' = '.$option['value'];
453 4
        }
454
455 6
        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 60
    protected function buildFacetPart()
464
    {
465 60
        if (!$this->sqlParts['facet']) {
466 57
            return '';
467
        }
468
469 3
        $facetParts = array();
470 3
        foreach ($this->sqlParts['facet'] as $facet) {
471 3
            $facetExpressions = array();
472 3
            foreach ($facet['facet'] as $key => $facetExpr) {
473 3
                if (is_int($key)) {
474 3
                    $facetExpressions[] = $facetExpr;
475 2
                } else {
476 3
                    $facetExpressions[] = $key.' AS '.$facetExpr;
477
                }
478 2
            }
479 3
            $facetPart = 'FACET '.implode(', ', $facetExpressions);
480 3
            if ($facet['by']) {
481 3
                $facetPart .= ' BY '.$facet['by'];
482 2
            }
483 3
            if ($facet['order']) {
484 3
                $facetPart .= ' ORDER BY '.$facet['order'].$this->getDirection($facet['order'], $facet['direction']);
485 2
            }
486 3
            if ($facet['limit']) {
487
                $facetPart .= ' LIMIT '.(int) $facet['skip'].', '.(int) $facet['limit'];
488
            }
489
490 3
            $facetParts[] = $facetPart;
491 2
        }
492
493 3
        return ' '.implode(' ', $facetParts);
494
    }
495
496 18
    protected function getDirection($order, $direction)
497
    {
498 18
        if (strtoupper($direction) === 'DESC') {
499 18
            return ' DESC';
500
        }
501
502 15
        if (null === $direction && strtoupper($order) === 'RAND()') {
503 3
            return '';
504
        }
505
506 15
        return ' ASC';
507
    }
508
}
509