Completed
Push — master ( 7e8e79...1f43e8 )
by Hung
02:20 queued 20s
created

SphinxQL   D

Complexity

Total Complexity 152

Size/Duplication

Total Lines 1454
Duplicated Lines 1.93 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 152
lcom 1
cbo 5
dl 28
loc 1454
rs 4.4102
c 0
b 0
f 0

61 Methods

Rating   Name   Duplication   Size   Complexity  
B compileFilterCondition() 0 28 5
F compileSelect() 5 135 28
B compileInsert() 3 32 6
B compileUpdate() 0 34 5
A compileDelete() 0 17 3
A query() 0 7 1
A select() 0 13 2
A getSelect() 0 4 1
A insert() 0 7 1
A replace() 0 7 1
A update() 0 8 1
A delete() 0 7 1
B from() 0 12 5
A match() 0 10 4
A where() 0 15 2
A groupBy() 0 6 1
A groupNBy() 0 6 1
A withinGroupOrderBy() 0 6 1
A having() 0 15 2
A orderBy() 0 6 1
A limit() 0 12 2
A offset() 0 6 1
A option() 0 6 1
A into() 0 6 1
A values() 0 10 2
A value() 0 11 3
A set() 0 12 3
A facet() 0 6 1
A setFullEscapeChars() 0 8 2
A setHalfEscapeChars() 0 8 2
A compileEscapeChars() 0 9 2
A escapeMatch() 0 8 2
B halfEscapeMatch() 0 26 3
A reset() 0 22 1
A setSelect() 10 10 2
A columns() 10 10 2
A __construct() 0 4 1
A getConnection() 0 4 1
A expr() 0 4 1
A execute() 0 5 1
A executeBatch() 0 14 3
A enqueue() 0 10 2
A getQueue() 0 13 3
A getQueuePrev() 0 4 1
A setQueuePrev() 0 6 1
A getResult() 0 4 1
A getCompiled() 0 4 1
A transactionBegin() 0 4 1
A transactionCommit() 0 4 1
A transactionRollback() 0 4 1
C compile() 0 23 7
A compileQuery() 0 6 1
D compileMatch() 0 41 9
B compileWhere() 0 19 7
A resetWhere() 0 6 1
A resetMatch() 0 6 1
A resetGroupBy() 0 7 1
A resetWithinGroupOrderBy() 0 6 1
A resetHaving() 0 6 1
A resetOrderBy() 0 6 1
A resetOptions() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SphinxQL often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SphinxQL, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Foolz\SphinxQL;
4
use Foolz\SphinxQL\Drivers\ConnectionInterface;
5
use Foolz\SphinxQL\Exception\SphinxQLException;
6
use Foolz\SphinxQL\Drivers\MultiResultSetInterface;
7
use Foolz\SphinxQL\Drivers\ResultSetInterface;
8
9
/**
10
 * Query Builder class for SphinxQL statements.
11
 * @package Foolz\SphinxQL
12
 */
