Completed
Push — master ( fcd719...7e2ade )
by Hung
01:26
created

SphinxQL   F

Complexity

Total Complexity 153

Size/Duplication

Total Lines 1465
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 1465
rs 0.6314
wmc 153

62 Methods

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

How to fix   Complexity   

Complex Class

Complex classes like SphinxQL often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

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

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

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

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