Completed
Pull Request — master (#124)
by
unknown
04:41
created

SphinxQL   D

Complexity

Total Complexity 153

Size/Duplication

Total Lines 1432
Duplicated Lines 0.56 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 153
lcom 1
cbo 5
dl 8
loc 1432
rs 4.4102
c 0
b 0
f 0

62 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A create() 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
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 setSelect() 0 10 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 columns() 0 10 2
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 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 = null;
21
22
    /**
23
     * The last result object.
24
     *
25
     * @var array
26
     */
27
    protected $last_result = null;
28
29
    /**
30
     * The last compiled query.
31
     *
32
     * @var string
33
     */
34
    protected $last_compiled = null;
35
36
    /**
37
     * The last chosen method (select, insert, replace, update, delete).
38
     *
39
     * @var string
40
     */
41
    protected $type = null;
42
43
    /**
44
     * An SQL query that is not yet executed or "compiled"
45
     *
46
     * @var string
47
     */
48
    protected $query = null;
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 = null;
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 = null;
119
120
    /**
121
     * When not null it adds a limit
122
     *
123
     * @var null|int
124
     */
125
    protected $limit = null;
126
127
    /**
128
     * Value of INTO query for INSERT or REPLACE
129
     *
130
     * @var null|string
131
     */
132
    protected $into = null;
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 = null;
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
    public function __construct(ConnectionInterface $connection = null, $static = false)
0 ignored issues
show
Unused Code introduced by
The parameter $static 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...
218
    {
219
        $this->connection = $connection;
220
    }
221
222
    /**
223
     * Creates and setups a SphinxQL object
224
     *
225
     * @param ConnectionInterface $connection
226
     *
227
     * @return SphinxQL
228
     */
229
    public static function create(ConnectionInterface $connection)
230
    {
231
        return new static($connection);
232
    }
233
234
    /**
235
     * Returns the currently attached connection
236
     *
237
     * @returns ConnectionInterface
238
     */
239
    public function getConnection()
240
    {
241
        return $this->connection;
242
    }
243
244
    /**
245
     * Avoids having the expressions escaped
246
     *
247
     * Examples:
248
     *    $query->where('time', '>', SphinxQL::expr('CURRENT_TIMESTAMP'));
249
     *    // WHERE time > CURRENT_TIMESTAMP
250
     *
251
     * @param string $string The string to keep unaltered
252
     *
253
     * @return Expression The new Expression
254
     */
255
    public static function expr($string = '')
256
    {
257
        return new Expression($string);
258
    }
259
260
    /**
261
     * Runs the query built
262
     *
263
     * @return ResultSetInterface The result of the query
264
     */
265
    public function execute()
266
    {
267
        // pass the object so execute compiles it by itself
268
        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...
269
    }
270
271
    /**
272
     * Executes a batch of queued queries
273
     *
274
     * @return MultiResultSetInterface The array of results
275
     * @throws SphinxQLException In case no query is in queue
276
     */
277
    public function executeBatch()
278
    {
279
        if (count($this->getQueue()) == 0) {
280
            throw new SphinxQLException('There is no Queue present to execute.');
281
        }
282
283
        $queue = array();
284
285
        foreach ($this->getQueue() as $query) {
286
            $queue[] = $query->compile()->getCompiled();
287
        }
288
289
        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...
290
    }
291
292
    /**
293
     * Enqueues the current object and returns a new one or the supplied one
294
     *
295
     * @param SphinxQL|null $next
296
     *
297
     * @return SphinxQL A new SphinxQL object with the current object referenced
298
     */
299
    public function enqueue(SphinxQL $next = null)
300
    {
301
        if ($next === null) {
302
            $next = new static($this->getConnection());
303
        }
304
305
        $next->setQueuePrev($this);
306
307
        return $next;
308
    }
309
310
    /**
311
     * Returns the ordered array of enqueued objects
312
     *
313
     * @return SphinxQL[] The ordered array of enqueued objects
314
     */
315
    public function getQueue()
316
    {
317
        $queue = array();
318
        $curr = $this;
319
320
        do {
321
            if ($curr->type != null) {
322
                $queue[] = $curr;
323
            }
324
        } while ($curr = $curr->getQueuePrev());
325
326
        return array_reverse($queue);
327
    }
328
329
    /**
330
     * Gets the enqueued object
331
     *
332
     * @return SphinxQL|null
333
     */
334
    public function getQueuePrev()
335
    {
336
        return $this->queue_prev;
337
    }
338
339
    /**
340
     * Sets the reference to the enqueued object
341
     *
342
     * @param SphinxQL $query The object to set as previous
343
     *
344
     * @return SphinxQL
345
     */
346
    public function setQueuePrev($query)
347
    {
348
        $this->queue_prev = $query;
349
350
        return $this;
351
    }
352
353
    /**
354
     * Returns the result of the last query
355
     *
356
     * @return array The result of the last query
357
     */
358
    public function getResult()
359
    {
360
        return $this->last_result;
361
    }
362
363
    /**
364
     * Returns the latest compiled query
365
     *
366
     * @return string The last compiled query
367
     */
368
    public function getCompiled()
369
    {
370
        return $this->last_compiled;
371
    }
372
373
    /**
374
     * Begins transaction
375
     */
376
    public function transactionBegin()
377
    {
378
        $this->getConnection()->query('BEGIN');
379
    }
380
381
    /**
382
     * Commits transaction
383
     */
384
    public function transactionCommit()
385
    {
386
        $this->getConnection()->query('COMMIT');
387
    }
388
389
    /**
390
     * Rollbacks transaction
391
     */
392
    public function transactionRollback()
393
    {
394
        $this->getConnection()->query('ROLLBACK');
395
    }
396
397
    /**
398
     * Runs the compile function
399
     *
400
     * @return SphinxQL
401
     */
402
    public function compile()
403
    {
404
        switch ($this->type) {
405
            case 'select':
406
                $this->compileSelect();
407
                break;
408
            case 'insert':
409
            case 'replace':
410
                $this->compileInsert();
411
                break;
412
            case 'update':
413
                $this->compileUpdate();
414
                break;
415
            case 'delete':
416
                $this->compileDelete();
417
                break;
418
            case 'query':
419
                $this->compileQuery();
420
                break;
421
        }
422
423
        return $this;
424
    }
425
426
    public function compileQuery()
427
    {
428
        $this->last_compiled = $this->query;
429
430
        return $this;
431
    }
432
433
    /**
434
     * Compiles the MATCH part of the queries
435
     * Used by: SELECT, DELETE, UPDATE
436
     *
437
     * @return string The compiled MATCH
438
     */
439
    public function compileMatch()
440
    {
441
        $query = '';
442
443
        if (!empty($this->match)) {
444
            $query .= 'WHERE MATCH(';
445
446
            $matched = array();
447
448
            foreach ($this->match as $match) {
449
                $pre = '';
450
                if ($match['column'] instanceof \Closure) {
451
                    $sub = new Match($this);
452
                    call_user_func($match['column'], $sub);
453
                    $pre .= $sub->compile()->getCompiled();
454
                } elseif ($match['column'] instanceof Match) {
455
                    $pre .= $match['column']->compile()->getCompiled();
456
                } elseif (empty($match['column'])) {
457
                    $pre .= '';
458
                } elseif (is_array($match['column'])) {
459
                    $pre .= '@('.implode(',', $match['column']).') ';
460
                } else {
461
                    $pre .= '@'.$match['column'].' ';
462
                }
463
464
                if ($match['half']) {
465
                    $pre .= $this->halfEscapeMatch($match['value']);
466
                } else {
467
                    $pre .= $this->escapeMatch($match['value']);
468
                }
469
470
                if ($pre !== '') {
471
                    $matched[] = '('.$pre.')';
472
                }
473
            }
474
475
            $matched = implode(' ', $matched);
476
            $query .= $this->getConnection()->escape(trim($matched)).') ';
477
        }
478
        return $query;
479
    }
480
481
    /**
482
     * Compiles the WHERE part of the queries
483
     * It interacts with the MATCH() and of course isn't usable stand-alone
484
     * Used by: SELECT, DELETE, UPDATE
485
     *
486
     * @return string The compiled WHERE
487
     */
488
    public function compileWhere()
489
    {
490
        $query = '';
491
492
        if (empty($this->match) && !empty($this->where)) {
493
            $query .= 'WHERE ';
494
        }
495
496
        if (!empty($this->where)) {
497
            foreach ($this->where as $key => $where) {
498
                if ($key > 0 || !empty($this->match)) {
499
                    $query .= 'AND ';
500
                }
501
                $query .= $this->compileFilterCondition($where);
502
            }
503
        }
504
505
        return $query;
506
    }
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
838
     */
839
    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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 SphinxQL
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 string   $operator The operator to use
978
     * @param string   $value    The value to check against
979
     *
980
     * @return SphinxQL
981
     */
982
    public function where($column, $operator, $value = null)
983
    {
984
        if ($value === null) {
985
            $value = $operator;
986
            $operator = '=';
987
        }
988
989
        $this->where[] = array(
990
            'column' => $column,
991
            'operator' => $operator,
992
            'value' => $value
993
        );
994
995
        return $this;
996
    }
997
998
    /**
999
     * GROUP BY clause
1000
     * Adds to the previously added columns
1001
     *
1002
     * @param string $column A column to group by
1003
     *
1004
     * @return SphinxQL
1005
     */
1006
    public function groupBy($column)
1007
    {
1008
        $this->group_by[] = $column;
1009
1010
        return $this;
1011
    }
1012
1013
    /**
1014
     * GROUP N BY clause (SphinxQL-specific)
1015
     * Changes 'GROUP BY' into 'GROUP N BY'
1016
     *
1017
     * @param int $n Number of items per group
1018
     *
1019
     * @return SphinxQL
1020
     */
1021
    public function groupNBy($n)
1022
    {
1023
        $this->group_n_by = (int) $n;
1024
1025
        return $this;
1026
    }
1027
1028
    /**
1029
     * WITHIN GROUP ORDER BY clause (SphinxQL-specific)
1030
     * Adds to the previously added columns
1031
     * Works just like a classic ORDER BY
1032
     *
1033
     * @param string $column    The column to group by
1034
     * @param string $direction The group by direction (asc/desc)
1035
     *
1036
     * @return SphinxQL
1037
     */
1038
    public function withinGroupOrderBy($column, $direction = null)
1039
    {
1040
        $this->within_group_order_by[] = array('column' => $column, 'direction' => $direction);
1041
1042
        return $this;
1043
    }
1044
1045
    /**
1046
     * HAVING clause
1047
     *
1048
     * Examples:
1049
     *    $sq->having('column', 'value');
1050
     *    // HAVING column = 'value'
1051
     *
1052
     *    $sq->having('column', '=', 'value');
1053
     *    // HAVING column = 'value'
1054
     *
1055
     *    $sq->having('column', '>=', 'value')
1056
     *    // HAVING column >= 'value'
1057
     *
1058
     *    $sq->having('column', 'IN', array('value1', 'value2', 'value3'));
1059
     *    // HAVING column IN ('value1', 'value2', 'value3')
1060
     *
1061
     *    $sq->having('column', 'BETWEEN', array('value1', 'value2'))
1062
     *    // HAVING column BETWEEN 'value1' AND 'value2'
1063
     *    // HAVING example BETWEEN 10 AND 100
1064
     *
1065
     * @param string   $column   The column name
1066
     * @param string   $operator The operator to use
1067
     * @param string   $value    The value to check against
1068
     *
1069
     * @return SphinxQL The current object
1070
     */
1071
    public function having($column, $operator, $value = null)
1072
    {
1073
        if ($value === null) {
1074
            $value = $operator;
1075
            $operator = '=';
1076
        }
1077
1078
        $this->having = array(
1079
            'column' => $column,
1080
            'operator' => $operator,
1081
            'value' => $value
1082
        );
1083
1084
        return $this;
1085
    }
1086
1087
    /**
1088
     * ORDER BY clause
1089
     * Adds to the previously added columns
1090
     *
1091
     * @param string $column    The column to order on
1092
     * @param string $direction The ordering direction (asc/desc)
1093
     *
1094
     * @return SphinxQL
1095
     */
1096
    public function orderBy($column, $direction = null)
1097
    {
1098
        $this->order_by[] = array('column' => $column, 'direction' => $direction);
1099
1100
        return $this;
1101
    }
1102
1103
    /**
1104
     * LIMIT clause
1105
     * Supports also LIMIT offset, limit
1106
     *
1107
     * @param int      $offset Offset if $limit is specified, else limit
1108
     * @param null|int $limit  The limit to set, null for no limit
1109
     *
1110
     * @return SphinxQL
1111
     */
1112
    public function limit($offset, $limit = null)
1113
    {
1114
        if ($limit === null) {
1115
            $this->limit = (int) $offset;
1116
            return $this;
1117
        }
1118
1119
        $this->offset($offset);
1120
        $this->limit = (int) $limit;
1121
1122
        return $this;
1123
    }
1124
1125
    /**
1126
     * OFFSET clause
1127
     *
1128
     * @param int $offset The offset
1129
     *
1130
     * @return SphinxQL
1131
     */
1132
    public function offset($offset)
1133
    {
1134
        $this->offset = (int) $offset;
1135
1136
        return $this;
1137
    }
1138
1139
    /**
1140
     * OPTION clause (SphinxQL-specific)
1141
     * Used by: SELECT
1142
     *
1143
     * @param string $name  Option name
1144
     * @param string $value Option value
1145
     *
1146
     * @return SphinxQL
1147
     */
1148
    public function option($name, $value)
1149
    {
1150
        $this->options[] = array('name' => $name, 'value' => $value);
1151
1152
        return $this;
1153
    }
1154
1155
    /**
1156
     * INTO clause
1157
     * Used by: INSERT, REPLACE
1158
     *
1159
     * @param string $index The index to insert/replace into
1160
     *
1161
     * @return SphinxQL
1162
     */
1163
    public function into($index)
1164
    {
1165
        $this->into = $index;
1166
1167
        return $this;
1168
    }
1169
1170
    /**
1171
     * Set columns
1172
     * Used in: INSERT, REPLACE
1173
     * func_get_args()-enabled
1174
     *
1175
     * @param array $array The array of columns
1176
     *
1177
     * @return SphinxQL
1178
     */
1179
    public function columns($array = array())
1180
    {
1181
        if (is_array($array)) {
1182
            $this->columns = $array;
1183
        } else {
1184
            $this->columns = \func_get_args();
1185
        }
1186
1187
        return $this;
1188
    }
1189
1190
    /**
1191
     * Set VALUES
1192
     * Used in: INSERT, REPLACE
1193
     * func_get_args()-enabled
1194
     *
1195
     * @param array $array The array of values matching the columns from $this->columns()
1196
     *
1197
     * @return SphinxQL
1198
     */
1199
    public function values($array)
1200
    {
1201
        if (is_array($array)) {
1202
            $this->values[] = $array;
1203
        } else {
1204
            $this->values[] = \func_get_args();
1205
        }
1206
1207
        return $this;
1208
    }
1209
1210
    /**
1211
     * Set column and relative value
1212
     * Used in: INSERT, REPLACE
1213
     *
1214
     * @param string $column The column name
1215
     * @param string $value  The value
1216
     *
1217
     * @return SphinxQL
1218
     */
1219
    public function value($column, $value)
1220
    {
1221
        if ($this->type === 'insert' || $this->type === 'replace') {
1222
            $this->columns[] = $column;
1223
            $this->values[0][] = $value;
1224
        } else {
1225
            $this->set[$column] = $value;
1226
        }
1227
1228
        return $this;
1229
    }
1230
1231
    /**
1232
     * Allows passing an array with the key as column and value as value
1233
     * Used in: INSERT, REPLACE, UPDATE
1234
     *
1235
     * @param array $array Array of key-values
1236
     *
1237
     * @return SphinxQL
1238
     */
1239
    public function set($array)
1240
    {
1241
        if ($this->columns === array_keys($array)) {
1242
            $this->values($array);
1243
        } else {
1244
            foreach ($array as $key => $item) {
1245
                $this->value($key, $item);
1246
            }
1247
        }
1248
1249
        return $this;
1250
    }
1251
1252
    /**
1253
     * Allows passing an array with the key as column and value as value
1254
     * Used in: INSERT, REPLACE, UPDATE
1255
     *
1256
     * @param Facet $facet
1257
     * @return SphinxQL
1258
     */
1259
    public function facet($facet)
1260
    {
1261
        $this->facets[] = $facet;
1262
1263
        return $this;
1264
    }
1265
1266
    /**
1267
     * Sets the characters used for escapeMatch().
1268
     *
1269
     * @param array $array The array of characters to escape
1270
     *
1271
     * @return SphinxQL The escaped characters
1272
     */
1273
    public function setFullEscapeChars($array = array())
1274
    {
1275
        if (!empty($array)) {
1276
            $this->escape_full_chars = $this->compileEscapeChars($array);
1277
        }
1278
1279
        return $this;
1280
    }
1281
1282
    /**
1283
     * Sets the characters used for halfEscapeMatch().
1284
     *
1285
     * @param array $array The array of characters to escape
1286
     *
1287
     * @return SphinxQL The escaped characters
1288
     */
1289
    public function setHalfEscapeChars($array = array())
1290
    {
1291
        if (!empty($array)) {
1292
            $this->escape_half_chars = $this->compileEscapeChars($array);
1293
        }
1294
1295
        return $this;
1296
    }
1297
1298
    /**
1299
     * Compiles an array containing the characters and escaped characters into a key/value configuration.
1300
     *
1301
     * @param array $array The array of characters to escape
1302
     *
1303
     * @return array An array of the characters and it's escaped counterpart
1304
     */
1305
    public function compileEscapeChars($array = array())
1306
    {
1307
        $result = array();
1308
        foreach ($array as $character) {
1309
            $result[$character] = '\\'.$character;
1310
        }
1311
1312
        return $result;
1313
    }
1314
1315
    /**
1316
     * Escapes the query for the MATCH() function
1317
     *
1318
     * @param string $string The string to escape for the MATCH
1319
     *
1320
     * @return string The escaped string
1321
     */
1322
    public function escapeMatch($string)
1323
    {
1324
        if ($string instanceof Expression) {
1325
            return $string->value();
1326
        }
1327
1328
        return mb_strtolower(str_replace(array_keys($this->escape_full_chars), array_values($this->escape_full_chars), $string), 'utf8');
1329
    }
1330
1331
    /**
1332
     * Escapes the query for the MATCH() function
1333
     * Allows some of the control characters to pass through for use with a search field: -, |, "
1334
     * It also does some tricks to wrap/unwrap within " the string and prevents errors
1335
     *
1336
     * @param string $string The string to escape for the MATCH
1337
     *
1338
     * @return string The escaped string
1339
     */
1340
    public function halfEscapeMatch($string)
1341
    {
1342
        if ($string instanceof Expression) {
1343
            return $string->value();
1344
        }
1345
1346
        $string = str_replace(array_keys($this->escape_half_chars), array_values($this->escape_half_chars), $string);
1347
1348
        // this manages to lower the error rate by a lot
1349
        if (mb_substr_count($string, '"', 'utf8') % 2 !== 0) {
1350
            $string .= '"';
1351
        }
1352
1353
        $string = preg_replace('/-[\s-]*-/u', '-', $string);
1354
1355
        $from_to_preg = array(
1356
            '/([-|])\s*$/u'        => '\\\\\1',
1357
            '/\|[\s|]*\|/u'        => '|',
1358
            '/(\S+)-(\S+)/u'       => '\1\-\2',
1359
            '/(\S+)\s+-\s+(\S+)/u' => '\1 \- \2',
1360
        );
1361
1362
        $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string), 'utf8');