13
class SphinxQL
14
{
15
    /**
16
     * A non-static connection for the current SphinxQL object
17
     *
18
     * @var ConnectionInterface
19
     */
20
    protected $connection;
21
22
    /**
23
     * The last result object.
24
     *
25
     * @var array
26
     */
27
    protected $last_result;
28
29
    /**
30
     * The last compiled query.
31
     *
32
     * @var string
33
     */
34
    protected $last_compiled;
35
36
    /**
37
     * The last chosen method (select, insert, replace, update, delete).
38
     *
39
     * @var string
40
     */
41
    protected $type;
42
43
    /**
44
     * An SQL query that is not yet executed or "compiled"
45
     *
46
     * @var string
47
     */
48
    protected $query;
49
50
    /**
51
     * Array of select elements that will be comma separated.
52
     *
53
     * @var array
54
     */
55
    protected $select = array();
56
57
    /**
58
     * From in SphinxQL is the list of indexes that will be used
59
     *
60
     * @var array
61
     */
62
    protected $from = array();
63
64
    /**
65
     * The list of where and parenthesis, must be inserted in order
66
     *
67
     * @var array
68
     */
69
    protected $where = array();
70
71
    /**
72
     * The list of matches for the MATCH function in SphinxQL
73
     *
74
     * @var array
75
     */
76
    protected $match = array();
77
78
    /**
79
     * GROUP BY array to be comma separated
80
     *
81
     * @var array
82
     */
83
    protected $group_by = array();
84
85
    /**
86
     * When not null changes 'GROUP BY' to 'GROUP N BY'
87
     *
88
     * @var null|int
89
     */
90
    protected $group_n_by;
91
92
    /**
93
     * ORDER BY array
94
     *
95
     * @var array
96
     */
97
    protected $within_group_order_by = array();
98
99
    /**
100
     * The list of where and parenthesis, must be inserted in order
101
     *
102
     * @var array
103
     */
104
    protected $having = array();
105
106
    /**
107
     * ORDER BY array
108
     *
109
     * @var array
110
     */
111
    protected $order_by = array();
112
113
    /**
114
     * When not null it adds an offset
115
     *
116
     * @var null|int
117
     */
118
    protected $offset;
119
120
    /**
121
     * When not null it adds a limit
122
     *
123
     * @var null|int
124
     */
125
    protected $limit;
126
127
    /**
128
     * Value of INTO query for INSERT or REPLACE
129
     *
130
     * @var null|string
131
     */
132
    protected $into;
133
134
    /**
135
     * Array of columns for INSERT or REPLACE
136
     *
137
     * @var array
138
     */
139
    protected $columns = array();
140
141
    /**
142
     * Array OF ARRAYS of values for INSERT or REPLACE
143
     *
144
     * @var array
145
     */
146
    protected $values = array();
147
148
    /**
149
     * Array arrays containing column and value for SET in UPDATE
150
     *
151
     * @var array
152
     */
153
    protected $set = array();
154
155
    /**
156
     * Array of OPTION specific to SphinxQL
157
     *
158
     * @var array
159
     */
160
    protected $options = array();
161
162
    /**
163
     * Array of FACETs
164
     *
165
     * @var Facet[]
166
     */
167
    protected $facets = array();
168
169
    /**
170
     * The reference to the object that queued itself and created this object
171
     *
172
     * @var null|SphinxQL
173
     */
174
    protected $queue_prev;
175
176
    /**
177
     * An array of escaped characters for escapeMatch()
178
     * @var array
179
     */
180
    protected $escape_full_chars = array(
181
        '\\' => '\\\\',
182
        '(' => '\(',
183
        ')' => '\)',
184
        '|' => '\|',
185
        '-' => '\-',
186
        '!' => '\!',
187
        '@' => '\@',
188
        '~' => '\~',
189
        '"' => '\"',
190
        '&' => '\&',
191
        '/' => '\/',
192
        '^' => '\^',
193
        '$' => '\$',
194
        '=' => '\=',
195
        '<' => '\<',
196
    );
197
198
    /**
199
     * An array of escaped characters for fullEscapeMatch()
200
     * @var array
201
     */
202
    protected $escape_half_chars = array(
203
        '\\' => '\\\\',
204
        '(' => '\(',
205
        ')' => '\)',
206
        '!' => '\!',
207
        '@' => '\@',
208
        '~' => '\~',
209
        '&' => '\&',
210
        '/' => '\/',
211
        '^' => '\^',
212
        '$' => '\$',
213
        '=' => '\=',
214
        '<' => '\<',
215
    );
216
217
    /**
218
     * @param ConnectionInterface|null $connection
219
     */
220
    public function __construct(ConnectionInterface $connection = null)
221
    {
222
        $this->connection = $connection;
223
    }
224
225
    /**
226
     * Returns the currently attached connection
227
     *
228
     * @returns ConnectionInterface
229
     */
230
    public function getConnection()
231
    {
232
        return $this->connection;
233
    }
234
235
    /**
236
     * Avoids having the expressions escaped
237
     *
238
     * Examples:
239
     *    $query->where('time', '>', SphinxQL::expr('CURRENT_TIMESTAMP'));
240
     *    // WHERE time > CURRENT_TIMESTAMP
241
     *
242
     * @param string $string The string to keep unaltered
243
     *
244
     * @return Expression The new Expression
245
     * @todo make non static
246
     */
247
    public static function expr($string = '')
248
    {
249
        return new Expression($string);
250
    }
251
252
    /**
253
     * Runs the query built
254
     *
255
     * @return ResultSetInterface The result of the query
256
     */
257
    public function execute()
258
    {
259
        // pass the object so execute compiles it by itself
260
        return $this->last_result = $this->getConnection()->query($this->compile()->getCompiled());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getConnection()->...mpile()->getCompiled()) of type object<Foolz\SphinxQL\Drivers\ResultSetInterface> is incompatible with the declared type array of property $last_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
261
    }
262
263
    /**
264
     * Executes a batch of queued queries
265
     *
266
     * @return MultiResultSetInterface The array of results
267
     * @throws SphinxQLException In case no query is in queue
268
     */
269
    public function executeBatch()
270
    {
271
        if (count($this->getQueue()) == 0) {
272
            throw new SphinxQLException('There is no Queue present to execute.');
273
        }
274
275
        $queue = array();
276
277
        foreach ($this->getQueue() as $query) {
278
            $queue[] = $query->compile()->getCompiled();
279
        }
280
281
        return $this->last_result = $this->getConnection()->multiQuery($queue);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getConnection()->multiQuery($queue) of type object<Foolz\SphinxQL\Dr...ultiResultSetInterface> is incompatible with the declared type array of property $last_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
282
    }
283
284
    /**
285
     * Enqueues the current object and returns a new one or the supplied one
286
     *
287
     * @param SphinxQL|null $next
288
     *
289
     * @return SphinxQL A new SphinxQL object with the current object referenced
290
     */
291
    public function enqueue(SphinxQL $next = null)
292
    {
293
        if ($next === null) {
294
            $next = new static($this->getConnection());
295
        }
296
297
        $next->setQueuePrev($this);
298
299
        return $next;
300
    }
301
302
    /**
303
     * Returns the ordered array of enqueued objects
304
     *
305
     * @return SphinxQL[] The ordered array of enqueued objects
306
     */
307
    public function getQueue()
