Passed
Pull Request — master (#173)
by Sergey
04:07
created

SphinxQL::matchAndMatch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Foolz\SphinxQL;
4
5
use Foolz\SphinxQL\Drivers\ConnectionInterface;
6
use Foolz\SphinxQL\Drivers\MultiResultSetInterface;
7
use Foolz\SphinxQL\Drivers\ResultSetInterface;
8
use Foolz\SphinxQL\Exception\ConnectionException;
9
use Foolz\SphinxQL\Exception\DatabaseException;
10
use Foolz\SphinxQL\Exception\SphinxQLException;
11
12
/**
13
 * Query Builder class for SphinxQL statements.
14
 */
15
class SphinxQL
16
{
17
    /**
18
     * A non-static connection for the current SphinxQL object
19
     *
20
     * @var ConnectionInterface
21
     */
22
    protected $connection;
23
24
    /**
25
     * The last result object.
26
     *
27
     * @var array
28
     */
29
    protected $last_result;
30
31
    /**
32
     * The last compiled query.
33
     *
34
     * @var string
35
     */
36
    protected $last_compiled;
37
38
    /**
39
     * The last chosen method (select, insert, replace, update, delete).
40
     *
41
     * @var string
42
     */
43
    protected $type;
44
45
    /**
46
     * An SQL query that is not yet executed or "compiled"
47
     *
48
     * @var string
49
     */
50
    protected $query;
51
52
    /**
53
     * Array of select elements that will be comma separated.
54
     *
55
     * @var array
56
     */
57
    protected $select = array();
58
59
    /**
60
     * From in SphinxQL is the list of indexes that will be used
61
     *
62
     * @var array
63
     */
64
    protected $from = array();
65
66
    /**
67
     * The list of where and parenthesis, must be inserted in order
68
     *
69
     * @var array
70
     */
71
    protected $where = array();
72
73
    /**
74
     * The list of matches for the MATCH function in SphinxQL
75
     *
76
     * @var array
77
     */
78
    protected $match = array();
79
80
    /**
81
     * GROUP BY array to be comma separated
82
     *
83
     * @var array
84
     */
85
    protected $group_by = array();
86
87
    /**
88
     * When not null changes 'GROUP BY' to 'GROUP N BY'
89
     *
90
     * @var null|int
91
     */
92
    protected $group_n_by;
93
94
    /**
95
     * ORDER BY array
96
     *
97
     * @var array
98
     */
99
    protected $within_group_order_by = array();
100
101
    /**
102
     * The list of where and parenthesis, must be inserted in order
103
     *
104
     * @var array
105
     */
106
    protected $having = array();
107
108
    /**
109
     * ORDER BY array
110
     *
111
     * @var array
112
     */
113
    protected $order_by = array();
114
115
    /**
116
     * When not null it adds an offset
117
     *
118
     * @var null|int
119
     */
120
    protected $offset;
121
122
    /**
123
     * When not null it adds a limit
124
     *
125
     * @var null|int
126
     */
127
    protected $limit;
128
129
    /**
130
     * Value of INTO query for INSERT or REPLACE
131
     *
132
     * @var null|string
133
     */
134
    protected $into;
135
136
    /**
137
     * Array of columns for INSERT or REPLACE
138
     *
139
     * @var array
140
     */
141
    protected $columns = array();
142
143
    /**
144
     * Array OF ARRAYS of values for INSERT or REPLACE
145
     *
146
     * @var array
147
     */
148
    protected $values = array();
149
150
    /**
151
     * Array arrays containing column and value for SET in UPDATE
152
     *
153
     * @var array
154
     */
155
    protected $set = array();
156
157
    /**
158
     * Array of OPTION specific to SphinxQL
159
     *
160
     * @var array
161
     */
162
    protected $options = array();
163
164
    /**
165
     * Array of FACETs
166
     *
167
     * @var Facet[]
168
     */
169
    protected $facets = array();
170
171
    /**
172
     * The reference to the object that queued itself and created this object
173
     *
174
     * @var null|SphinxQL
175
     */
176
    protected $queue_prev;
177
178
    /**
179
     * An array of escaped characters for escapeMatch()
180
     * @var array
181
     */
182
    protected $escape_full_chars = array(
183
        '\\' => '\\\\',
184
        '('  => '\(',
185
        ')'  => '\)',
186
        '|'  => '\|',
187
        '-'  => '\-',
188
        '!'  => '\!',
189
        '@'  => '\@',
190
        '~'  => '\~',
191
        '"'  => '\"',
192
        '&'  => '\&',
193
        '/'  => '\/',
194
        '^'  => '\^',
195
        '$'  => '\$',
196
        '='  => '\=',
197
        '<'  => '\<',
198
    );
199
200
    /**
201
     * An array of escaped characters for fullEscapeMatch()
202
     * @var array
203
     */
204
    protected $escape_half_chars = array(
205
        '\\' => '\\\\',
206
        '('  => '\(',
207
        ')'  => '\)',
208
        '!'  => '\!',
209
        '@'  => '\@',
210
        '~'  => '\~',
211
        '&'  => '\&',
212
        '/'  => '\/',
213
        '^'  => '\^',
214
        '$'  => '\$',
215
        '='  => '\=',
216
        '<'  => '\<',
217
    );
218
219
    /**
220
     * @param ConnectionInterface|null $connection
221
     */
222
    public function __construct(ConnectionInterface $connection = null)
223
    {
224
        $this->connection = $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
     * @todo make non static
248
     */
249
    public static function expr($string = '')
250
    {
251
        return new Expression($string);
252
    }
253
254
    /**
255
     * Runs the query built
256
     *
257
     * @return ResultSetInterface The result of the query
258
     * @throws DatabaseException
259
     * @throws ConnectionException
260
     * @throws SphinxQLException
261
     */
262
    public function execute()
263
    {
264
        // pass the object so execute compiles it by itself
265
        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 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...
266
    }
267
268
    /**
269
     * Executes a batch of queued queries
270
     *
271
     * @return MultiResultSetInterface The array of results
272
     * @throws SphinxQLException In case no query is in queue
273
     * @throws Exception\DatabaseException
274
     * @throws ConnectionException
275
     */
276
    public function executeBatch()
277
    {
278
        if (count($this->getQueue()) == 0) {
279
            throw new SphinxQLException('There is no Queue present to execute.');
280
        }
281
282
        $queue = array();
283
284
        foreach ($this->getQueue() as $query) {
285
            $queue[] = $query->compile()->getCompiled();
286
        }
287
288
        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 Foolz\SphinxQL\Drivers\MultiResultSetInterface 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...
289
    }
290
291
    /**
292
     * Enqueues the current object and returns a new one or the supplied one
293
     *
294
     * @param SphinxQL|null $next
295
     *
296
     * @return SphinxQL A new SphinxQL object with the current object referenced
297
     */
298
    public function enqueue(SphinxQL $next = null)
299
    {
300
        if ($next === null) {
301
            $next = new static($this->getConnection());
302
        }
303
304
        $next->setQueuePrev($this);
305
306
        return $next;
307
    }
308
309
    /**
310
     * Returns the ordered array of enqueued objects
311
     *
312
     * @return SphinxQL[] The ordered array of enqueued objects
313
     */
314
    public function getQueue()
315
    {
316
        $queue = array();
317
        $curr = $this;
318
319
        do {
320
            if ($curr->type != null) {
321
                $queue[] = $curr;
322
            }
323
        } while ($curr = $curr->getQueuePrev());
324
325
        return array_reverse($queue);
326
    }
327
328
    /**
329
     * Gets the enqueued object
330
     *
331
     * @return SphinxQL|null
332
     */
333
    public function getQueuePrev()
334
    {
335
        return $this->queue_prev;
336
    }
337
338
    /**
339
     * Sets the reference to the enqueued object
340
     *
341
     * @param SphinxQL $query The object to set as previous
342
     *
343
     * @return $this
344
     */
345
    public function setQueuePrev($query)
346
    {
347
        $this->queue_prev = $query;
348
349
        return $this;
350
    }
351
352
    /**
353
     * Returns the result of the last query
354
     *
355
     * @return array The result of the last query
356
     */
357
    public function getResult()
358
    {
359
        return $this->last_result;
360
    }
361
362
    /**
363
     * Returns the latest compiled query
364
     *
365
     * @return string The last compiled query
366
     */
367
    public function getCompiled()
368
    {
369
        return $this->last_compiled;
370
    }
371
372
    /**
373
     * Begins transaction
374
     * @throws DatabaseException
375
     * @throws ConnectionException
376
     */
377
    public function transactionBegin()
378
    {
379
        $this->getConnection()->query('BEGIN');
380
    }
381
382
    /**
383
     * Commits transaction
384
     * @throws DatabaseException
385
     * @throws ConnectionException
386
     */
387
    public function transactionCommit()
388
    {
389
        $this->getConnection()->query('COMMIT');
390
    }
391
392
    /**
393
     * Rollbacks transaction
394
     * @throws DatabaseException
395
     * @throws ConnectionException
396
     */
397
    public function transactionRollback()
398
    {
399
        $this->getConnection()->query('ROLLBACK');
400
    }
401
402
    /**
403
     * Runs the compile function
404
     *
405
     * @return $this
406
     * @throws ConnectionException
407
     * @throws DatabaseException
408
     * @throws SphinxQLException
409
     */
410
    public function compile()
411
    {
412
        switch ($this->type) {
413
            case 'select':
414
                $this->compileSelect();
415
                break;
416
            case 'insert':
417
            case 'replace':
418
                $this->compileInsert();
419
                break;
420
            case 'update':
421
                $this->compileUpdate();
422
                break;
423
            case 'delete':
424
                $this->compileDelete();
425
                break;
426
            case 'query':
427
                $this->compileQuery();
428
                break;
429
        }
430
431
        return $this;
432
    }
433
434
    /**
435
     * @return $this
436
     */
437
    public function compileQuery()
438
    {
439
        $this->last_compiled = $this->query;
440
441
        return $this;
442
    }
443
444
    /**
445
     * Compiles the MATCH part of the queries
446
     * Used by: SELECT, DELETE, UPDATE
447
     *
448
     * @return string The compiled MATCH
449
     * @throws Exception\ConnectionException
450
     * @throws Exception\DatabaseException
451
     */
452
    public function compileMatch()
453
    {
454
        $query = '';
455
456
        if (!empty($this->match)) {
457
            $query .= 'WHERE MATCH(';
458
459
            $matched = array();
460
461
            foreach ($this->match as $match) {
462
                $pre = '';
463
                if ($match['column'] instanceof \Closure) {
464
                    $sub = new Match($this);
465
                    call_user_func($match['column'], $sub);
466
                    $pre .= $sub->compile()->getCompiled();
467
                } elseif ($match['column'] instanceof Match) {
468
                    $pre .= $match['column']->compile()->getCompiled();
469
                } elseif (empty($match['column'])) {
470
                    $pre .= '';
471
                } elseif (is_array($match['column'])) {
472
                    $pre .= '@('.implode(',', $match['column']).') ';
473
                } else {
474
                    $pre .= '@'.$match['column'].' ';
475
                }
476
477
                if ($match['half']) {
478
                    $pre .= $this->halfEscapeMatch($match['value']);
479
                } else {
480
                    $pre .= $this->escapeMatch($match['value']);
481
                }
482
483
                if ($pre !== '') {
484
                    $matched[] = '('.$pre.')';
485
                }
486
            }
487
488
            $matched = implode(' ', $matched);
489
            $query .= $this->getConnection()->escape(trim($matched)).') ';
490
        }
491
492
        return $query;
493
    }
494
495
    /**
496
     * Compiles the WHERE part of the queries
497
     * It interacts with the MATCH() and of course isn't usable stand-alone
498
     * Used by: SELECT, DELETE, UPDATE
499
     *
500
     * @return string The compiled WHERE
501
     * @throws ConnectionException
502
     * @throws DatabaseException
503
     */
504
    public function compileWhere()
505
    {
506
        $query = '';
507
508
        if (empty($this->match) && !empty($this->where)) {
509
            $query .= 'WHERE ';
510
        }
511
512
        if (!empty($this->where)) {
513
            foreach ($this->where as $key => $where) {
514
                if ($key > 0 || !empty($this->match)) {
515
                    $query .= 'AND ';
516
                }
517
                $query .= $this->compileFilterCondition($where);
518
            }
519
        }
520
521
        return $query;
522
    }
523
524
    /**
525
     * @param array $filter
526
     *
527
     * @return string
528
     * @throws ConnectionException
529
     * @throws DatabaseException
530
     */
531
    public function compileFilterCondition($filter)
532
    {
533
        $query = '';
534
535
        if (!empty($filter)) {
536
            if (strtoupper($filter['operator']) === 'BETWEEN') {
537
                $query .= $filter['column'];
538
                $query .= ' BETWEEN ';
539
                $query .= $this->getConnection()->quote($filter['value'][0]).' AND '
540
                    .$this->getConnection()->quote($filter['value'][1]).' ';
541
            } else {
542
                // id can't be quoted!
543
                if ($filter['column'] === 'id') {
544
                    $query .= 'id ';
545
                } else {
546
                    $query .= $filter['column'].' ';
547
                }
548
549
                if (in_array(strtoupper($filter['operator']), array('IN', 'NOT IN'), true)) {
550
                    $query .= strtoupper($filter['operator']).' ('.implode(', ', $this->getConnection()->quoteArr($filter['value'])).') ';
551
                } else {
552
                    $query .= $filter['operator'].' '.$this->getConnection()->quote($filter['value']).' ';
553
                }
554
            }
555
        }
556
557
        return $query;
558
    }
559
560
    /**
561
     * Compiles the statements for SELECT
562
     *
563
     * @return $this
564
     * @throws ConnectionException
565
     * @throws DatabaseException
566
     * @throws SphinxQLException
567
     */
568
    public function compileSelect()
569
    {
570
        $query = '';
571
572
        if ($this->type == 'select') {
573
            $query .= 'SELECT ';
574
575
            if (!empty($this->select)) {
576
                $query .= implode(', ', $this->select).' ';
577
            } else {
578
                $query .= '* ';
579
            }
580
        }
581
582
        if (!empty($this->from)) {
583
            if ($this->from instanceof \Closure) {
0 ignored issues
show
introduced by
$this->from is never a sub-type of Closure.
Loading history...
584
                $sub = new static($this->getConnection());
585
                call_user_func($this->from, $sub);
586
                $query .= 'FROM ('.$sub->compile()->getCompiled().') ';
587
            } elseif ($this->from instanceof SphinxQL) {
0 ignored issues
show
introduced by
$this->from is never a sub-type of Foolz\SphinxQL\SphinxQL.
Loading history...
588
                $query .= 'FROM ('.$this->from->compile()->getCompiled().') ';
589
            } else {
590
                $query .= 'FROM '.implode(', ', $this->from).' ';
591
            }
592
        }
593
594
        $query .= $this->compileMatch().$this->compileWhere();
595
596
        if (!empty($this->group_by)) {
597
            $query .= 'GROUP ';
598
            if ($this->group_n_by !== null) {
599
                $query .= $this->group_n_by.' ';
600
            }
601
            $query .= 'BY '.implode(', ', $this->group_by).' ';
602
        }
603
604
        if (!empty($this->within_group_order_by)) {
605
            $query .= 'WITHIN GROUP ORDER BY ';
606
607
            $order_arr = array();
608
609
            foreach ($this->within_group_order_by as $order) {
610
                $order_sub = $order['column'].' ';
611
612
                if ($order['direction'] !== null) {
613
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
614
                }
615
616
                $order_arr[] = $order_sub;
617
            }
618
619
            $query .= implode(', ', $order_arr).' ';
620
        }
621
622
        if (!empty($this->having)) {
623
            $query .= 'HAVING '.$this->compileFilterCondition($this->having);
624
        }
625
626
        if (!empty($this->order_by)) {
627
            $query .= 'ORDER BY ';
628
629
            $order_arr = array();
630
631
            foreach ($this->order_by as $order) {
632
                $order_sub = $order['column'].' ';
633
634
                if ($order['direction'] !== null) {
635
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
636
                }
637
638
                $order_arr[] = $order_sub;
639
            }
640
641
            $query .= implode(', ', $order_arr).' ';
642
        }
643
644
        if ($this->limit !== null || $this->offset !== null) {
645
            if ($this->offset === null) {
646
                $this->offset = 0;
647
            }
648
649
            if ($this->limit === null) {
650
                $this->limit = 9999999999999;
651
            }
652
653
            $query .= 'LIMIT '.((int) $this->offset).', '.((int) $this->limit).' ';
654
        }
655
656
        if (!empty($this->options)) {
657
            $options = array();
658
659
            foreach ($this->options as $option) {
660
                if ($option['value'] instanceof Expression) {
661
                    $option['value'] = $option['value']->value();
662
                } elseif (is_array($option['value'])) {
663
                    array_walk(
664
                        $option['value'],
665
                        function (&$val, $key) {
666
                            $val = $key.'='.$val;
667
                        }
668
                    );
669
                    $option['value'] = '('.implode(', ', $option['value']).')';
670
                } else {
671
                    $option['value'] = $this->getConnection()->quote($option['value']);
672
                }
673
674
                $options[] = $option['name'].' = '.$option['value'];
675
            }
676
677
            $query .= 'OPTION '.implode(', ', $options).' ';
678
        }
679
680
        if (!empty($this->facets)) {
681
            $facets = array();
682
683
            foreach ($this->facets as $facet) {
684
                // dynamically set the own SphinxQL connection if the Facet doesn't own one
685
                if ($facet->getConnection() === null) {
686
                    $facet->setConnection($this->getConnection());
687
                    $facets[] = $facet->getFacet();
688
                    // go back to the status quo for reuse
689
                    $facet->setConnection();
690
                } else {
691
                    $facets[] = $facet->getFacet();
692
                }
693
            }
694
695
            $query .= implode(' ', $facets);
696
        }
697
698
        $query = trim($query);
699
        $this->last_compiled = $query;
700
701
        return $this;
702
    }
703
704
    /**
705
     * Compiles the statements for INSERT or REPLACE
706
     *
707
     * @return $this
708
     * @throws ConnectionException
709
     * @throws DatabaseException
710
     */
711
    public function compileInsert()
712
    {
713
        if ($this->type == 'insert') {
714
            $query = 'INSERT ';
715
        } else {
716
            $query = 'REPLACE ';
717
        }
718
719
        if ($this->into !== null) {
720
            $query .= 'INTO '.$this->into.' ';
721
        }
722
723
        if (!empty($this->columns)) {
724
            $query .= '('.implode(', ', $this->columns).') ';
725
        }
726
727
        if (!empty($this->values)) {
728
            $query .= 'VALUES ';
729
            $query_sub = array();
730
731
            foreach ($this->values as $value) {
732
                $query_sub[] = '('.implode(', ', $this->getConnection()->quoteArr($value)).')';
733
            }
734
735
            $query .= implode(', ', $query_sub);
736
        }
737
738
        $query = trim($query);
739
        $this->last_compiled = $query;
740
741
        return $this;
742
    }
743
744
    /**
745
     * Compiles the statements for UPDATE
746
     *
747
     * @return $this
748
     * @throws ConnectionException
749
     * @throws DatabaseException
750
     */
751
    public function compileUpdate()
752
    {
753
        $query = 'UPDATE ';
754
755
        if ($this->into !== null) {
756
            $query .= $this->into.' ';
757
        }
758
759
        if (!empty($this->set)) {
760
            $query .= 'SET ';
761
762
            $query_sub = array();
763
764
            foreach ($this->set as $column => $value) {
765
                // MVA support
766
                if (is_array($value)) {
767
                    $query_sub[] = $column
768
                        .' = ('.implode(', ', $this->getConnection()->quoteArr($value)).')';
769
                } else {
770
                    $query_sub[] = $column
771
                        .' = '.$this->getConnection()->quote($value);
772
                }
773
            }
774
775
            $query .= implode(', ', $query_sub).' ';
776
        }
777
778
        $query .= $this->compileMatch().$this->compileWhere();
779
780
        $query = trim($query);
781
        $this->last_compiled = $query;
782
783
        return $this;
784
    }
785
786
    /**
787
     * Compiles the statements for DELETE
788
     *
789
     * @return $this
790
     * @throws ConnectionException
791
     * @throws DatabaseException
792
     */
793
    public function compileDelete()
794
    {
795
        $query = 'DELETE ';
796
797
        if (!empty($this->from)) {
798
            $query .= 'FROM '.$this->from[0].' ';
799
        }
800
801
        if (!empty($this->where)) {
802
            $query .= $this->compileWhere();
803
        }
804
805
        $query = trim($query);
806
        $this->last_compiled = $query;
807
808
        return $this;
809
    }
810
811
    /**
812
     * Sets a query to be executed
813
     *
814
     * @param string $sql A SphinxQL query to execute
815
     *
816
     * @return $this
817
     */
818
    public function query($sql)
819
    {
820
        $this->type = 'query';
821
        $this->query = $sql;
822
823
        return $this;
824
    }
825
826
    /**
827
     * Select the columns
828
     *
829
     * Gets the arguments passed as $sphinxql->select('one', 'two')
830
     * Using it without arguments equals to having '*' as argument
831
     * Using it with array maps values as column names
832
     *
833
     * Examples:
834
     *    $query->select('title');
835
     *    // SELECT title
836
     *
837
     *    $query->select('title', 'author', 'date');
838
     *    // SELECT title, author, date
839
     *
840
     *    $query->select(['id', 'title']);
841
     *    // SELECT id, title
842
     *
843
     * @param array|string $columns Array or multiple string arguments containing column names
844
     *
845
     * @return $this
846
     */
847
    public function select($columns = null)
848
    {
849
        $this->reset();
850
        $this->type = 'select';
851
852
        if (is_array($columns)) {
853
            $this->select = $columns;
854
        } else {
855
            $this->select = \func_get_args();
856
        }
857
858
        return $this;
859
    }
860
861
    /**
862
     * Alters which arguments to select
863
     *
864
     * Query is assumed to be in SELECT mode
865
     * See select() for usage
866
     *
867
     * @param array|string $columns Array or multiple string arguments containing column names
868
     *
869
     * @return $this
870
     */
871
    public function setSelect($columns = null)
872
    {
873
        if (is_array($columns)) {
874
            $this->select = $columns;
875
        } else {
876
            $this->select = \func_get_args();
877
        }
878
879
        return $this;
880
    }
881
882
    /**
883
     * Get the columns staged to select
884
     *
885
     * @return array
886
     */
887
    public function getSelect()
888
    {
889
        return $this->select;
890
    }
891
892
    /**
893
     * Activates the INSERT mode
894
     *
895
     * @return $this
896
     */
897
    public function insert()
898
    {
899
        $this->reset();
900
        $this->type = 'insert';
901
902
        return $this;
903
    }
904
905
    /**
906
     * Activates the REPLACE mode
907
     *
908
     * @return $this
909
     */
910
    public function replace()
911
    {
912
        $this->reset();
913
        $this->type = 'replace';
914
915
        return $this;
916
    }
917
918
    /**
919
     * Activates the UPDATE mode
920
     *
921
     * @param string $index The index to update into
922
     *
923
     * @return $this
924
     */
925
    public function update($index)
926
    {
927
        $this->reset();
928
        $this->type = 'update';
929
        $this->into($index);
930
931
        return $this;
932
    }
933
934
    /**
935
     * Activates the DELETE mode
936
     *
937
     * @return $this
938
     */
939
    public function delete()
940
    {
941
        $this->reset();
942
        $this->type = 'delete';
943
944
        return $this;
945
    }
946
947
    /**
948
     * FROM clause (Sphinx-specific since it works with multiple indexes)
949
     * func_get_args()-enabled
950
     *
951
     * @param array $array An array of indexes to use
952
     *
953
     * @return $this
954
     */
955
    public function from($array = null)
956
    {
957
        if (is_string($array)) {
958
            $this->from = \func_get_args();
959
        }
960
961
        if (is_array($array) || $array instanceof \Closure || $array instanceof SphinxQL) {
962
            $this->from = $array;
0 ignored issues
show
Documentation Bug introduced by
It seems like $array can also be of type Closure or Foolz\SphinxQL\SphinxQL. However, the property $from is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
963
        }
964
965
        return $this;
966
    }
967
    
968
    /**
969
     * MATCH field IN (values)
970
     *
971
     * @param string $field  The field
972
     * @param array  $values Values for IN clause
973
     *
974
     * @return Match
975
     */
976
    public function matchFieldIN(string $field, array $values) : Match {
977
        $match = (new Match($this))->field($field);
978
979
        /**
980
         * Reindex array
981
         */
982
        $values = array_values(array_unique($values));
983
984
        $count  = count($values);
985
        for ($n = 0; $n < $count; $n++) {
986
            if (!$n) {
987
                $match->match($values[$n]);
988
            } else {
989
                $match->orMatch($values[$n]);
990
            }
991
        }
992
        return $match;
993
    }
994
995
    /**
996
     * Wrap match into another match helper to build complex (A OR (B AND C)) matches
997
     *
998
     * @param Match[] $matches Matches to group up with AND
999
     *
1000
     * @return Match
1001
     */    
1002
    public function matchAndMatch(array $matches) {
1003
        $match = new Match($this);
1004
        foreach ($matches as $m) {
1005
            $match->match($m);
1006
        }
1007
        return $match;
1008
    }
1009
1010
    /**
1011
     * MATCH clause (Sphinx-specific)
1012
     *
1013
     * @param mixed  $column The column name (can be array, string, Closure, or Match)
1014
     * @param string $value  The value
1015
     * @param bool   $half   Exclude ", |, - control characters from being escaped
1016
     *
1017
     * @return $this
1018
     */
1019
    public function match($column, $value = null, $half = false)
1020
    {
1021
        if ($column === '*' || (is_array($column) && in_array('*', $column))) {
1022
            $column = array();
1023
        }
1024
1025
        $this->match[] = array('column' => $column, 'value' => $value, 'half' => $half);
1026
1027
        return $this;
1028
    }
1029
1030
    /**
1031
     * WHERE clause
1032
     *
1033
     * Examples:
1034
     *    $query->where('column', 'value');
1035
     *    // WHERE column = 'value'
1036
     *
1037
     *    $query->where('column', '=', 'value');
1038
     *    // WHERE column = 'value'
1039
     *
1040
     *    $query->where('column', '>=', 'value')
1041
     *    // WHERE column >= 'value'
1042
     *
1043
     *    $query->where('column', 'IN', array('value1', 'value2', 'value3'));
1044
     *    // WHERE column IN ('value1', 'value2', 'value3')
1045
     *
1046
     *    $query->where('column', 'BETWEEN', array('value1', 'value2'))
1047
     *    // WHERE column BETWEEN 'value1' AND 'value2'
1048
     *    // WHERE example BETWEEN 10 AND 100
1049
     *
1050
     * @param string                                      $column   The column name
1051
     * @param Expression|string|null|bool|array|int|float $operator The operator to use (if value is not null, you can
1052
     *      use only string)
1053
     * @param Expression|string|null|bool|array|int|float $value    The value to check against
1054
     *
1055
     * @return $this
1056
     */
1057
    public function where($column, $operator, $value = null)
1058
    {
1059
        if ($value === null) {
1060
            $value = $operator;
1061
            $operator = '=';
1062
        }
1063
1064
        $this->where[] = array(
1065
            'column'   => $column,
1066
            'operator' => $operator,
1067
            'value'    => $value,
1068
        );
1069
1070
        return $this;
1071
    }
1072
1073
    /**
1074
     * GROUP BY clause
1075
     * Adds to the previously added columns
1076
     *
1077
     * @param string $column A column to group by
1078
     *
1079
     * @return $this
1080
     */
1081
    public function groupBy($column)
1082
    {
1083
        $this->group_by[] = $column;
1084
1085
        return $this;
1086
    }
1087
1088
    /**
1089
     * GROUP N BY clause (SphinxQL-specific)
1090
     * Changes 'GROUP BY' into 'GROUP N BY'
1091
     *
1092
     * @param int $n Number of items per group
1093
     *
1094
     * @return $this
1095
     */
1096
    public function groupNBy($n)
1097
    {
1098
        $this->group_n_by = (int) $n;
1099
1100
        return $this;
1101
    }
1102
1103
    /**
1104
     * WITHIN GROUP ORDER BY clause (SphinxQL-specific)
1105
     * Adds to the previously added columns
1106
     * Works just like a classic ORDER BY
1107
     *
1108
     * @param string $column    The column to group by
1109
     * @param string $direction The group by direction (asc/desc)
1110
     *
1111
     * @return $this
1112
     */
1113
    public function withinGroupOrderBy($column, $direction = null)
1114
    {
1115
        $this->within_group_order_by[] = array('column' => $column, 'direction' => $direction);
1116
1117
        return $this;
1118
    }
1119
1120
    /**
1121
     * HAVING clause
1122
     *
1123
     * Examples:
1124
     *    $sq->having('column', 'value');
1125
     *    // HAVING column = 'value'
1126
     *
1127
     *    $sq->having('column', '=', 'value');
1128
     *    // HAVING column = 'value'
1129
     *
1130
     *    $sq->having('column', '>=', 'value')
1131
     *    // HAVING column >= 'value'
1132
     *
1133
     *    $sq->having('column', 'IN', array('value1', 'value2', 'value3'));
1134
     *    // HAVING column IN ('value1', 'value2', 'value3')
1135
     *
1136
     *    $sq->having('column', 'BETWEEN', array('value1', 'value2'))
1137
     *    // HAVING column BETWEEN 'value1' AND 'value2'
1138
     *    // HAVING example BETWEEN 10 AND 100
1139
     *
1140
     * @param string $column   The column name
1141
     * @param string $operator The operator to use
1142
     * @param string $value    The value to check against
1143
     *
1144
     * @return $this
1145
     */
1146
    public function having($column, $operator, $value = null)
1147
    {
1148
        if ($value === null) {
1149
            $value = $operator;
1150
            $operator = '=';
1151
        }
1152
1153
        $this->having = array(
1154
            'column'   => $column,
1155
            'operator' => $operator,
1156
            'value'    => $value,
1157
        );
1158
1159
        return $this;
1160
    }
1161
1162
    /**
1163
     * ORDER BY clause
1164
     * Adds to the previously added columns
1165
     *
1166
     * @param string $column    The column to order on
1167
     * @param string $direction The ordering direction (asc/desc)
1168
     *
1169
     * @return $this
1170
     */
1171
    public function orderBy($column, $direction = null)
1172
    {
1173
        $this->order_by[] = array('column' => $column, 'direction' => $direction);
1174
1175
        return $this;
1176
    }
1177
1178
    /**
1179
     * LIMIT clause
1180
     * Supports also LIMIT offset, limit
1181
     *
1182
     * @param int      $offset Offset if $limit is specified, else limit
1183
     * @param null|int $limit  The limit to set, null for no limit
1184
     *
1185
     * @return $this
1186
     */
1187
    public function limit($offset, $limit = null)
1188
    {
1189
        if ($limit === null) {
1190
            $this->limit = (int) $offset;
1191
1192
            return $this;
1193
        }
1194
1195
        $this->offset($offset);
1196
        $this->limit = (int) $limit;
1197
1198
        return $this;
1199
    }
1200
1201
    /**
1202
     * OFFSET clause
1203
     *
1204
     * @param int $offset The offset
1205
     *
1206
     * @return $this
1207
     */
1208
    public function offset($offset)
1209
    {
1210
        $this->offset = (int) $offset;
1211
1212
        return $this;
1213
    }
1214
1215
    /**
1216
     * OPTION clause (SphinxQL-specific)
1217
     * Used by: SELECT
1218
     *
1219
     * @param string                                      $name  Option name
1220
     * @param Expression|array|string|int|bool|float|null $value Option value
1221
     *
1222
     * @return $this
1223
     */
1224
    public function option($name, $value)
1225
    {
1226
        $this->options[] = array('name' => $name, 'value' => $value);
1227
1228
        return $this;
1229
    }
1230
1231
    /**
1232
     * INTO clause
1233
     * Used by: INSERT, REPLACE
1234
     *
1235
     * @param string $index The index to insert/replace into
1236
     *
1237
     * @return $this
1238
     */
1239
    public function into($index)
1240
    {
1241
        $this->into = $index;
1242
1243
        return $this;
1244
    }
1245
1246
    /**
1247
     * Set columns
1248
     * Used in: INSERT, REPLACE
1249
     * func_get_args()-enabled
1250
     *
1251
     * @param array $array The array of columns
1252
     *
1253
     * @return $this
1254
     */
1255
    public function columns($array = array())
1256
    {
1257
        if (is_array($array)) {
1258
            $this->columns = $array;
1259
        } else {
1260
            $this->columns = \func_get_args();
1261
        }
1262
1263
        return $this;
1264
    }
1265
1266
    /**
1267
     * Set VALUES
1268
     * Used in: INSERT, REPLACE
1269
     * func_get_args()-enabled
1270
     *
1271
     * @param array $array The array of values matching the columns from $this->columns()
1272
     *
1273
     * @return $this
1274
     */
1275
    public function values($array)
1276
    {
1277
        if (is_array($array)) {
1278
            $this->values[] = $array;
1279
        } else {
1280
            $this->values[] = \func_get_args();
1281
        }
1282
1283
        return $this;
1284
    }
1285
1286
    /**
1287
     * Set column and relative value
1288
     * Used in: INSERT, REPLACE
1289
     *
1290
     * @param string $column The column name
1291
     * @param string $value  The value
1292
     *
1293
     * @return $this
1294
     */
1295
    public function value($column, $value)
1296
    {
1297
        if ($this->type === 'insert' || $this->type === 'replace') {
1298
            $this->columns[] = $column;
1299
            $this->values[0][] = $value;
1300
        } else {
1301
            $this->set[$column] = $value;
1302
        }
1303
1304
        return $this;
1305
    }
1306
1307
    /**
1308
     * Allows passing an array with the key as column and value as value
1309
     * Used in: INSERT, REPLACE, UPDATE
1310
     *
1311
     * @param array $array Array of key-values
1312
     *
1313
     * @return $this
1314
     */
1315
    public function set($array)
1316
    {
1317
        if ($this->columns === array_keys($array)) {
1318
            $this->values($array);
1319
        } else {
1320
            foreach ($array as $key => $item) {
1321
                $this->value($key, $item);
1322
            }
1323
        }
1324
1325
        return $this;
1326
    }
1327
1328
    /**
1329
     * Allows passing an array with the key as column and value as value
1330
     * Used in: INSERT, REPLACE, UPDATE
1331
     *
1332
     * @param Facet $facet
1333
     *
1334
     * @return $this
1335
     */
1336
    public function facet($facet)
1337
    {
1338
        $this->facets[] = $facet;
1339
1340
        return $this;
1341
    }
1342
1343
    /**
1344
     * Sets the characters used for escapeMatch().
1345
     *
1346
     * @param array $array The array of characters to escape
1347
     *
1348
     * @return $this
1349
     */
1350
    public function setFullEscapeChars($array = array())
1351
    {
1352
        if (!empty($array)) {
1353
            $this->escape_full_chars = $this->compileEscapeChars($array);
1354
        }
1355
1356
        return $this;
1357
    }
1358
1359
    /**
1360
     * Sets the characters used for halfEscapeMatch().
1361
     *
1362
     * @param array $array The array of characters to escape
1363
     *
1364
     * @return $this
1365
     */
1366
    public function setHalfEscapeChars($array = array())
1367
    {
1368
        if (!empty($array)) {
1369
            $this->escape_half_chars = $this->compileEscapeChars($array);
1370
        }
1371
1372
        return $this;
1373
    }
1374
1375
    /**
1376
     * Compiles an array containing the characters and escaped characters into a key/value configuration.
1377
     *
1378
     * @param array $array The array of characters to escape
1379
     *
1380
     * @return array An array of the characters and it's escaped counterpart
1381
     */
1382
    public function compileEscapeChars($array = array())
1383
    {
1384
        $result = array();
1385
        foreach ($array as $character) {
1386
            $result[$character] = '\\'.$character;
1387
        }
1388
1389
        return $result;
1390
    }
1391
1392
    /**
1393
     * Escapes the query for the MATCH() function
1394
     *
1395
     * @param string $string The string to escape for the MATCH
1396
     *
1397
     * @return string The escaped string
1398
     */
1399
    public function escapeMatch($string)
1400
    {
1401
        if ($string instanceof Expression) {
0 ignored issues
show
introduced by
$string is never a sub-type of Foolz\SphinxQL\Expression.
Loading history...
1402
            return $string->value();
1403
        }
1404
1405
        return mb_strtolower(str_replace(array_keys($this->escape_full_chars), array_values($this->escape_full_chars), $string), 'utf8');
1406
    }
1407
1408
    /**
1409
     * Escapes the query for the MATCH() function
1410
     * Allows some of the control characters to pass through for use with a search field: -, |, "
1411
     * It also does some tricks to wrap/unwrap within " the string and prevents errors
1412
     *
1413
     * @param string $string The string to escape for the MATCH
1414
     *
1415
     * @return string The escaped string
1416
     */
1417
    public function halfEscapeMatch($string)
1418
    {
1419
        if ($string instanceof Expression) {
0 ignored issues
show
introduced by
$string is never a sub-type of Foolz\SphinxQL\Expression.
Loading history...
1420
            return $string->value();
1421
        }
1422
1423
        $string = str_replace(array_keys($this->escape_half_chars), array_values($this->escape_half_chars), $string);
1424
1425
        // this manages to lower the error rate by a lot
1426
        if (mb_substr_count($string, '"', 'utf8') % 2 !== 0) {
1427
            $string .= '"';
1428
        }
1429
1430
        $string = preg_replace('/-[\s-]*-/u', '-', $string);
1431
1432
        $from_to_preg = array(
1433
            '/([-|])\s*$/u'        => '\\\\\1',
1434
            '/\|[\s|]*\|/u'        => '|',
1435
            '/(\S+)-(\S+)/u'       => '\1\-\2',
1436
            '/(\S+)\s+-\s+(\S+)/u' => '\1 \- \2',
1437
        );
1438
1439
        $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string), 'utf8');
1440
1441
        return $string;
1442
    }
1443
1444
    /**
1445
     * Clears the existing query build for new query when using the same SphinxQL instance.
1446
     *
1447
     * @return $this
1448
     */
1449
    public function reset()
1450
    {
1451
        $this->query = null;
1452
        $this->select = array();
1453
        $this->from = array();
1454
        $this->where = array();
1455
        $this->match = array();
1456
        $this->group_by = array();
1457
        $this->group_n_by = null;
1458
        $this->within_group_order_by = array();
1459
        $this->having = array();
1460
        $this->order_by = array();
1461
        $this->offset = null;
1462
        $this->limit = null;
1463
        $this->into = null;
1464
        $this->columns = array();
1465
        $this->values = array();
1466
        $this->set = array();
1467
        $this->options = array();
1468
1469
        return $this;
1470
    }
1471
1472
    /**
1473
     * @return $this
1474
     */
1475
    public function resetWhere()
1476
    {
1477
        $this->where = array();
1478
1479
        return $this;
1480
    }
1481
1482
    /**
1483
     * @return $this
1484
     */
1485
    public function resetMatch()
1486
    {
1487
        $this->match = array();
1488
1489
        return $this;
1490
    }
1491
1492
    /**
1493
     * @return $this
1494
     */
1495
    public function resetGroupBy()
1496
    {
1497
        $this->group_by = array();
1498
        $this->group_n_by = null;
1499
1500
        return $this;
1501
    }
1502
1503
    /**
1504
     * @return $this
1505
     */
1506
    public function resetWithinGroupOrderBy()
1507
    {
1508
        $this->within_group_order_by = array();
1509
1510
        return $this;
1511
    }
1512
1513
    /**
1514
     * @return $this
1515
     */
1516
    public function resetFacets()
1517
    {
1518
        $this->facets = array();
1519
1520
        return $this;
1521
    }
1522
1523
    /**
1524
     * @return $this
1525
     */
1526
    public function resetHaving()
1527
    {
1528
        $this->having = array();
1529
1530
        return $this;
1531
    }
1532
1533
    /**
1534
     * @return $this
1535
     */
1536
    public function resetOrderBy()
1537
    {
1538
        $this->order_by = array();
1539
1540
        return $this;
1541
    }
1542
1543
    /**
1544
     * @return $this
1545
     */
1546
    public function resetOptions()
1547
    {
1548
        $this->options = array();
1549
1550
        return $this;
1551
    }
1552
}
1553