Completed
Push — master ( 43fa01...97dfc1 )
by Konstantin
03:15
created

QueryBuilder::nameResultSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 2
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 9
    public function setParameter($parameter, $value)
217
    {
218 9
        $this->parameters[$parameter] = $value;
219
220 9
        return $this;
221
    }
222
223
    /**
224
     * Creates a new named parameter and bind the value $value to it.
225
     *
226
     * @param string $value
227
     * @param string $prefix The name to bind with.
228
     *
229
     * @return string the placeholder name used.
230
     */
231 6
    public function createParameter($value, $prefix = 'gen_')
232
    {
233 6
        $prefix = preg_replace('/[^a-z0-9_]/ui', '_', $prefix);
234 6
        $prefix .= ++$this->parametersCounter;
235 6
        $this->setParameter($prefix, $value);
236
237 6
        return ':'.$prefix;
238
    }
239
240 9
    public function getParameters()
241
    {
242 9
        return $this->parameters;
243
    }
244
245
    public function execute()
246
    {
247
        return $this->conn->executeUpdate($this->getSql(), $this->parameters);
248
    }
249
250
    public function getResult()
251
    {
252
        return $this->conn->executeQuery($this->getSql(), $this->parameters);
253
    }
254
255
    public function getMultiResult()
256
    {
257
        return $this->conn->executeMultiQuery($this->getSql(), $this->parameters, array(), $this->sqlParts['resultSetNames']);
258
    }
259
260 75
    public function getSql()
261
    {
262 75
        if (!$this->isDirty) {
263
            return $this->sql;
264
        }
265
266 75
        switch ($this->type) {
267 75
            case self::TYPE_SELECT:
268 57
                $this->sql = $this->buildSqlForSelect();
269 57
                break;
270
271 18
            case self::TYPE_INSERT:
272 18
            case self::TYPE_REPLACE:
273 9
                $this->sql = $this->buildSqlForInsert();
274 9
                break;
275
276 9
            case self::TYPE_UPDATE:
277 6
                $this->sql = $this->buildSqlForUpdate();
278 6
                break;
279
280 3
            case self::TYPE_DELETE:
281 3
                $this->sql = $this->buildSqlForDelete();
282 3
                break;
283 75
        }
284
285 75
        $this->isDirty = false;
286
287 75
        return $this->sql;
288
    }
289
290
    /**
291
     * Either appends to or replaces a single, generic query part.
292
     *
293
     * @param string $sqlPartName
294
     * @param string|array $sqlPart
295
     * @param bool $append
296
     *
297
     * @return $this This QueryBuilder instance.
298
     */
299 75
    protected function add($sqlPartName, $sqlPart, $append = false)
300
    {
301 75
        $this->isDirty = true;
302
303 75
        if (self::$multipleParts[$sqlPartName]) {
304 75
            if ($append) {
305 42
                $this->sqlParts[$sqlPartName][] = $sqlPart;
306 42
            } else {
307 75
                $this->sqlParts[$sqlPartName] = array($sqlPart);
308
            }
309 75
        } else {
310 18
            $this->sqlParts[$sqlPartName] = $sqlPart;
311
        }
312
313 75
        return $this;
314
    }
315
316 57
    protected function buildSqlForSelect()
317
    {
318 57
        $select = call_user_func_array('array_merge', $this->sqlParts['select']);
319 57
        $query = 'SELECT '.implode(', ', $select).' FROM ';
320
321 57
        $fromParts = array();
322 57
        foreach ($this->sqlParts['from'] as $from) {
323 57
            $table = $from['table'];
324 57
            if ($table instanceof static) {
325 3
                $fromParts[] = '('.$table->getSql().')';
326 3
                foreach ($table->getParameters() as $parameter => $value) {
327 3
                    $this->setParameter($parameter, $value);
328 3
                }
329 3
            } else {
330 57
                $fromParts[] = $table;
331
            }
332 57
        }
333
334 57
        $query .= implode(', ', $fromParts)
335 57
            .$this->buildWherePart()
336 57
            .$this->buildGroupByPart()
337 57
            .$this->buildOrderByPart();
338
339
        //TODO: inject limit, skip as parameters for better caching? Or just move caching to upper layer
340 57
        if ($this->sqlParts['maxResults']) {
341 6
            $query .= ' LIMIT '.(int)$this->sqlParts['firstResult'].', '.(int)$this->sqlParts['maxResults'];
342 6
        }
343
344 57
        $query .= $this->buildOptionsPart()
345 57
            .$this->buildFacetPart();
346
347 57
        return $query;
348
    }
349
350 9
    protected function buildSqlForInsert()