308
    {
309
        $queue = array();
310
        $curr = $this;
311
312
        do {
313
            if ($curr->type != null) {
314
                $queue[] = $curr;
315
            }
316
        } while ($curr = $curr->getQueuePrev());
317
318
        return array_reverse($queue);
319
    }
320
321
    /**
322
     * Gets the enqueued object
323
     *
324
     * @return SphinxQL|null
325
     */
326
    public function getQueuePrev()
327
    {
328
        return $this->queue_prev;
329
    }
330
331
    /**
332
     * Sets the reference to the enqueued object
333
     *
334
     * @param SphinxQL $query The object to set as previous
335
     *
336
     * @return $this
337
     */
338
    public function setQueuePrev($query)
339
    {
340
        $this->queue_prev = $query;
341
342
        return $this;
343
    }
344
345
    /**
346
     * Returns the result of the last query
347
     *
348
     * @return array The result of the last query
349
     */
350
    public function getResult()
351
    {
352
        return $this->last_result;
353
    }
354
355
    /**
356
     * Returns the latest compiled query
357
     *
358
     * @return string The last compiled query
359
     */
360
    public function getCompiled()
361
    {
362
        return $this->last_compiled;
363
    }
364
365
    /**
366
     * Begins transaction
367
     */
368
    public function transactionBegin()
369
    {
370
        $this->getConnection()->query('BEGIN');
371
    }
372
373
    /**
374
     * Commits transaction
375
     */
376
    public function transactionCommit()
377
    {
378
        $this->getConnection()->query('COMMIT');
379
    }
380
381
    /**
382
     * Rollbacks transaction
383
     */
384
    public function transactionRollback()
385
    {
386
        $this->getConnection()->query('ROLLBACK');
387
    }
388
389
    /**
390
     * Runs the compile function
391
     *
392
     * @return $this
393
     */
394
    public function compile()
395
    {
396
        switch ($this->type) {
397
            case 'select':
398
                $this->compileSelect();
399
                break;
400
            case 'insert':
401
            case 'replace':
402
                $this->compileInsert();
403
                break;
404
            case 'update':
405
                $this->compileUpdate();
406
                break;
407
            case 'delete':
408
                $this->compileDelete();
409
                break;
410
            case 'query':
411
                $this->compileQuery();
412
                break;
413
        }
414
415
        return $this;
416
    }
417
418
    /**
419
     * @return $this
420
     */
421
    public function compileQuery()
422
    {
423
        $this->last_compiled = $this->query;
424
425
        return $this;
426
    }
427
428
    /**
429
     * Compiles the MATCH part of the queries
430
     * Used by: SELECT, DELETE, UPDATE
431
     *
432
     * @return string The compiled MATCH
433
     */
434
    public function compileMatch()
435
    {
436
        $query = '';
437
438
        if (!empty($this->match)) {
439
            $query .= 'WHERE MATCH(';
440
441
            $matched = array();
442
443
            foreach ($this->match as $match) {
444
                $pre = '';
445
                if ($match['column'] instanceof \Closure) {
446
                    $sub = new Match($this);
447
                    call_user_func($match['column'], $sub);
448
                    $pre .= $sub->compile()->getCompiled();
449
                } elseif ($match['column'] instanceof Match) {
450
                    $pre .= $match['column']->compile()->getCompiled();
451
                } elseif (empty($match['column'])) {
452
                    $pre .= '';
453
                } elseif (is_array($match['column'])) {
454
                    $pre .= '@('.implode(',', $match['column']).') ';
455
                } else {
456
                    $pre .= '@'.$match['column'].' ';
457
                }
458
459
                if ($match['half']) {
460
                    $pre .= $this->halfEscapeMatch($match['value']);
461
                } else {
462
                    $pre .= $this->escapeMatch($match['value']);
463
                }
464
465
                if ($pre !== '') {
466
                    $matched[] = '('.$pre.')';
467
                }
468
            }
469
470
            $matched = implode(' ', $matched);
471
            $query .= $this->getConnection()->escape(trim($matched)).') ';
472
        }
473
        return $query;
474
    }
475
476
    /**
477
     * Compiles the WHERE part of the queries
478
     * It interacts with the MATCH() and of course isn't usable stand-alone
479
     * Used by: SELECT, DELETE, UPDATE
480
     *
481
     * @return string The compiled WHERE
482
     */
483
    public function compileWhere()
484
    {
485
        $query = '';
486
487
        if (empty($this->match) && !empty($this->where)) {
488
            $query .= 'WHERE ';
489
        }
490
491
        if (!empty($this->where)) {
492
            foreach ($this->where as $key => $where) {
493
                if ($key > 0 || !empty($this->match)) {
494
                    $query .= 'AND ';
495
                }
496
                $query .= $this->compileFilterCondition($where);
497
            }
498
        }
499
500
        return $query;
501
    }
502
503
    /**
504
     * @param array $filter
505
     *
506
     * @return string
507
     */
508
    public function compileFilterCondition($filter)
