Completed
Push — master ( 734f74...75deb4 )
by Hung
11s
created

SphinxQL::compileMatch()   D

Complexity

Conditions 9
Paths 2

Size

Total Lines 41
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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