SphinxQL::compileDelete()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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