509
    {
510
        $query = '';
511
512
        if (!empty($filter)) {
513
            if (strtoupper($filter['operator']) === 'BETWEEN') {
514
                $query .= $filter['column'];
515
                $query .= ' BETWEEN ';
516
                $query .= $this->getConnection()->quote($filter['value'][0]).' AND '
517
                    .$this->getConnection()->quote($filter['value'][1]).' ';
518
            } else {
519
                // id can't be quoted!
520
                if ($filter['column'] === 'id') {
521
                    $query .= 'id ';
522
                } else {
523
                    $query .= $filter['column'].' ';
524
                }
525
526
                if (in_array(strtoupper($filter['operator']), array('IN', 'NOT IN'), true)) {
527
                    $query .= strtoupper($filter['operator']).' ('.implode(', ', $this->getConnection()->quoteArr($filter['value'])).') ';
528
                } else {
529
                    $query .= $filter['operator'].' '.$this->getConnection()->quote($filter['value']).' ';
530
                }
531
            }
532
        }
533
534
        return $query;
535
    }
536
537
    /**
538
     * Compiles the statements for SELECT
539
     *
540
     * @return $this
541
     */
542
    public function compileSelect()
543
    {
544
        $query = '';
545
546
        if ($this->type == 'select') {
547
            $query .= 'SELECT ';
548
549 View Code Duplication
            if (!empty($this->select)) {
550
                $query .= implode(', ', $this->select).' ';
551
            } else {
552
                $query .= '* ';
553
            }
554
        }
555
556
        if (!empty($this->from)) {
557
            if ($this->from instanceof \Closure) {
558
                $sub = new static($this->getConnection());
559
                call_user_func($this->from, $sub);
560
                $query .= 'FROM ('.$sub->compile()->getCompiled().') ';
561
            } elseif ($this->from instanceof SphinxQL) {
562
                $query .= 'FROM ('.$this->from->compile()->getCompiled().') ';
563
            } else {
564
                $query .= 'FROM '.implode(', ', $this->from).' ';
565
            }
566
        }
567
568
        $query .= $this->compileMatch().$this->compileWhere();
569
570
        if (!empty($this->group_by)) {
571
            $query .= 'GROUP ';
572
            if ($this->group_n_by !== null) {
573
                $query .= $this->group_n_by.' ';
574
            }
575
            $query .= 'BY '.implode(', ', $this->group_by).' ';
576
        }
577
578
        if (!empty($this->within_group_order_by)) {
579
            $query .= 'WITHIN GROUP ORDER BY ';
580
581
            $order_arr = array();
582
583
            foreach ($this->within_group_order_by as $order) {
584
                $order_sub = $order['column'].' ';
585
586
                if ($order['direction'] !== null) {
587
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
588
                }
589
590
                $order_arr[] = $order_sub;
591
            }
592
593
            $query .= implode(', ', $order_arr).' ';
594
        }
595
596
        if (!empty($this->having)) {
597
            $query .= 'HAVING '.$this->compileFilterCondition($this->having);
598
        }
599
600
        if (!empty($this->order_by)) {
601
            $query .= 'ORDER BY ';
602
603
            $order_arr = array();
604
605
            foreach ($this->order_by as $order) {
606
                $order_sub = $order['column'].' ';
607
608
                if ($order['direction'] !== null) {
609
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
610
                }
611
612
                $order_arr[] = $order_sub;
613
            }
614
615
            $query .= implode(', ', $order_arr).' ';
616
        }
617
618
        if ($this->limit !== null || $this->offset !== null) {
619
            if ($this->offset === null) {
620
                $this->offset = 0;
621
            }
622
623
            if ($this->limit === null) {
624
                $this->limit = 9999999999999;
625
            }
626
627
            $query .= 'LIMIT '.((int) $this->offset).', '.((int) $this->limit).' ';
628
        }
629
630
        if (!empty($this->options)) {
631
            $options = array();
632
633
            foreach ($this->options as $option) {
634
                if ($option['value'] instanceof Expression) {
635
                    $option['value'] = $option['value']->value();
636
                } elseif (is_array($option['value'])) {
637
                    array_walk(
638
                        $option['value'],
639
                        function (&$val, $key) {
640
                            $val = $key.'='.$val;
641
                        }
642
                    );
643
                    $option['value'] = '('.implode(', ', $option['value']).')';
644
                } else {
645
                    $option['value'] = $this->getConnection()->quote($option['value']);
646
                }
647
648
                $options[] = $option['name'].' = '.$option['value'];
649
            }
650
651
            $query .= 'OPTION '.implode(', ', $options).' ';
652
        }
653
654
        if (!empty($this->facets)) {
655
            $facets = array();
656
657
            foreach ($this->facets as $facet) {
658
                // dynamically set the own SphinxQL connection if the Facet doesn't own one
659
                if ($facet->getConnection() === null) {
660
                    $facet->setConnection($this->getConnection());
661
                    $facets[] = $facet->getFacet();
662
                    // go back to the status quo for reuse
663
                    $facet->setConnection();
664
                } else {
665
                    $facets[] = $facet->getFacet();
666
                }
667
            }
668
669
            $query .= implode(' ', $facets);
670
        }
671
672
        $query = trim($query);
673
        $this->last_compiled = $query;
674
675
        return $this;
676
    }
677
678
    /**
679
     * Compiles the statements for INSERT or REPLACE
680
     *
681
     * @return $this
682
     */
683
    public function compileInsert()
