Passed
Push — ci-build-matrix ( 1c3307...9a015f )
by Hung
02:21
created

SphinxQL::groupNBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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