1363
1364
        return $string;
1365
    }
1366
1367
    /**
1368
     * Clears the existing query build for new query when using the same SphinxQL instance.
1369
     *
1370
     * @return SphinxQL
1371
     */
1372
    public function reset()
1373
    {
1374
        $this->query = null;
1375
        $this->select = array();
1376
        $this->from = array();
1377
        $this->where = array();
1378
        $this->match = array();
1379
        $this->group_by = array();
1380
        $this->group_n_by = null;
1381
        $this->within_group_order_by = array();
1382
        $this->having = array();
1383
        $this->order_by = array();
1384
        $this->offset = null;
1385
        $this->limit = null;
1386
        $this->into = null;
1387
        $this->columns = array();
1388
        $this->values = array();
1389
        $this->set = array();
1390
        $this->options = array();
1391
1392
        return $this;
1393
    }
1394
1395
    public function resetWhere()
1396
    {
1397
        $this->where = array();
1398
1399
        return $this;
1400
    }
1401
1402
    public function resetMatch()
1403
    {
1404
        $this->match = array();
1405
1406
        return $this;
1407
    }
1408
1409
    public function resetGroupBy()
1410
    {
1411
        $this->group_by = array();
1412
        $this->group_n_by = null;
1413
1414
        return $this;
1415
    }
1416
1417
    public function resetWithinGroupOrderBy()
1418
    {
1419
        $this->within_group_order_by = array();
1420
1421
        return $this;
1422
    }
1423
1424
    public function resetHaving()
1425
    {
1426
        $this->having = array();
1427
1428
        return $this;
1429
    }
1430
1431
    public function resetOrderBy()
1432
    {
1433
        $this->order_by = array();
1434
1435
        return $this;
1436
    }
1437
1438
    public function resetOptions()
1439
    {
1440
        $this->options = array();
1441
1442
        return $this;
1443
    }
1444
}
1445