684
    {
685
        if ($this->type == 'insert') {
686
            $query = 'INSERT ';
687
        } else {
688
            $query = 'REPLACE ';
689
        }
690
691
        if ($this->into !== null) {
692
            $query .= 'INTO '.$this->into.' ';
693
        }
694
695 View Code Duplication
        if (!empty($this->columns)) {
696
            $query .= '('.implode(', ', $this->columns).') ';
697
        }
698
699
        if (!empty($this->values)) {
700
            $query .= 'VALUES ';
701
            $query_sub = array();
702
703
            foreach ($this->values as $value) {
704
                $query_sub[] = '('.implode(', ', $this->getConnection()->quoteArr($value)).')';
705
            }
706
707
            $query .= implode(', ', $query_sub);
708
        }
709
710
        $query = trim($query);
711
        $this->last_compiled = $query;
712
713
        return $this;
714
    }
715
716
    /**
717
     * Compiles the statements for UPDATE
718
     *
719
     * @return $this
720
     */
721
    public function compileUpdate()
722
    {
723
        $query = 'UPDATE ';
724
725
        if ($this->into !== null) {
726
            $query .= $this->into.' ';
727
        }
728
729
        if (!empty($this->set)) {
730
            $query .= 'SET ';
731
732
            $query_sub = array();
733
734
            foreach ($this->set as $column => $value) {
735
                // MVA support
736
                if (is_array($value)) {
737
                    $query_sub[] = $column
738
                        .' = ('.implode(', ', $this->getConnection()->quoteArr($value)).')';
739
                } else {
740
                    $query_sub[] = $column
741
                        .' = '.$this->getConnection()->quote($value);
742
                }
743
            }
744
745
            $query .= implode(', ', $query_sub).' ';
746
        }
747
748
        $query .= $this->compileMatch().$this->compileWhere();
749
750
        $query = trim($query);
751
        $this->last_compiled = $query;
752
753
        return $this;
754
    }
755
756
    /**
757
     * Compiles the statements for DELETE
758
     *
759
     * @return $this
760
     */
761
    public function compileDelete()
762
    {
763
        $query = 'DELETE ';
764
765
        if (!empty($this->from)) {
766
            $query .= 'FROM '.$this->from[0].' ';
767
        }
768
769
        if (!empty($this->where)) {
770
            $query .= $this->compileWhere();
771
        }
772
773
        $query = trim($query);
774
        $this->last_compiled = $query;
775
776
        return $this;
777
    }
778
779
    /**
780
     * Sets a query to be executed
781
     *
782
     * @param string $sql A SphinxQL query to execute
783
     *
784
     * @return $this
785
     */
786
    public function query($sql)
787
    {
788
        $this->type = 'query';
789
        $this->query = $sql;
790
791
        return $this;
792
    }
793
794
    /**
795
     * Select the columns
796
     *
797
     * Gets the arguments passed as $sphinxql->select('one', 'two')
798
     * Using it without arguments equals to having '*' as argument
799
     * Using it with array maps values as column names
800
     *
801
     * Examples:
802
     *    $query->select('title');
803
     *    // SELECT title
804
     *
805
     *    $query->select('title', 'author', 'date');
806
     *    // SELECT title, author, date
807
     *
808
     *    $query->select(['id', 'title']);
809
     *    // SELECT id, title
810
     *
811
     * @param array|string $columns Array or multiple string arguments containing column names
812
     *
813
     * @return $this
814
     */
815
    public function select($columns = null)
816
    {
817
        $this->reset();
818
        $this->type = 'select';
819
820
        if (is_array($columns)) {
821
            $this->select = $columns;
822
        } else {
823
            $this->select = \func_get_args();
824
        }
825
826
        return $this;
827
    }
828
829
    /**
830
     * Alters which arguments to select
831
     *
832
     * Query is assumed to be in SELECT mode
833
     * See select() for usage
834
     *
835
     * @param array|string $columns Array or multiple string arguments containing column names
836
     *
837
     * @return $this
838
     */
839 View Code Duplication
    public function setSelect($columns = null)
840
    {
841
        if (is_array($columns)) {
842
            $this->select = $columns;
843
        } else {
844
            $this->select = \func_get_args();
845
        }
846
847
        return $this;
848
    }
849
850
    /**
851
     * Get the columns staged to select
852
     *
853
     * @return array
854
     */
855
    public function getSelect()
856
    {
857
        return $this->select;
858
    }
859
860
    /**
861
     * Activates the INSERT mode
862
     *
863
     * @return $this
864
     */
865
    public function insert()
866
    {
867
        $this->reset();
868
        $this->type = 'insert';
869
870
        return $this;
871
    }
872
873
    /**
874
     * Activates the REPLACE mode
875
     *
876
     * @return $this
877
     */
878
    public function replace()
879
    {
880
        $this->reset();
881
        $this->type = 'replace';
882
883
        return $this;
884
    }
885
886
    /**
887
     * Activates the UPDATE mode
888
     *
889
     * @param string $index The index to update into
890
     *
891
     * @return $this
892
     */
893
    public function update($index)
894
    {
895
        $this->reset();
896
        $this->type = 'update';
897
        $this->into($index);
898
899
        return $this;
900
    }
901
902
    /**
903
     * Activates the DELETE mode
904
     *
905
     * @return $this
906
     */
907
    public function delete()
908
    {
909
        $this->reset();
910
        $this->type = 'delete';
911
912
        return $this;
913
    }
