Completed
Push — master ( bb1c5d...4696bd )
by Konstantin
09:11
created

QueryBuilder.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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' => null,
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' => false,
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 78
    public function __construct(Connection $conn)
63
    {
64 78
        $this->conn = $conn;
65 78
    }
66
67
    public function getEscaper()
68
    {
69
        return $this->conn->getEscaper();
70
    }
71
72 57
    public function select($select = null)
73
    {
74 57
        $this->type = self::TYPE_SELECT;
75 57
        if (null === $select) {
76
            return $this;
77
        }
78
79 57
        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 57
    public function from($index)
133
    {
134 57
        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 12
    public function groupBy($groupBy, $limit = null)
153
    {
154 12
        return $this
155 12
            ->add('groupBy', $groupBy)
156 12
            ->add('groupByLimit', $limit);
157
    }
158
159 3
    public function addGroupBy($groupBy)
160
    {
161 3
        return $this->add('groupBy', $groupBy, true);
162
    }
163
164 3
    public function withinGroupOrderBy($order, $direction = null)
165
    {
166 3
        return $this->add('withinGroupOrderBy', compact('order', 'direction'));
167
    }
168
169
    /**
170
     * @param string|array $facet 'brand_id', or array('brand_id', 'year') or array('brand_id' => 'brand', 'year')
171
     * @param string $by
172
     * @param string $order
173
     * @param string $direction
174
     * @param int $limit
175
     * @param int $skip
176
     *
177
     * @return $this
178
     */
179 3
    public function facet($facet, $by = null, $order = null, $direction = null, $limit = null, $skip = 0)
180
    {
181 3
        $facet = (array)$facet;
182
183 3
        return $this->add('facet', compact('facet', 'by', 'order', 'direction', 'limit', 'skip'), true);
184
    }
185
186
    public function nameResultSet($name)
187
    {
188
        return $this->add('resultSetNames', $name, true);
189
    }
190
191 9
    public function orderBy($order, $direction = null)
192
    {
193 9
        return $this->add('orderBy', compact('order', 'direction'));
194
    }
195
196 6
    public function addOrderBy($order, $direction = null)
197
    {
198 6
        return $this->add('orderBy', compact('order', 'direction'), true);
199
    }
200
201 6
    public function setOption($name, $value)
202
    {
203 6
        return $this->add('options', compact('name', 'value'), true);
204
    }
205
206 6
    public function setMaxResults($limit)
207
    {
208 6
        return $this->add('maxResults', $limit);
209
    }
210
211 3
    public function setFirstResult($skip)
212
    {
213 3
        return $this->add('firstResult', $skip);
214
    }
215
216
    public function merge(self $qb)
0 ignored issues
show
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...
217
    {
218
        //TODO: делать или не делать?
219
        // ...
220
    }
221
222 9
    public function setParameter($parameter, $value)
223
    {
224 9
        $this->parameters[$parameter] = $value;
225
226 9
        return $this;
227
    }
228
229
    /**
230
     * Creates a new named parameter and bind the value $value to it.
231
     *
232
     * @param string $value
233
     * @param string $prefix The name to bind with.
234
     *
235
     * @return string the placeholder name used.
236
     */
237 6
    public function createParameter($value, $prefix = 'gen_')
238
    {
239 6
        $prefix = preg_replace('/[^a-z0-9_]/ui', '_', $prefix);
240 6
        $prefix .= ++$this->parametersCounter;
241 6
        $this->setParameter($prefix, $value);
242
243 6
        return ':'.$prefix;
244
    }
245
246 9
    public function getParameters()
247
    {
248 9
        return $this->parameters;
249
    }
250
251
    public function execute()
252
    {
253
        return $this->conn->executeUpdate($this->getSql(), $this->parameters);
254
    }
255
256
    public function getResult()
257
    {
258
        return $this->conn->executeQuery($this->getSql(), $this->parameters);
259
    }
260
261
    public function getMultiResult()
262
    {
263
        return $this->conn->executeMultiQuery($this->getSql(), $this->parameters, array(), $this->sqlParts['resultSetNames']);
264
    }
265
266 75
    public function getSql()
267
    {
268 75
        if (!$this->isDirty) {
269
            return $this->sql;
270
        }
271
272 75
        switch ($this->type) {
273 75
            case self::TYPE_SELECT:
274 57
                $this->sql = $this->buildSqlForSelect();
275 57
                break;
276
277 18
            case self::TYPE_INSERT:
278 18
            case self::TYPE_REPLACE:
279 9
                $this->sql = $this->buildSqlForInsert();
280 9
                break;
281
282 9
            case self::TYPE_UPDATE:
283 6
                $this->sql = $this->buildSqlForUpdate();
284 6
                break;
285
286 3
            case self::TYPE_DELETE:
287 3
                $this->sql = $this->buildSqlForDelete();
288 3
                break;
289 75
        }
290
291 75
        $this->isDirty = false;
292
293 75
        return $this->sql;
294
    }
295
296
    /**
297
     * Either appends to or replaces a single, generic query part.
298
     *
299
     * @param string $sqlPartName
300
     * @param string|array $sqlPart
301
     * @param bool $append
302
     *
303
     * @return $this This QueryBuilder instance.
304
     */
305 75
    protected function add($sqlPartName, $sqlPart, $append = false)
306
    {
307 75
        $this->isDirty = true;
308
309 75
        if (self::$multipleParts[$sqlPartName]) {
310 75
            if ($append) {
311 42
                $this->sqlParts[$sqlPartName][] = $sqlPart;
312 42
            } else {
313 75
                $this->sqlParts[$sqlPartName] = array($sqlPart);
314
            }
315 75
        } else {
316 18
            $this->sqlParts[$sqlPartName] = $sqlPart;
317
        }
318
319 75
        return $this;
320
    }
321
322 57
    protected function buildSqlForSelect()
323
    {
324 57
        $select = call_user_func_array('array_merge', $this->sqlParts['select']);
325 57
        $query = 'SELECT '.implode(', ', $select).' FROM ';
326
327 57
        $fromParts = array();
328 57
        foreach ($this->sqlParts['from'] as $from) {
329 57
            $table = $from['table'];
330 57
            if ($table instanceof static) {
331 3
                $fromParts[] = '('.$table->getSql().')';
332 3
                foreach ($table->getParameters() as $parameter => $value) {
333 3
                    $this->setParameter($parameter, $value);
334 3
                }
335 3
            } else {
336 57
                $fromParts[] = $table;
337
            }
338 57
        }
339
340 57
        $query .= implode(', ', $fromParts)
341 57
            .$this->buildWherePart()
342 57
            .$this->buildGroupByPart()
343 57
            .$this->buildOrderByPart();
344
345
        //TODO: inject limit, skip as parameters for better caching? Or just move caching to upper layer
346 57
        if ($this->sqlParts['maxResults']) {
347 6
            $query .= ' LIMIT '.(int)$this->sqlParts['firstResult'].', '.(int)$this->sqlParts['maxResults'];
348 6
        }
349
350 57
        $query .= $this->buildOptionsPart()
351 57
            .$this->buildFacetPart();
352
353 57
        return $query;
354
    }
355
356 9
    protected function buildSqlForInsert()
357
    {
358 9
        $columns = array();
359 9
        $valuesParts = array();
360 9
        foreach ($this->sqlParts['values'] as $value) {
361
            //TODO: check columns
362 9
            $columns = array_keys($value);
363 9
            $valuesParts[] = '('.implode(', ', $value).')';
364 9
        }
365
366 9
        $index = current($this->sqlParts['from'])['table'];
367 9
        $query = ($this->type === self::TYPE_REPLACE ? 'REPLACE' : 'INSERT')
368 9
            .' INTO '.$index
369 9
            .' ('.implode(', ', $columns).') VALUES '.implode(', ', $valuesParts);
370
371 9
        return $query;
372
    }
373
374 6
    protected function buildSqlForUpdate()
375
    {
376 6
        $index = current($this->sqlParts['from'])['table'];
377 6
        $setParts = array();
378 6
        foreach ($this->sqlParts['set'] as $setPart) {
379 6
            $setParts[] = $setPart['key'].' = '.$setPart['value'];
380 6
        }
381
382 6
        $query = 'UPDATE '.$index.' SET '.implode(', ', $setParts).$this->buildWherePart();
383
384 6
        return $query;
385
    }
386
387 3
    protected function buildSqlForDelete()
388
    {
389 3
        $index = current($this->sqlParts['from'])['table'];
390 3
        $query = 'DELETE FROM '.$index.$this->buildWherePart();
391
392 3
        return $query;
393
    }
394
395 66
    protected function buildWherePart()
396
    {
397 66
        if (!$this->sqlParts['where']) {
398 30
            return '';
399
        }
400
401 39
        return ' WHERE '.implode(' AND ', $this->sqlParts['where']);
402
    }
403
404 57
    protected function buildGroupByPart()
405
    {
406 57
        if (!$this->sqlParts['groupBy']) {
407 45
            return '';
408
        }
409
410 12
        $sql = ' GROUP'.($this->sqlParts['groupByLimit'] ? ' '.$this->sqlParts['groupByLimit'] : '')
411 12
            .' BY '.implode(', ', $this->sqlParts['groupBy']);
412 12
        $orderBy = $this->sqlParts['withinGroupOrderBy'];
413 12
        if ($orderBy) {
414 3
            $sql .= ' WITHIN GROUP ORDER BY '.$orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
415 3
        }
416
417 12
        return $sql;
418
    }
419
420 57
    protected function buildOrderByPart()
421
    {
422 57
        if (!$this->sqlParts['orderBy']) {
423 48
            return '';
424
        }
425
426 9
        $orderByParts = array();
427 9
        foreach ($this->sqlParts['orderBy'] as $orderBy) {
428 9
            $orderByParts[] = $orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
429 9
        }
430
431 9
        return ' ORDER BY '.implode(', ', $orderByParts);
432
    }
433
434 57
    protected function buildOptionsPart()
435
    {
436 57
        if (!$this->sqlParts['options']) {
437 51
            return '';
438
        }
439
440 6
        $optionsParts = array();
441 6
        foreach ($this->sqlParts['options'] as $option) {
442 6
            $optionsParts[] = $option['name'].' = '.$option['value'];
443 6
        }
444
445 6
        return ' OPTION '.implode(', ', $optionsParts);
446
    }
447
448
    /**
449
     * Build FACET {expr_list} [BY {expr_list}] [ORDER BY {expr | FACET()} {ASC | DESC}] [LIMIT [offset,] count]
450
     *
451
     * @return string
452
     */
453 57
    protected function buildFacetPart()
454
    {
455 57
        if (!$this->sqlParts['facet']) {
456 54
            return '';
457
        }
458
459 3
        $facetParts = array();
460 3
        foreach ($this->sqlParts['facet'] as $facet) {
461 3
            $facetExpressions = array();
462 3
            foreach ($facet['facet'] as $key => $facetExpr) {
463 3
                if (is_int($key)) {
464 3
                    $facetExpressions[] = $facetExpr;
465 3
                } else {
466 3
                    $facetExpressions[] = $key.' AS '.$facetExpr;
467
                }
468 3
            }
469 3
            $facetPart = 'FACET '.implode(', ', $facetExpressions);
470 3
            if ($facet['by']) {
471 3
                $facetPart .= ' BY '.$facet['by'];
472 3
            }
473 3
            if ($facet['order']) {
474 3
                $facetPart .= ' ORDER BY '.$facet['order'].$this->getDirection($facet['order'], $facet['direction']);
475 3
            }
476 3
            if ($facet['limit']) {
477
                $facetPart .= ' LIMIT '.(int)$facet['skip'].', '.(int)$facet['limit'];
478
            }
479
480 3
            $facetParts[] = $facetPart;
481 3
        }
482
483 3
        return ' '.implode(' ', $facetParts);
484
    }
485
486 15
    protected function getDirection($order, $direction)
487
    {
488 15
        if (strtoupper($direction) === 'DESC') {
489 15
            return ' DESC';
490
        }
491
492 12
        if (null === $direction && strtoupper($order) === 'RAND()') {
493 3
            return '';
494
        }
495
496 12
        return ' ASC';
497
    }
498
}
499