351
    {
352 9
        $columns = array();
353 9
        $valuesParts = array();
354 9
        foreach ($this->sqlParts['values'] as $value) {
355
            //TODO: check columns
356 9
            $columns = array_keys($value);
357 9
            $valuesParts[] = '('.implode(', ', $value).')';
358 9
        }
359
360 9
        $index = current($this->sqlParts['from'])['table'];
361 9
        $query = ($this->type === self::TYPE_REPLACE ? 'REPLACE' : 'INSERT')
362 9
            .' INTO '.$index
363 9
            .' ('.implode(', ', $columns).') VALUES '.implode(', ', $valuesParts);
364
365 9
        return $query;
366
    }
367
368 6
    protected function buildSqlForUpdate()
369
    {
370 6
        $index = current($this->sqlParts['from'])['table'];
371 6
        $setParts = array();
372 6
        foreach ($this->sqlParts['set'] as $setPart) {
373 6
            $setParts[] = $setPart['key'].' = '.$setPart['value'];
374 6
        }
375
376 6
        $query = 'UPDATE '.$index.' SET '.implode(', ', $setParts).$this->buildWherePart();
377
378 6
        return $query;
379
    }
380
381 3
    protected function buildSqlForDelete()
382
    {
383 3
        $index = current($this->sqlParts['from'])['table'];
384 3
        $query = 'DELETE FROM '.$index.$this->buildWherePart();
385
386 3
        return $query;
387
    }
388
389 66
    protected function buildWherePart()
390
    {
391 66
        if (!$this->sqlParts['where']) {
392 30
            return '';
393
        }
394
395 39
        return ' WHERE '.implode(' AND ', $this->sqlParts['where']);
396
    }
397
398 57
    protected function buildGroupByPart()
399
    {
400 57
        if (!$this->sqlParts['groupBy']) {
401 45
            return '';
402
        }
403
404 12
        $sql = ' GROUP'.($this->sqlParts['groupByLimit'] ? ' '.$this->sqlParts['groupByLimit'] : '')
405 12
            .' BY '.implode(', ', $this->sqlParts['groupBy']);
406 12
        $orderBy = $this->sqlParts['withinGroupOrderBy'];
407 12
        if ($orderBy) {
408 3
            $sql .= ' WITHIN GROUP ORDER BY '.$orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
409 3
        }
410
411 12
        return $sql;
412
    }
413
414 57
    protected function buildOrderByPart()
415
    {
416 57
        if (!$this->sqlParts['orderBy']) {
417 48
            return '';
418
        }
419
420 9
        $orderByParts = array();
421 9
        foreach ($this->sqlParts['orderBy'] as $orderBy) {
422 9
            $orderByParts[] = $orderBy['order'].$this->getDirection($orderBy['order'], $orderBy['direction']);
423 9
        }
424
425 9
        return ' ORDER BY '.implode(', ', $orderByParts);
426
    }
427
428 57
    protected function buildOptionsPart()
429
    {
430 57
        if (!$this->sqlParts['options']) {
431 51
            return '';
432
        }
433
434 6
        $optionsParts = array();
435 6
        foreach ($this->sqlParts['options'] as $option) {
436 6
            $optionsParts[] = $option['name'].' = '.$option['value'];
437 6
        }
438
439 6
        return ' OPTION '.implode(', ', $optionsParts);
440
    }
441
442
    /**
443
     * Build FACET {expr_list} [BY {expr_list}] [ORDER BY {expr | FACET()} {ASC | DESC}] [LIMIT [offset,] count]
444
     *
445
     * @return string
446
     */
447 57
    protected function buildFacetPart()
448
    {
449 57
        if (!$this->sqlParts['facet']) {
450 54
            return '';
451
        }
452
453 3
        $facetParts = array();
454 3
        foreach ($this->sqlParts['facet'] as $facet) {
455 3
            $facetExpressions = array();
456 3
            foreach ($facet['facet'] as $key => $facetExpr) {
457 3
                if (is_int($key)) {
458 3
                    $facetExpressions[] = $facetExpr;
459 3
                } else {
460 3
                    $facetExpressions[] = $key.' AS '.$facetExpr;
461
                }
462 3
            }
463 3
            $facetPart = 'FACET '.implode(', ', $facetExpressions);
464 3
            if ($facet['by']) {
465 3
                $facetPart .= ' BY '.$facet['by'];
466 3
            }
467 3
            if ($facet['order']) {
468 3
                $facetPart .= ' ORDER BY '.$facet['order'].$this->getDirection($facet['order'], $facet['direction']);
469 3
            }
470 3
            if ($facet['limit']) {
471
                $facetPart .= ' LIMIT '.(int)$facet['skip'].', '.(int)$facet['limit'];
472
            }
473
474 3
            $facetParts[] = $facetPart;
475 3
        }
476
477 3
        return ' '.implode(' ', $facetParts);
478
    }
479
480 15
    protected function getDirection($order, $direction)
481
    {
482 15
        if (strtoupper($direction) === 'DESC') {
483 15
            return ' DESC';
484
        }
485
486 12
        if (null === $direction && strtoupper($order) === 'RAND()') {
487 3
            return '';
488
        }
489
490 12
        return ' ASC';
491
    }
492
}
493