914
915
    /**
916
     * FROM clause (Sphinx-specific since it works with multiple indexes)
917
     * func_get_args()-enabled
918
     *
919
     * @param array $array An array of indexes to use
920
     *
921
     * @return $this
922
     */
923
    public function from($array = null)
924
    {
925
        if (is_string($array)) {
926
            $this->from = \func_get_args();
927
        }
928
929
        if (is_array($array) || $array instanceof \Closure || $array instanceof SphinxQL) {
930
            $this->from = $array;
931
        }
932
933
        return $this;
934
    }
935
936
    /**
937
     * MATCH clause (Sphinx-specific)
938
     *
939
     * @param mixed    $column The column name (can be array, string, Closure, or Match)
940
     * @param string   $value  The value
941
     * @param boolean  $half  Exclude ", |, - control characters from being escaped
942
     *
943
     * @return $this
944
     */
945
    public function match($column, $value = null, $half = false)
946
    {
947
        if ($column === '*' || (is_array($column) && in_array('*', $column))) {
948
            $column = array();
949
        }
950
951
        $this->match[] = array('column' => $column, 'value' => $value, 'half' => $half);
952
953
        return $this;
954
    }
955
956
    /**
957
     * WHERE clause
958
     *
959
     * Examples:
960
     *    $query->where('column', 'value');
961
     *    // WHERE column = 'value'
962
     *
963
     *    $query->where('column', '=', 'value');
964
     *    // WHERE column = 'value'
965
     *
966
     *    $query->where('column', '>=', 'value')
967
     *    // WHERE column >= 'value'
968
     *
969
     *    $query->where('column', 'IN', array('value1', 'value2', 'value3'));
970
     *    // WHERE column IN ('value1', 'value2', 'value3')
971
     *
972
     *    $query->where('column', 'BETWEEN', array('value1', 'value2'))
973
     *    // WHERE column BETWEEN 'value1' AND 'value2'
974
     *    // WHERE example BETWEEN 10 AND 100
975
     *
976
     * @param string   $column   The column name
977
     * @param Expression|string|null|bool|array|int|float $operator The operator to use (if value is not null, you can
978
     *      use only string)
979
     * @param Expression|string|null|bool|array|int|float $value The value to check against
980
     *
981
     * @return $this
982
     */
983
    public function where($column, $operator, $value = null)
984
    {
985
        if ($value === null) {
986
            $value = $operator;
987
            $operator = '=';
988
        }
989
990
        $this->where[] = array(
991
            'column' => $column,
992
            'operator' => $operator,
993
            'value' => $value
994
        );
995
996
        return $this;
997
    }
998
999
    /**
1000
     * GROUP BY clause
1001
     * Adds to the previously added columns
1002
     *
1003
     * @param string $column A column to group by
1004
     *
1005
     * @return $this
1006
     */
1007
    public function groupBy($column)
1008
    {
1009
        $this->group_by[] = $column;
1010
1011
        return $this;
1012
    }
1013
1014
    /**
1015
     * GROUP N BY clause (SphinxQL-specific)
1016
     * Changes 'GROUP BY' into 'GROUP N BY'
1017
     *
1018
     * @param int $n Number of items per group
1019
     *
1020
     * @return $this
1021
     */
1022
    public function groupNBy($n)
1023
    {
1024
        $this->group_n_by = (int) $n;
1025
1026
        return $this;
1027
    }
1028
1029
    /**
1030
     * WITHIN GROUP ORDER BY clause (SphinxQL-specific)
1031
     * Adds to the previously added columns
1032
     * Works just like a classic ORDER BY
1033
     *
1034
     * @param string $column    The column to group by
1035
     * @param string $direction The group by direction (asc/desc)
1036
     *
1037
     * @return $this
1038
     */
1039
    public function withinGroupOrderBy($column, $direction = null)
1040
    {
1041
        $this->within_group_order_by[] = array('column' => $column, 'direction' => $direction);
1042
1043
        return $this;
1044
    }
1045
1046
    /**
1047
     * HAVING clause
1048
     *
1049
     * Examples:
1050
     *    $sq->having('column', 'value');
1051
     *    // HAVING column = 'value'
1052
     *
1053
     *    $sq->having('column', '=', 'value');
1054
     *    // HAVING column = 'value'
1055
     *
1056
     *    $sq->having('column', '>=', 'value')
1057
     *    // HAVING column >= 'value'
1058
     *
1059
     *    $sq->having('column', 'IN', array('value1', 'value2', 'value3'));
1060
     *    // HAVING column IN ('value1', 'value2', 'value3')
1061
     *
1062
     *    $sq->having('column', 'BETWEEN', array('value1', 'value2'))
1063
     *    // HAVING column BETWEEN 'value1' AND 'value2'
1064
     *    // HAVING example BETWEEN 10 AND 100
1065
     *
1066
     * @param string   $column   The column name
1067
     * @param string   $operator The operator to use
1068
     * @param string   $value    The value to check against
1069
     *
1070
     * @return $this
1071
     */
1072
    public function having($column, $operator, $value = null)
1073
    {
1074
        if ($value === null) {
1075
            $value = $operator;
1076
            $operator = '=';
1077
        }
1078
1079
        $this->having = array(
1080
            'column' => $column,
1081
            'operator' => $operator,
1082
            'value' => $value
1083
        );
1084
1085
        return $this;
1086
    }
1087
1088
    /**
1089
     * ORDER BY clause
1090
     * Adds to the previously added columns
1091
     *
1092
     * @param string $column    The column to order on
1093
     * @param string $direction The ordering direction (asc/desc)
1094
     *
1095
     * @return $this
1096
     */
1097
    public function orderBy($column, $direction = null)
1098
    {
1099
        $this->order_by[] = array('column' => $column, 'direction' => $direction);
1100
1101
        return $this;
1102
    }
1103
1104
    /**
1105
     * LIMIT clause
1106
     * Supports also LIMIT offset, limit
1107
     *
1108
     * @param int      $offset Offset if $limit is specified, else limit
1109
     * @param null|int $limit  The limit to set, null for no limit
1110
     *
1111
     * @return $this
1112
     */
1113
    public function limit($offset, $limit = null)
1114
    {
1115
        if ($limit === null) {
1116
            $this->limit = (int) $offset;
1117
            return $this;
1118
        }
1119
1120
        $this->offset($offset);
1121
        $this->limit = (int) $limit;
1122
1123
        return $this;
1124
    }
1125
1126
    /**
1127
     * OFFSET clause
1128
     *
1129
     * @param int $offset The offset
1130
     *
1131
     * @return $this
1132
     */
1133
    public function offset($offset)
1134
    {
1135
        $this->offset = (int) $offset;
1136
1137
        return $this;
1138
    }
1139
1140
    /**
1141
     * OPTION clause (SphinxQL-specific)
1142
     * Used by: SELECT
1143
     *
1144
     * @param string $name  Option name
1145
     * @param Expression|array|string|int|bool|float|null $value Option value
1146
     *
1147
     * @return $this
1148
     */
1149
    public function option($name, $value)
1150
    {
1151
        $this->options[] = array('name' => $name, 'value' => $value);
1152
1153
        return $this;
1154
    }
1155
1156
    /**
1157
     * INTO clause
1158
     * Used by: INSERT, REPLACE
1159
     *
1160
     * @param string $index The index to insert/replace into
1161
     *
1162
     * @return $this
1163
     */
1164
    public function into($index)
1165
    {
1166
        $this->into = $index;
1167
1168
        return $this;
1169
    }
1170
1171
    /**
1172
     * Set columns
1173
     * Used in: INSERT, REPLACE
1174
     * func_get_args()-enabled
1175
     *
1176
     * @param array $array The array of columns
1177
     *
1178
     * @return $this
1179
     */
1180 View Code Duplication
    public function columns($array = array())
1181
    {
1182
        if (is_array($array)) {
1183
            $this->columns = $array;
1184
        } else {
1185
            $this->columns = \func_get_args();
1186
        }
1187
1188
        return $this;
1189
    }
1190
1191
    /**
1192
     * Set VALUES
1193
     * Used in: INSERT, REPLACE
1194
     * func_get_args()-enabled
1195
     *
1196
     * @param array $array The array of values matching the columns from $this->columns()
1197
     *
1198
     * @return $this
1199
     */
1200
    public function values($array)
1201
    {
1202
        if (is_array($array)) {
1203
            $this->values[] = $array;
1204
        } else {
1205
            $this->values[] = \func_get_args();
1206
        }
1207
1208
        return $this;
1209
    }
1210
1211
    /**
1212
     * Set column and relative value
1213
     * Used in: INSERT, REPLACE
1214
     *
1215
     * @param string $column The column name
1216
     * @param string $value  The value
1217
     *
1218
     * @return $this
1219
     */
1220
    public function value($column, $value)
1221
    {
1222
        if ($this->type === 'insert' || $this->type === 'replace') {
1223
            $this->columns[] = $column;
1224
            $this->values[0][] = $value;
1225
        } else {
1226
            $this->set[$column] = $value;
1227
        }
1228
1229
        return $this;
1230
    }
1231
1232
    /**
1233
     * Allows passing an array with the key as column and value as value
1234
     * Used in: INSERT, REPLACE, UPDATE
1235
     *
1236
     * @param array $array Array of key-values
1237
     *
1238
     * @return $this
1239
     */
1240
    public function set($array)
1241
    {
1242
        if ($this->columns === array_keys($array)) {
1243
            $this->values($array);
1244
        } else {
1245
            foreach ($array as $key => $item) {
1246
                $this->value($key, $item);
1247
            }
1248
        }
1249
1250
        return $this;
1251
    }
1252
1253
    /**
1254
     * Allows passing an array with the key as column and value as value
1255
     * Used in: INSERT, REPLACE, UPDATE
1256
     *
1257
     * @param Facet $facet
1258
     * @return $this
1259
     */
1260
    public function facet($facet)
1261
    {
1262
        $this->facets[] = $facet;
1263
1264
        return $this;
1265
    }
1266
1267
    /**
1268
     * Sets the characters used for escapeMatch().
1269
     *
1270
     * @param array $array The array of characters to escape
1271
     *
1272
     * @return $this
1273
     */
1274
    public function setFullEscapeChars($array = array())
1275
    {
1276
        if (!empty($array)) {
1277
            $this->escape_full_chars = $this->compileEscapeChars($array);
1278
        }
1279
1280
        return $this;
1281
    }
1282
1283
    /**
1284
     * Sets the characters used for halfEscapeMatch().
1285
     *
1286
     * @param array $array The array of characters to escape
1287
     *
1288
     * @return $this
1289
     */
1290
    public function setHalfEscapeChars($array = array())
1291
    {
1292
        if (!empty($array)) {
1293
            $this->escape_half_chars = $this->compileEscapeChars($array);
1294
        }
1295
1296
        return $this;
1297
    }
1298
1299
    /**
1300
     * Compiles an array containing the characters and escaped characters into a key/value configuration.
1301
     *
1302
     * @param array $array The array of characters to escape
1303
     *
1304
     * @return array An array of the characters and it's escaped counterpart
1305
     */
1306
    public function compileEscapeChars($array = array())
1307
    {
1308
        $result = array();
1309
        foreach ($array as $character) {
1310
            $result[$character] = '\\'.$character;
1311
        }
1312
1313
        return $result;
1314
    }
1315
1316
    /**
1317
     * Escapes the query for the MATCH() function
1318
     *
1319
     * @param string $string The string to escape for the MATCH
1320
     *
1321
     * @return string The escaped string
1322
     */
1323
    public function escapeMatch($string)
1324
    {
1325
        if ($string instanceof Expression) {
1326
            return $string->value();
1327
        }
1328
1329
        return mb_strtolower(str_replace(array_keys($this->escape_full_chars), array_values($this->escape_full_chars), $string), 'utf8');
1330
    }
1331
1332
    /**
1333
     * Escapes the query for the MATCH() function
1334
     * Allows some of the control characters to pass through for use with a search field: -, |, "
1335
     * It also does some tricks to wrap/unwrap within " the string and prevents errors
1336
     *
1337
     * @param string $string The string to escape for the MATCH
1338
     *
1339
     * @return string The escaped string
1340
     */
1341
    public function halfEscapeMatch($string)
1342
    {
1343
        if ($string instanceof Expression) {
1344
            return $string->value();
1345
        }
1346
1347
        $string = str_replace(array_keys($this->escape_half_chars), array_values($this->escape_half_chars), $string);
1348
1349
        // this manages to lower the error rate by a lot
1350
        if (mb_substr_count($string, '"', 'utf8') % 2 !== 0) {
1351
            $string .= '"';
1352
        }
1353
1354
        $string = preg_replace('/-[\s-]*-/u', '-', $string);
1355
1356
        $from_to_preg = array(
1357
            '/([-|])\s*$/u'        => '\\\\\1',
1358
            '/\|[\s|]*\|/u'        => '|',
1359
            '/(\S+)-(\S+)/u'       => '\1\-\2',
1360
            '/(\S+)\s+-\s+(\S+)/u' => '\1 \- \2',
1361
        );
1362
1363
        $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string), 'utf8');
1364
1365
        return $string;
1366
    }
1367
1368
    /**
1369
     * Clears the existing query build for new query when using the same SphinxQL instance.
1370
     *
1371
     * @return $this
1372
     */
1373
    public function reset()
1374
    {
1375
        $this->query = null;
1376
        $this->select = array();
1377
        $this->from = array();
1378
        $this->where = array();
1379
        $this->match = array();
1380
        $this->group_by = array();
1381
        $this->group_n_by = null;
1382
        $this->within_group_order_by = array();
1383
        $this->having = array();
1384
        $this->order_by = array();
1385
        $this->offset = null;
1386
        $this->limit = null;
1387
        $this->into = null;
1388
        $this->columns = array();
1389
        $this->values = array();
1390
        $this->set = array();
1391
        $this->options = array();
1392
1393
        return $this;
1394
    }
1395
1396
    /**
1397
     * @return $this
1398
     */
1399
    public function resetWhere()
1400
    {
1401
        $this->where = array();
1402
1403
        return $this;
1404
    }
1405
1406
    /**
1407
     * @return $this
1408
     */
1409
    public function resetMatch()
1410
    {
1411
        $this->match = array();
1412
1413
        return $this;
1414
    }
1415
1416
    /**
1417
     * @return $this
1418
     */
1419
    public function resetGroupBy()
1420
    {
1421
        $this->group_by = array();
1422
        $this->group_n_by = null;
1423
1424
        return $this;
1425
    }
1426
1427
    /**
1428
     * @return $this
1429
     */
1430
    public function resetWithinGroupOrderBy()
1431
    {
1432
        $this->within_group_order_by = array();
1433
1434
        return $this;
1435
    }
1436
1437
    /**
1438
     * @return $this
1439
     */
1440
    public function resetHaving()
1441
    {
1442
        $this->having = array();
1443
1444
        return $this;
1445
    }
1446
1447
    /**
1448
     * @return $this
1449
     */
1450
    public function resetOrderBy()
1451
    {
1452
        $this->order_by = array();
1453
1454
        return $this;
1455
    }
1456
1457
    /**
1458
     * @return $this
1459
     */
1460
    public function resetOptions()
1461
    {
1462
        $this->options = array();
1463
1464
        return $this;
1465
    }
1466
}
1467