Client::resetFilters()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
ccs 0
cts 4
cp 0
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * $Id$
4
 */
5
6
/**
7
 * Copyright (c) 2001-2015, Andrew Aksyonoff
8
 * Copyright (c) 2008-2015, Sphinx Technologies Inc
9
 * All rights reserved
10
 *
11
 * This program is free software; you can redistribute it and/or modify
12
 * it under the terms of the GNU Library General Public License. You should
13
 * have received a copy of the LGPL license along with this program; if you
14
 * did not, you can find it at http://www.gnu.org/
15
 */
16
17
namespace Sphinx;
18
19
/**
20
 * WARNING!!!
21
 *
22
 * As of 2015, we strongly recommend to use either SphinxQL or REST APIs
23
 * rather than the native SphinxAPI.
24
 *
25
 * While both the native SphinxAPI protocol and the existing APIs will
26
 * continue to exist, and perhaps should not even break (too much), exposing
27
 * all the new features via multiple different native API implementations
28
 * is too much of a support complication for us.
29
 *
30
 * That said, you're welcome to overtake the maintenance of any given
31
 * official API, and remove this warning ;)
32
 */
33
34
/**
35
 * Sphinx searchd client class
36
 * PHP version of Sphinx searchd client (PHP API)
37
 */
38
class Client
39
{
40
    /**
41
     * Searchd host
42
     *
43
     * @var string
44
     */
45
    protected $host = 'localhost';
46
47
    /**
48
     * Searchd port
49
     *
50
     * @var int
51
     */
52
    protected $port = 9312;
53
54
    /**
55
     * How many records to seek from result-set start
56
     *
57
     * @var int
58
     */
59
    protected $offset = 0;
60
61
    /**
62
     * How many records to return from result-set starting at offset
63
     *
64
     * @var int
65
     */
66
    protected $limit = 20;
67
68
    /**
69
     * Query matching mode
70
     *
71
     * @var int
72
     */
73
    protected $mode = self::MATCH_EXTENDED2;
74
75
    /**
76
     * Per-field weights (default is 1 for all fields)
77
     *
78
     * @var array
79
     */
80
    protected $weights = array();
81
82
    /**
83
     * Match sorting mode
84
     *
85
     * @var int
86
     */
87
    protected $sort = self::SORT_RELEVANCE;
88
89
    /**
90
     * Attribute to sort by
91
     *
92
     * @var string
93
     */
94
    protected $sort_by = '';
95
96
    /**
97
     * Min ID to match (0 means no limit)
98
     *
99
     * @var int
100
     */
101
    protected $min_id = 0;
102
103
    /**
104
     * Max ID to match (0 means no limit)
105
     *
106
     * @var int
107
     */
108
    protected $max_id = 0;
109
110
    /**
111
     * Search filters
112
     *
113
     * @var array
114
     */
115
    protected $filters = array();
116
117
    /**
118
     * Group-by attribute name
119
     *
120
     * @var string
121
     */
122
    protected $group_by = '';
123
124
    /**
125
     * Group-by function (to pre-process group-by attribute value with)
126
     *
127
     * @var int
128
     */
129
    protected $group_func = self::GROUP_BY_DAY;
130
131
    /**
132
     * Group-by sorting clause (to sort groups in result set with)
133
     *
134
     * @var string
135
     */
136
    protected $group_sort = '@group desc';
137
138
    /**
139
     * Group-by count-distinct attribute
140
     *
141
     * @var string
142
     */
143
    protected $group_distinct = '';
144
145
    /**
146
     * Max matches to retrieve
147
     *
148
     * @var int
149
     */
150
    protected $max_matches = 1000;
151
152
    /**
153
     * Cutoff to stop searching at
154
     *
155
     * @var int
156
     */
157
    protected $cutoff = 0;
158
159
    /**
160
     * Distributed retries count
161
     *
162
     * @var int
163
     */
164
    protected $retry_count = 0;
165
166
    /**
167
     * Distributed retries delay
168
     *
169
     * @var int
170
     */
171
    protected $retry_delay = 0;
172
173
    /**
174
     * Geographical anchor point
175
     *
176
     * @var array
177
     */
178
    protected $anchor = array();
179
180
    /**
181
     * Per-index weights
182
     *
183
     * @var array
184
     */
185
    protected $index_weights = array();
186
187
    /**
188
     * Ranking mode
189
     *
190
     * @var int
191
     */
192
    protected $ranker = self::RANK_PROXIMITY_BM25;
193
194
    /**
195
     * Ranking mode expression (for self::RANK_EXPR)
196
     *
197
     * @var string
198
     */
199
    protected $rank_expr = '';
200
201
    /**
202
     * Max query time, milliseconds (0 means no limit)
203
     *
204
     * @var int
205
     */
206
    protected $max_query_time = 0;
207
208
    /**
209
     * Per-field-name weights
210
     *
211
     * @var array
212
     */
213
    protected $field_weights = array();
214
215
    /**
216
     * Per-query attribute values overrides
217
     *
218
     * @var array
219
     */
220
    protected $overrides = array();
221
222
    /**
223
     * Select-list (attributes or expressions, with optional aliases)
224
     *
225
     * @var string
226
     */
227
    protected $select = '*';
228
229
    /**
230
     * Per-query various flags
231
     *
232
     * @var int
233
     */
234
    protected $query_flags = 0;
235
236
    /**
237
     * Per-query max_predicted_time
238
     *
239
     * @var int
240
     */
241
    protected $predicted_time = 0;
242
243
    /**
244
     * Outer match sort by
245
     *
246
     * @var string
247
     */
248
    protected $outer_order_by = '';
249
250
    /**
251
     * Outer offset
252
     *
253
     * @var int
254
     */
255
    protected $outer_offset = 0;
256
257
    /**
258
     * Outer limit
259
     *
260
     * @var int
261
     */
262
    protected $outer_limit = 0;
263
264
    /**
265
     * @var bool
266
     */
267
    protected $has_outer = false;
268
269
    /**
270
     * Last error message
271
     *
272
     * @var string
273
     */
274
    protected $error = '';
275
276
    /**
277
     * Last warning message
278
     *
279
     * @var string
280
     */
281
    protected $warning = '';
282
283
    /**
284
     * Connection error vs remote error flag
285
     *
286
     * @var bool
287
     */
288
    protected $conn_error = false;
289
290
    /**
291
     * Requests array for multi-query
292
     *
293
     * @var array
294
     */
295
    protected $reqs = array();
296
297
    /**
298
     * Stored mbstring encoding
299
     *
300
     * @var string
301
     */
302
    protected $mbenc = '';
303
304
    /**
305
     * Whether $result['matches'] should be a hash or an array
306
     *
307
     * @var bool
308
     */
309
    protected $array_result = false;
310
311
    /**
312
     * Connect timeout
313
     *
314
     * @var int|float
315
     */
316
    protected $timeout = 0;
317
318
    /**
319
     * @var string
320
     */
321
    protected $path = '';
322
323
    /**
324
     * @var resource|bool
325
     */
326
    protected $socket = false;
327
328
    // known searchd commands
329
    const SEARCHD_COMMAND_SEARCH      = 0;
330
    const SEARCHD_COMMAND_EXCERPT     = 1;
331
    const SEARCHD_COMMAND_UPDATE      = 2;
332
    const SEARCHD_COMMAND_KEYWORDS    = 3;
333
    const SEARCHD_COMMAND_PERSIST     = 4;
334
    const SEARCHD_COMMAND_STATUS      = 5;
335
    const SEARCHD_COMMAND_FLUSH_ATTRS = 7;
336
337
    // current client-side command implementation versions
338
    const VER_COMMAND_SEARCH      = 0x11E;
339
    const VER_COMMAND_EXCERPT     = 0x104;
340
    const VER_COMMAND_UPDATE      = 0x103;
341
    const VER_COMMAND_KEYWORDS    = 0x100;
342
    const VER_COMMAND_STATUS      = 0x101;
343
    const VER_COMMAND_QUERY       = 0x100;
344
    const VER_COMMAND_FLUSH_ATTRS = 0x100;
345
346
    // known searchd status codes
347
    const SEARCHD_OK      = 0;
348
    const SEARCHD_ERROR   = 1;
349
    const SEARCHD_RETRY   = 2;
350
    const SEARCHD_WARNING = 3;
351
352
    // known match modes
353
    const MATCH_ALL        = 0;
354
    const MATCH_ANY        = 1;
355
    const MATCH_PHRASE     = 2;
356
    const MATCH_BOOLEAN    = 3;
357
    const MATCH_EXTENDED   = 4;
358
    const MATCH_FULL_SCAN  = 5;
359
    const MATCH_EXTENDED2  = 6; // extended engine V2 (TEMPORARY, WILL BE REMOVED)
360
361
    // known ranking modes (ext2 only)
362
    const RANK_PROXIMITY_BM25  = 0; // default mode, phrase proximity major factor and BM25 minor one
363
    const RANK_BM25            = 1; // statistical mode, BM25 ranking only (faster but worse quality)
364
    const RANK_NONE            = 2; // no ranking, all matches get a weight of 1
365
    const RANK_WORD_COUNT      = 3; // simple word-count weighting, rank is a weighted sum of per-field keyword
366
                                    // occurrence counts
367
    const RANK_PROXIMITY       = 4;
368
    const RANK_MATCH_ANY       = 5;
369
    const RANK_FIELD_MASK      = 6;
370
    const RANK_SPH04           = 7;
371
    const RANK_EXPR            = 8;
372
    const RANK_TOTAL           = 9;
373
374
    // known sort modes
375
    const SORT_RELEVANCE     = 0;
376
    const SORT_ATTR_DESC     = 1;
377
    const SORT_ATTR_ASC      = 2;
378
    const SORT_TIME_SEGMENTS = 3;
379
    const SORT_EXTENDED      = 4;
380
    const SORT_EXPR          = 5;
381
382
    // known filter types
383
    const FILTER_VALUES      = 0;
384
    const FILTER_RANGE       = 1;
385
    const FILTER_FLOAT_RANGE = 2;
386
    const FILTER_STRING      = 3;
387
388
    // known attribute types
389
    const ATTR_INTEGER   = 1;
390
    const ATTR_TIMESTAMP = 2;
391
    const ATTR_ORDINAL   = 3;
392
    const ATTR_BOOL      = 4;
393
    const ATTR_FLOAT     = 5;
394
    const ATTR_BIGINT    = 6;
395
    const ATTR_STRING    = 7;
396
    const ATTR_FACTORS   = 1001;
397
    const ATTR_MULTI     = 0x40000001;
398
    const ATTR_MULTI64   = 0x40000002;
399
400
    // known grouping functions
401
    const GROUP_BY_DAY       = 0;
402
    const GROUP_BY_WEEK      = 1;
403
    const GROUP_BY_MONTH     = 2;
404
    const GROUP_BY_YEAR      = 3;
405
    const GROUP_BY_ATTR      = 4;
406
    const GROUP_BY_ATTR_PAIR = 5;
407
408
    /////////////////////////////////////////////////////////////////////////////
409
    // common stuff
410
    /////////////////////////////////////////////////////////////////////////////
411
412 4
    public function __construct()
413
    {
414
        // default idf=tfidf_normalized
415 4
        $this->query_flags = setBit(0, 6, true);
416 4
    }
417
418
    public function __destruct()
419
    {
420
        if ($this->socket !== false) {
421
            fclose($this->socket);
422
        }
423
    }
424
425
    /**
426
     * @return string
427
     */
428 1
    public function getLastError()
429
    {
430 1
        return $this->error;
431
    }
432
433
    /**
434
     * @return string
435
     */
436 1
    public function getLastWarning()
437
    {
438 1
        return $this->warning;
439
    }
440
441
    /**
442
     * Get last error flag (to tell network connection errors from searchd errors or broken responses)
443
     *
444
     * @return bool
445
     */
446 1
    public function isConnectError()
447
    {
448 1
        return $this->conn_error;
449
    }
450
451
    /**
452
     * Set searchd host name and port
453
     *
454
     * @param string $host
455
     * @param int $port
456
     */
457
    public function setServer($host, $port = 0)
458
    {
459
        assert(is_string($host));
460
        if ($host[0] == '/') {
461
            $this->path = 'unix://' . $host;
462
            return;
463
        }
464
        if (substr($host, 0, 7) == 'unix://') {
465
            $this->path = $host;
466
            return;
467
        }
468
469
        $this->host = $host;
470
        $port = intval($port);
471
        assert(0 <= $port && $port < 65536);
472
        $this->port = $port == 0 ? 9312 : $port;
473
        $this->path = '';
474
    }
475
476
    /**
477
     * Set server connection timeout (0 to remove)
478
     *
479
     * @param int|float|string $timeout
480
     */
481
    public function setConnectTimeout($timeout)
482
    {
483
        assert(is_numeric($timeout));
484
        $this->timeout = $timeout;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timeout can also be of type string. However, the property $timeout is declared as type integer|double. 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...
485
    }
486
487
    /**
488
     * @param resource $handle
489
     * @param string $data
490
     * @param int $length
491
     *
492
     * @return bool
493
     */
494
    protected function send($handle, $data, $length)
495
    {
496
        if (feof($handle) || fwrite($handle, $data, $length) !== $length) {
497
            $this->error = 'connection unexpectedly closed (timed out?)';
498
            $this->conn_error = true;
499
            return false;
500
        }
501
        return true;
502
    }
503
504
    /////////////////////////////////////////////////////////////////////////////
505
506
    /**
507
     * Enter mbstring workaround mode
508
     */
509
    protected function mbPush()
510
    {
511
        $this->mbenc = '';
512
        if (ini_get('mbstring.func_overload') & 2) {
513
            $this->mbenc = mb_internal_encoding();
514
            mb_internal_encoding('latin1');
515
        }
516
    }
517
518
    /**
519
     * Leave mbstring workaround mode
520
     */
521
    protected function mbPop()
522
    {
523
        if ($this->mbenc) {
524
            mb_internal_encoding($this->mbenc);
525
        }
526
    }
527
528
    /**
529
     * Connect to searchd server
530
     *
531
     * @return bool|resource
532
     */
533
    protected function connect()
534
    {
535
        if (is_resource($this->socket)) {
536
            // we are in persistent connection mode, so we have a socket
537
            // however, need to check whether it's still alive
538
            if (!feof($this->socket)) {
539
                return $this->socket;
540
            }
541
542
            // force reopen
543
            $this->socket = false;
544
        }
545
546
        $errno = 0;
547
        $errstr = '';
548
        $this->conn_error = false;
549
550
        if ($this->path) {
551
            $host = $this->path;
552
            $port = 0;
553
        } else {
554
            $host = $this->host;
555
            $port = $this->port;
556
        }
557
558
        if ($this->timeout <= 0) {
559
            $fp = @fsockopen($host, $port, $errno, $errstr);
560
        } else {
561
            $fp = @fsockopen($host, $port, $errno, $errstr, $this->timeout);
562
        }
563
564
        if (!is_resource($fp)) {
565
            if ($this->path) {
566
                $location = $this->path;
567
            } else {
568
                $location = "{$this->host}:{$this->port}";
569
            }
570
571
            $errstr = trim($errstr);
572
            $this->error = "connection to $location failed (errno=$errno, msg=$errstr)";
573
            $this->conn_error = true;
574
            return false;
575
        }
576
577
        // send my version
578
        // this is a subtle part. we must do it before (!) reading back from searchd.
579
        // because otherwise under some conditions (reported on FreeBSD for instance)
580
        // TCP stack could throttle write-write-read pattern because of Nagle.
581
        if (!$this->send($fp, pack('N', 1), 4)) {
582
            fclose($fp);
583
            $this->error = 'failed to send client protocol version';
584
            return false;
585
        }
586
587
        // check version
588
        list(, $v) = unpack('N*', fread($fp, 4));
589
        $v = (int)$v;
590
        if ($v < 1) {
591
            fclose($fp);
592
            $this->error = "expected searchd protocol version 1+, got version '$v'";
593
            return false;
594
        }
595
596
        return $fp;
597
    }
598
599
    /**
600
     * Get and check response packet from searchd server
601
     *
602
     * @param resource $fp
603
     * @param int $client_ver
604
     *
605
     * @return bool|string
606
     */
607
    protected function getResponse($fp, $client_ver)
608
    {
609
        $ver = 0;
610
        $len = 0;
611
        $status = -1;
612
        $response = '';
613
614
        $header = fread($fp, 8);
615
        if (strlen($header) == 8) {
616
            list($status, $ver, $len) = array_values(unpack('n2a/Nb', $header));
617
            $left = $len;
618
            while ($left > 0 && !feof($fp)) {
619
                $chunk = fread($fp, min(8192, $left));
620
                if ($chunk) {
621
                    $response .= $chunk;
622
                    $left -= strlen($chunk);
623
                }
624
            }
625
        }
626
627
        if ($this->socket === false) {
628
            fclose($fp);
629
        }
630
631
        // check response
632
        $read = strlen($response);
633
        if (!$response || $read != $len) {
634
            if ($len) {
635
                $this->error = "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)";
636
            } else {
637
                $this->error = 'received zero-sized searchd response';
638
            }
639
            return false;
640
        }
641
642
        switch ($status) {
643
            case self::SEARCHD_WARNING:
644
                list(, $wlen) = unpack('N*', substr($response, 0, 4));
645
                $this->warning = substr($response, 4, $wlen);
646
                return substr($response, 4 + $wlen);
647
            case self::SEARCHD_ERROR:
648
                $this->error = 'searchd error: ' . substr($response, 4);
649
                return false;
650
            case self::SEARCHD_RETRY:
651
                $this->error = 'temporary searchd error: ' . substr($response, 4);
652
                return false;
653
            case self::SEARCHD_OK:
654
                if ($ver < $client_ver) { // check version
655
                    $this->warning = sprintf(
656
                        'searchd command v.%d.%d older than client\'s v.%d.%d, some options might not work',
657
                        $ver >> 8,
658
                        $ver & 0xff,
659
                        $client_ver >> 8,
660
                        $client_ver & 0xff
661
                    );
662
                }
663
664
                return $response;
665
            default:
666
                $this->error = "unknown status code '$status'";
667
                return false;
668
        }
669
    }
670
671
    /////////////////////////////////////////////////////////////////////////////
672
    // searching
673
    /////////////////////////////////////////////////////////////////////////////
674
675
    /**
676
     * Set offset and count into result set, and optionally set max-matches and cutoff limits
677
     *
678
     * @param int $offset
679
     * @param int $limit
680
     * @param int $max
681
     * @param int $cutoff
682
     */
683
    public function setLimits($offset, $limit, $max = 0, $cutoff = 0)
684
    {
685
        assert(is_int($offset));
686
        assert(is_int($limit));
687
        assert($offset >= 0);
688
        assert($limit > 0);
689
        assert($max >= 0);
690
        $this->offset = $offset;
691
        $this->limit = $limit;
692
        if ($max > 0) {
693
            $this->max_matches = $max;
694
        }
695
        if ($cutoff > 0) {
696
            $this->cutoff = $cutoff;
697
        }
698
    }
699
700
    /**
701
     * Set maximum query time, in milliseconds, per-index, 0 means 'do not limit'
702
     *
703
     * @param int $max
704
     */
705
    public function setMaxQueryTime($max)
706
    {
707
        assert(is_int($max));
708
        assert($max >= 0);
709
        $this->max_query_time = $max;
710
    }
711
712
    /**
713
     * Set matching mode
714
     *
715
     * @param int $mode
716
     */
717
    public function setMatchMode($mode)
718
    {
719
        trigger_error(
720
            'DEPRECATED: Do not call this method or, even better, use SphinxQL instead of an API',
721
            E_USER_DEPRECATED
722
        );
723
        assert(in_array($mode, array(
724
            self::MATCH_ALL,
725
            self::MATCH_ANY,
726
            self::MATCH_PHRASE,
727
            self::MATCH_BOOLEAN,
728
            self::MATCH_EXTENDED,
729
            self::MATCH_FULL_SCAN,
730
            self::MATCH_EXTENDED2
731
        )));
732
        $this->mode = $mode;
733
    }
734
735
    /**
736
     * Set ranking mode
737
     *
738
     * @param int $ranker
739
     * @param string $rank_expr
740
     */
741
    public function setRankingMode($ranker, $rank_expr='')
742
    {
743
        assert($ranker === 0 || $ranker >= 1 && $ranker < self::RANK_TOTAL);
744
        assert(is_string($rank_expr));
745
        $this->ranker = $ranker;
746
        $this->rank_expr = $rank_expr;
747
    }
748
749
    /**
750
     * Set matches sorting mode
751
     *
752
     * @param int $mode
753
     * @param string $sort_by
754
     */
755
    public function setSortMode($mode, $sort_by = '')
756
    {
757
        assert(in_array($mode, array(
758
            self::SORT_RELEVANCE,
759
            self::SORT_ATTR_DESC,
760
            self::SORT_ATTR_ASC,
761
            self::SORT_TIME_SEGMENTS,
762
            self::SORT_EXTENDED,
763
            self::SORT_EXPR
764
        )));
765
        assert(is_string($sort_by));
766
        assert($mode == self::SORT_RELEVANCE || strlen($sort_by) > 0);
767
768
        $this->sort = $mode;
769
        $this->sort_by = $sort_by;
770
    }
771
772
    /**
773
     * Bind per-field weights by order
774
     *
775
     * @deprecated use setFieldWeights() instead
776
     */
777 1
    public function setWeights()
778
    {
779 1
        throw new \RuntimeException('This method is now deprecated; please use setFieldWeights instead');
780
    }
781
782
    /**
783
     * Bind per-field weights by name
784
     *
785
     * @param array $weights
786
     */
787
    public function setFieldWeights(array $weights)
788
    {
789
        foreach ($weights as $name => $weight) {
790
            assert(is_string($name));
791
            assert(is_int($weight));
792
        }
793
        $this->field_weights = $weights;
794
    }
795
796
    /**
797
     * Bind per-index weights by name
798
     *
799
     * @param array $weights
800
     */
801
    public function setIndexWeights(array $weights)
802
    {
803
        foreach ($weights as $index => $weight) {
804
            assert(is_string($index));
805
            assert(is_int($weight));
806
        }
807
        $this->index_weights = $weights;
808
    }
809
810
    /**
811
     * Set IDs range to match. Only match records if document ID is beetwen $min and $max (inclusive)
812
     *
813
     * @param int $min
814
     * @param int $max
815
     */
816
    public function setIDRange($min, $max)
817
    {
818
        assert(is_numeric($min));
819
        assert(is_numeric($max));
820
        assert($min <= $max);
821
822
        $this->min_id = $min;
0 ignored issues
show
Documentation Bug introduced by
It seems like $min can also be of type double or string. However, the property $min_id is declared as type integer. 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...
823
        $this->max_id = $max;
0 ignored issues
show
Documentation Bug introduced by
It seems like $max can also be of type double or string. However, the property $max_id is declared as type integer. 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...
824
    }
825
826
    /**
827
     * Set values set filter. Only match records where $attribute value is in given set
828
     *
829
     * @param string $attribute
830
     * @param array $values
831
     * @param bool $exclude
832
     */
833
    public function setFilter($attribute, array $values, $exclude = false)
834
    {
835
        assert(is_string($attribute));
836
        assert(count($values));
837
838
        foreach ($values as $value) {
839
            assert(is_numeric($value));
840
        }
841
842
        $this->filters[] = array(
843
            'type' => self::FILTER_VALUES,
844
            'attr' => $attribute,
845
            'exclude' => $exclude,
846
            'values' => $values
847
        );
848
    }
849
850
    /**
851
     * Set string filter
852
     * Only match records where $attribute value is equal
853
     *
854
     * @param string $attribute
855
     * @param string $value
856
     * @param bool $exclude
857
     */
858
    public function setFilterString($attribute, $value, $exclude = false)
859
    {
860
        assert(is_string($attribute));
861
        assert(is_string($value));
862
        $this->filters[] = array(
863
            'type' => self::FILTER_STRING,
864
            'attr' => $attribute,
865
            'exclude' => $exclude,
866
            'value' => $value
867
        );
868
    }    
869
870
    /**
871
     * Set range filter
872
     * Only match records if $attribute value is beetwen $min and $max (inclusive)
873
     *
874
     * @param string $attribute
875
     * @param int $min
876
     * @param int $max
877
     * @param bool $exclude
878
     */
879
    public function setFilterRange($attribute, $min, $max, $exclude = false)
880
    {
881
        assert(is_string($attribute));
882
        assert(is_numeric($min));
883
        assert(is_numeric($max));
884
        assert($min <= $max);
885
886
        $this->filters[] = array(
887
            'type' => self::FILTER_RANGE,
888
            'attr' => $attribute,
889
            'exclude' => $exclude,
890
            'min' => $min,
891
            'max' => $max
892
        );
893
    }
894
895
    /**
896
     * Set float range filter
897
     * Only match records if $attribute value is beetwen $min and $max (inclusive)
898
     *
899
     * @param string $attribute
900
     * @param float $min
901
     * @param float $max
902
     * @param bool $exclude
903
     */
904
    public function setFilterFloatRange($attribute, $min, $max, $exclude = false)
905
    {
906
        assert(is_string($attribute));
907
        assert(is_float($min));
908
        assert(is_float($max));
909
        assert($min <= $max);
910
911
        $this->filters[] = array(
912
            'type' => self::FILTER_FLOAT_RANGE,
913
            'attr' => $attribute,
914
            'exclude' => $exclude,
915
            'min' => $min,
916
            'max' => $max
917
        );
918
    }
919
920
    /**
921
     * Setup anchor point for geosphere distance calculations
922
     * Required to use @geodist in filters and sorting
923
     * Latitude and longitude must be in radians
924
     *
925
     * @param string $attr_lat
926
     * @param string $attr_long
927
     * @param float $lat
928
     * @param float $long
929
     */
930
    public function setGeoAnchor($attr_lat, $attr_long, $lat, $long)
931
    {
932
        assert(is_string($attr_lat));
933
        assert(is_string($attr_long));
934
        assert(is_float($lat));
935
        assert(is_float($long));
936
937
        $this->anchor = array(
938
            'attrlat' => $attr_lat,
939
            'attrlong' => $attr_long,
940
            'lat' => $lat,
941
            'long' => $long
942
        );
943
    }
944
945
    /**
946
     * Set grouping attribute and function
947
     *
948
     * @param string $attribute
949
     * @param int $func
950
     * @param string $group_sort
951
     */
952
    public function setGroupBy($attribute, $func, $group_sort = '@group desc')
953
    {
954
        assert(is_string($attribute));
955
        assert(is_string($group_sort));
956
        assert(in_array($func, array(
957
            self::GROUP_BY_DAY,
958
            self::GROUP_BY_WEEK,
959
            self::GROUP_BY_MONTH,
960
            self::GROUP_BY_YEAR,
961
            self::GROUP_BY_ATTR,
962
            self::GROUP_BY_ATTR_PAIR
963
        )));
964
965
        $this->group_by = $attribute;
966
        $this->group_func = $func;
967
        $this->group_sort = $group_sort;
968
    }
969
970
    /**
971
     * Set count-distinct attribute for group-by queries
972
     *
973
     * @param string $attribute
974
     */
975
    public function setGroupDistinct($attribute)
976
    {
977
        assert(is_string($attribute));
978
        $this->group_distinct = $attribute;
979
    }
980
981
    /**
982
     * Set distributed retries count and delay
983
     *
984
     * @param int $count
985
     * @param int $delay
986
     */
987
    public function setRetries($count, $delay = 0)
988
    {
989
        assert(is_int($count) && $count >= 0);
990
        assert(is_int($delay) && $delay >= 0);
991
        $this->retry_count = $count;
992
        $this->retry_delay = $delay;
993
    }
994
995
    /**
996
     * Set result set format (hash or array; hash by default)
997
     * PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
998
     *
999
     * @param bool $array_result
1000
     */
1001
    public function setArrayResult($array_result)
1002
    {
1003
        assert(is_bool($array_result));
1004
        $this->array_result = $array_result;
1005
    }
1006
1007
    /**
1008
     * Set attribute values override
1009
     * There can be only one override per attribute
1010
     * $values must be a hash that maps document IDs to attribute values
1011
     *
1012
     * @deprecated Do not call this method. Use SphinxQL REMAP() function instead.
1013
     *
1014
     * @param string $attr_name
1015
     * @param string $attr_type
1016
     * @param array $values
1017
     */
1018
    public function setOverride($attr_name, $attr_type, array $values)
1019
    {
1020
        trigger_error(
1021
            'DEPRECATED: Do not call this method. Use SphinxQL REMAP() function instead.',
1022
            E_USER_DEPRECATED
1023
        );
1024
        assert(is_string($attr_name));
1025
        assert(in_array($attr_type, array(
1026
            self::ATTR_INTEGER,
1027
            self::ATTR_TIMESTAMP,
1028
            self::ATTR_BOOL,
1029
            self::ATTR_FLOAT,
1030
            self::ATTR_BIGINT
1031
        )));
1032
1033
        $this->overrides[$attr_name] = array(
1034
            'attr' => $attr_name,
1035
            'type' => $attr_type,
1036
            'values' => $values
1037
        );
1038
    }
1039
1040
    /**
1041
     * Set select-list (attributes or expressions), SQL-like syntax
1042
     *
1043
     * @param string $select
1044
     */
1045
    public function setSelect($select)
1046
    {
1047
        assert(is_string($select));
1048
        $this->select = $select;
1049
    }
1050
1051
    /**
1052
     * @param string $flag_name
1053
     * @param string|int $flag_value
1054
     */
1055
    public function setQueryFlag($flag_name, $flag_value)
1056
    {
1057
        $known_names = array(
1058
            'reverse_scan',
1059
            'sort_method',
1060
            'max_predicted_time',
1061
            'boolean_simplify',
1062
            'idf',
1063
            'global_idf',
1064
            'low_priority'
1065
        );
1066
        $flags = array (
1067
            'reverse_scan' => array(0, 1),
1068
            'sort_method' => array('pq', 'kbuffer'),
1069
            'max_predicted_time' => array(0),
1070
            'boolean_simplify' => array(true, false),
1071
            'idf' => array ('normalized', 'plain', 'tfidf_normalized', 'tfidf_unnormalized'),
1072
            'global_idf' => array(true, false),
1073
            'low_priority' => array(true, false)
1074
        );
1075
1076
        assert(isset($flag_name, $known_names));
1077
        assert(
1078
            in_array($flag_value, $flags[$flag_name], true) ||
1079
            ($flag_name == 'max_predicted_time' && is_int($flag_value) && $flag_value >= 0)
1080
        );
1081
1082
        switch ($flag_name) {
1083
            case 'reverse_scan':
1084
                $this->query_flags = setBit($this->query_flags, 0, $flag_value == 1);
1085
                break;
1086
            case 'sort_method':
1087
                $this->query_flags = setBit($this->query_flags, 1, $flag_value == 'kbuffer');
1088
                break;
1089
            case 'max_predicted_time':
1090
                $this->query_flags = setBit($this->query_flags, 2, $flag_value > 0);
1091
                $this->predicted_time = (int)$flag_value;
1092
                break;
1093
            case 'boolean_simplify':
1094
                $this->query_flags = setBit($this->query_flags, 3, $flag_value);
1095
                break;
1096
            case 'idf':
1097
                if ($flag_value == 'normalized' || $flag_value == 'plain') {
1098
                    $this->query_flags = setBit($this->query_flags, 4, $flag_value == 'plain');
1099
                }
1100
                if ($flag_value == 'tfidf_normalized' || $flag_value == 'tfidf_unnormalized') {
1101
                    $this->query_flags = setBit($this->query_flags, 6, $flag_value == 'tfidf_normalized');
1102
                }
1103
                break;
1104
            case 'global_idf':
1105
                $this->query_flags = setBit($this->query_flags, 5, $flag_value);
1106
                break;
1107
            case 'low_priority':
1108
                $this->query_flags = setBit($this->query_flags, 8, $flag_value);
1109
                break;
1110
        }
1111
    }
1112
1113
    /**
1114
     * Set outer order by parameters
1115
     *
1116
     * @param string $order_by
1117
     * @param int $offset
1118
     * @param int $limit
1119
     */
1120
    public function setOuterSelect($order_by, $offset, $limit)
1121
    {
1122
        assert(is_string($order_by));
1123
        assert(is_int($offset));
1124
        assert(is_int($limit));
1125
        assert($offset >= 0);
1126
        assert($limit > 0);
1127
1128
        $this->outer_order_by = $order_by;
1129
        $this->outer_offset = $offset;
1130
        $this->outer_limit = $limit;
1131
        $this->has_outer = true;
1132
    }
1133
1134
1135
    //////////////////////////////////////////////////////////////////////////////
1136
1137
    /**
1138
     * Clear all filters (for multi-queries)
1139
     */
1140
    public function resetFilters()
1141
    {
1142
        $this->filters = array();
1143
        $this->anchor = array();
1144
    }
1145
1146
    /**
1147
     * Clear groupby settings (for multi-queries)
1148
     */
1149
    public function resetGroupBy()
1150
    {
1151
        $this->group_by = '';
1152
        $this->group_func = self::GROUP_BY_DAY;
1153
        $this->group_sort = '@group desc';
1154
        $this->group_distinct = '';
1155
    }
1156
1157
    /**
1158
     * Clear all attribute value overrides (for multi-queries)
1159
     */
1160
    public function resetOverrides()
1161
    {
1162
        $this->overrides = array();
1163
    }
1164
1165
    public function resetQueryFlag()
1166
    {
1167
        $this->query_flags = setBit(0, 6, true); // default idf=tfidf_normalized
1168
        $this->predicted_time = 0;
1169
    }
1170
1171
    public function resetOuterSelect()
1172
    {
1173
        $this->outer_order_by = '';
1174
        $this->outer_offset = 0;
1175
        $this->outer_limit = 0;
1176
        $this->has_outer = false;
1177
    }
1178
1179
    //////////////////////////////////////////////////////////////////////////////
1180
1181
    /**
1182
     * Connect to searchd server, run given search query through given indexes, and return the search results
1183
     *
1184
     * @param string  $query
1185
     * @param string $index
1186
     * @param string $comment
1187
     *
1188
     * @return bool
1189
     */
1190
    public function query($query, $index = '*', $comment = '')
1191
    {
1192
        assert(empty($this->reqs));
1193
1194
        $this->addQuery($query, $index, $comment);
1195
        $results = $this->runQueries();
1196
        $this->reqs = array(); // just in case it failed too early
1197
1198
        if (!is_array($results)) {
1199
            return false; // probably network error; error message should be already filled
1200
        }
1201
1202
        $this->error = $results[0]['error'];
1203
        $this->warning = $results[0]['warning'];
1204
1205
        if ($results[0]['status'] == self::SEARCHD_ERROR) {
1206
            return false;
1207
        } else {
1208
            return $results[0];
1209
        }
1210
    }
1211
1212
    /**
1213
     * Helper to pack floats in network byte order
1214
     *
1215
     * @param float $float
1216
     *
1217
     * @return string
1218
     */
1219
    protected function packFloat($float)
1220
    {
1221
        $t1 = pack('f', $float); // machine order
1222
        list(, $t2) = unpack('L*', $t1); // int in machine order
1223
        return pack('N', $t2);
1224
    }
1225
1226
    /**
1227
     * Add query to multi-query batch
1228
     * Returns index into results array from RunQueries() call
1229
     *
1230
     * @param string $query
1231
     * @param string $index
1232
     * @param string $comment
1233
     *
1234
     * @return int
1235
     */
1236
    public function addQuery($query, $index = '*', $comment = '')
1237
    {
1238
        // mbstring workaround
1239
        $this->mbPush();
1240
1241
        // build request
1242
        $req = pack('NNNNN', $this->query_flags, $this->offset, $this->limit, $this->mode, $this->ranker);
1243
        if ($this->ranker == self::RANK_EXPR) {
1244
            $req .= pack('N', strlen($this->rank_expr)) . $this->rank_expr;
1245
        }
1246
        $req .= pack('N', $this->sort); // (deprecated) sort mode
1247
        $req .= pack('N', strlen($this->sort_by)) . $this->sort_by;
1248
        $req .= pack('N', strlen($query)) . $query; // query itself
1249
        $req .= pack('N', count($this->weights)); // weights
1250
        foreach ($this->weights as $weight) {
1251
            $req .= pack('N', (int)$weight);
1252
        }
1253
        $req .= pack('N', strlen($index)) . $index; // indexes
1254
        $req .= pack('N', 1); // id64 range marker
1255
        $req .= pack64IntUnsigned($this->min_id) . pack64IntUnsigned($this->max_id); // id64 range
1256
1257
        // filters
1258
        $req .= pack('N', count($this->filters));
1259
        foreach ($this->filters as $filter) {
1260
            $req .= pack('N', strlen($filter['attr'])) . $filter['attr'];
1261
            $req .= pack('N', $filter['type']);
1262
            switch ($filter['type']) {
1263
                case self::FILTER_VALUES:
1264
                    $req .= pack('N', count($filter['values']));
1265
                    foreach ($filter['values'] as $value) {
1266
                        $req .= pack64IntSigned($value);
1267
                    }
1268
                    break;
1269
                case self::FILTER_RANGE:
1270
                    $req .= pack64IntSigned($filter['min']) . pack64IntSigned($filter['max']);
1271
                    break;
1272
                case self::FILTER_FLOAT_RANGE:
1273
                    $req .= $this->packFloat($filter['min']) . $this->packFloat($filter['max']);
1274
                    break;
1275
                case self::FILTER_STRING:
1276
                    $req .= pack('N', strlen($filter['value'])) . $filter['value'];
1277
                    break;
1278
                default:
1279
                    assert(0 && 'internal error: unhandled filter type');
1280
            }
1281
            $req .= pack('N', $filter['exclude']);
1282
        }
1283
1284
        // group-by clause, max-matches count, group-sort clause, cutoff count
1285
        $req .= pack('NN', $this->group_func, strlen($this->group_by)) . $this->group_by;
1286
        $req .= pack('N', $this->max_matches);
1287
        $req .= pack('N', strlen($this->group_sort)) . $this->group_sort;
1288
        $req .= pack('NNN', $this->cutoff, $this->retry_count, $this->retry_delay);
1289
        $req .= pack('N', strlen($this->group_distinct)) . $this->group_distinct;
1290
1291
        // anchor point
1292
        if (empty($this->anchor)) {
1293
            $req .= pack('N', 0);
1294
        } else {
1295
            $a =& $this->anchor;
1296
            $req .= pack('N', 1);
1297
            $req .= pack('N', strlen($a['attrlat'])) . $a['attrlat'];
1298
            $req .= pack('N', strlen($a['attrlong'])) . $a['attrlong'];
1299
            $req .= $this->packFloat($a['lat']) . $this->packFloat($a['long']);
1300
        }
1301
1302
        // per-index weights
1303
        $req .= pack('N', count($this->index_weights));
1304
        foreach ($this->index_weights as $idx => $weight) {
1305
            $req .= pack('N', strlen($idx)) . $idx . pack('N', $weight);
1306
        }
1307
1308
        // max query time
1309
        $req .= pack('N', $this->max_query_time);
1310
1311
        // per-field weights
1312
        $req .= pack('N', count($this->field_weights));
1313
        foreach ($this->field_weights as $field => $weight) {
1314
            $req .= pack('N', strlen($field)) . $field . pack('N', $weight);
1315
        }
1316
1317
        // comment
1318
        $req .= pack('N', strlen($comment)) . $comment;
1319
1320
        // attribute overrides
1321
        $req .= pack('N', count($this->overrides));
1322
        foreach ($this->overrides as $key => $entry) {
1323
            $req .= pack('N', strlen($entry['attr'])) . $entry['attr'];
1324
            $req .= pack('NN', $entry['type'], count($entry['values']));
1325
            foreach ($entry['values'] as $id => $val) {
1326
                assert(is_numeric($id));
1327
                assert(is_numeric($val));
1328
1329
                $req .= pack64IntUnsigned($id);
1330
                switch ($entry['type']) {
1331
                    case self::ATTR_FLOAT:
1332
                        $req .= $this->packFloat($val);
1333
                        break;
1334
                    case self::ATTR_BIGINT:
1335
                        $req .= pack64IntSigned($val);
1336
                        break;
1337
                    default:
1338
                        $req .= pack('N', $val);
1339
                        break;
1340
                }
1341
            }
1342
        }
1343
1344
        // select-list
1345
        $req .= pack('N', strlen($this->select)) . $this->select;
1346
1347
        // max_predicted_time
1348
        if ($this->predicted_time > 0) {
1349
            $req .= pack('N', (int)$this->predicted_time);
1350
        }
1351
1352
        $req .= pack('N', strlen($this->outer_order_by)) . $this->outer_order_by;
1353
        $req .= pack('NN', $this->outer_offset, $this->outer_limit);
1354
        if ($this->has_outer) {
1355
            $req .= pack('N', 1);
1356
        } else {
1357
            $req .= pack('N', 0);
1358
        }
1359
1360
        // mbstring workaround
1361
        $this->mbPop();
1362
1363
        // store request to requests array
1364
        $this->reqs[] = $req;
1365
        return count($this->reqs) - 1;
1366
    }
1367
1368
    /**
1369
     * Connect to searchd, run queries batch, and return an array of result sets
1370
     *
1371
     * @return array|bool
1372
     */
1373
    public function runQueries()
1374
    {
1375
        if (empty($this->reqs)) {
1376
            $this->error = 'no queries defined, issue AddQuery() first';
1377
            return false;
1378
        }
1379
1380
        // mbstring workaround
1381
        $this->mbPush();
1382
1383
        if (($fp = $this->connect()) === false) {
1384
            $this->mbPop();
1385
            return false;
1386
        }
1387
1388
        // send query, get response
1389
        $nreqs = count($this->reqs);
1390
        $req = join('', $this->reqs);
1391
        $len = 8 + strlen($req);
1392
        // add header
1393
        $req = pack('nnNNN', self::SEARCHD_COMMAND_SEARCH, self::VER_COMMAND_SEARCH, $len, 0, $nreqs) . $req;
1394
1395
        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_SEARCH))) {
1396
            $this->mbPop();
1397
            return false;
1398
        }
1399
1400
        // query sent ok; we can reset reqs now
1401
        $this->reqs = array();
1402
1403
        // parse and return response
1404
        return $this->parseSearchResponse($response, $nreqs);
1405
    }
1406
1407
    /**
1408
     * Parse and return search query (or queries) response
1409
     *
1410
     * @param string $response
1411
     * @param int $nreqs
1412
     *
1413
     * @return array
1414
     */
1415
    protected function parseSearchResponse($response, $nreqs)
1416
    {
1417
        $p = 0; // current position
1418
        $max = strlen($response); // max position for checks, to protect against broken responses
1419
1420
        $results = array();
1421
        for ($ires = 0; $ires < $nreqs && $p < $max; $ires++) {
1422
            $results[] = array();
1423
            $result =& $results[$ires];
1424
1425
            $result['error'] = '';
1426
            $result['warning'] = '';
1427
1428
            // extract status
1429
            list(, $status) = unpack('N*', substr($response, $p, 4));
1430
            $p += 4;
1431
            $result['status'] = $status;
1432
            if ($status != self::SEARCHD_OK) {
1433
                list(, $len) = unpack('N*', substr($response, $p, 4));
1434
                $p += 4;
1435
                $message = substr($response, $p, $len);
1436
                $p += $len;
1437
1438
                if ($status == self::SEARCHD_WARNING) {
1439
                    $result['warning'] = $message;
1440
                } else {
1441
                    $result['error'] = $message;
1442
                    continue;
1443
                }
1444
            }
1445
1446
            // read schema
1447
            $fields = array();
1448
            $attrs = array();
1449
1450
            list(, $nfields) = unpack('N*', substr($response, $p, 4));
1451
            $p += 4;
1452
            while ($nfields --> 0 && $p < $max) {
1453
                list(, $len) = unpack('N*', substr($response, $p, 4));
1454
                $p += 4;
1455
                $fields[] = substr($response, $p, $len);
1456
                $p += $len;
1457
            }
1458
            $result['fields'] = $fields;
1459
1460
            list(, $n_attrs) = unpack('N*', substr($response, $p, 4));
1461
            $p += 4;
1462
            while ($n_attrs --> 0 && $p < $max) {
1463
                list(, $len) = unpack('N*', substr($response, $p, 4));
1464
                $p += 4;
1465
                $attr = substr($response, $p, $len);
1466
                $p += $len;
1467
                list(, $type) = unpack('N*', substr($response, $p, 4));
1468
                $p += 4;
1469
                $attrs[$attr] = $type;
1470
            }
1471
            $result['attrs'] = $attrs;
1472
1473
            // read match count
1474
            list(, $count) = unpack('N*', substr($response, $p, 4));
1475
            $p += 4;
1476
            list(, $id64) = unpack('N*', substr($response, $p, 4));
1477
            $p += 4;
1478
1479
            // read matches
1480
            $idx = -1;
1481
            while ($count --> 0 && $p < $max) {
1482
                // index into result array
1483
                $idx++;
1484
1485
                // parse document id and weight
1486
                if ($id64) {
1487
                    $doc = unpack64IntUnsigned(substr($response, $p, 8));
1488
                    $p += 8;
1489
                    list(,$weight) = unpack('N*', substr($response, $p, 4));
1490
                    $p += 4;
1491
                } else {
1492
                    list($doc, $weight) = array_values(unpack('N*N*', substr($response, $p, 8)));
1493
                    $p += 8;
1494
                    $doc = fixUInt($doc);
1495
                }
1496
                $weight = sprintf('%u', $weight);
1497
1498
                // create match entry
1499
                if ($this->array_result) {
1500
                    $result['matches'][$idx] = array('id' => $doc, 'weight' => $weight);
1501
                } else {
1502
                    $result['matches'][$doc]['weight'] = $weight;
1503
                }
1504
1505
                // parse and create attributes
1506
                $attr_values = array();
1507
                foreach ($attrs as $attr => $type) {
1508
                    // handle 64bit int
1509
                    if ($type == self::ATTR_BIGINT) {
1510
                        $attr_values[$attr] = unpack64IntSigned(substr($response, $p, 8));
1511
                        $p += 8;
1512
                        continue;
1513
                    }
1514
1515
                    // handle floats
1516
                    if ($type == self::ATTR_FLOAT) {
1517
                        list(, $u_value) = unpack('N*', substr($response, $p, 4));
1518
                        $p += 4;
1519
                        list(, $f_value) = unpack('f*', pack('L', $u_value));
1520
                        $attr_values[$attr] = $f_value;
1521
                        continue;
1522
                    }
1523
1524
                    // handle everything else as unsigned int
1525
                    list(, $val) = unpack('N*', substr($response, $p, 4));
1526
                    $p += 4;
1527
                    if ($type == self::ATTR_MULTI) {
1528
                        $attr_values[$attr] = array();
1529
                        $n_values = $val;
1530 View Code Duplication
                        while ($n_values --> 0 && $p < $max) {
1531
                            list(, $val) = unpack('N*', substr($response, $p, 4));
1532
                            $p += 4;
1533
                            $attr_values[$attr][] = fixUInt($val);
1534
                        }
1535
                    } elseif ($type == self::ATTR_MULTI64) {
1536
                        $attr_values[$attr] = array();
1537
                        $n_values = $val;
1538 View Code Duplication
                        while ($n_values > 0 && $p < $max) {
1539
                            $attr_values[$attr][] = unpack64IntSigned(substr($response, $p, 8));
1540
                            $p += 8;
1541
                            $n_values -= 2;
1542
                        }
1543
                    } elseif ($type == self::ATTR_STRING) {
1544
                        $attr_values[$attr] = substr($response, $p, $val);
1545
                        $p += $val;
1546
                    } elseif ($type == self::ATTR_FACTORS) {
1547
                        $attr_values[$attr] = substr($response, $p, $val - 4);
1548
                        $p += $val-4;
1549
                    } else {
1550
                        $attr_values[$attr] = fixUInt($val);
1551
                    }
1552
                }
1553
1554
                if ($this->array_result) {
1555
                    $result['matches'][$idx]['attrs'] = $attr_values;
1556
                } else {
1557
                    $result['matches'][$doc]['attrs'] = $attr_values;
1558
                }
1559
            }
1560
1561
            list($total, $total_found, $msecs, $words) = array_values(unpack('N*N*N*N*', substr($response, $p, 16)));
1562
            $result['total'] = sprintf('%u', $total);
1563
            $result['total_found'] = sprintf('%u', $total_found);
1564
            $result['time'] = sprintf('%.3f', $msecs / 1000);
1565
            $p += 16;
1566
1567
            while ($words --> 0 && $p < $max) {
1568
                list(, $len) = unpack('N*', substr($response, $p, 4));
1569
                $p += 4;
1570
                $word = substr($response, $p, $len);
1571
                $p += $len;
1572
                list($docs, $hits) = array_values(unpack('N*N*', substr($response, $p, 8)));
1573
                $p += 8;
1574
                $result['words'][$word] = array (
1575
                    'docs' => sprintf('%u', $docs),
1576
                    'hits' => sprintf('%u', $hits)
1577
                );
1578
            }
1579
        }
1580
1581
        $this->mbPop();
1582
        return $results;
1583
    }
1584
1585
    /////////////////////////////////////////////////////////////////////////////
1586
    // excerpts generation
1587
    /////////////////////////////////////////////////////////////////////////////
1588
1589
    /**
1590
     * Connect to searchd server, and generate exceprts (snippets) of given documents for given query.
1591
     * Returns false on failure, an array of snippets on success
1592
     *
1593
     * @param array $docs
1594
     * @param string $index
1595
     * @param string $words
1596
     * @param array $opts
1597
     *
1598
     * @return array|bool
1599
     */
1600
    public function buildExcerpts(array $docs, $index, $words, array $opts = array())
1601
    {
1602
        assert(is_string($index));
1603
        assert(is_string($words));
1604
1605
        $this->mbPush();
1606
1607
        if (($fp = $this->connect()) === false) {
1608
            $this->mbPop();
1609
            return false;
1610
        }
1611
1612
        /////////////////
1613
        // fixup options
1614
        /////////////////
1615
1616
        $opts = array_merge(array(
1617
            'before_match' => '<b>',
1618
            'after_match' => '</b>',
1619
            'chunk_separator' => ' ... ',
1620
            'limit' => 256,
1621
            'limit_passages' => 0,
1622
            'limit_words' => 0,
1623
            'around' => 5,
1624
            'exact_phrase' => false,
1625
            'single_passage' => false,
1626
            'use_boundaries' => false,
1627
            'weight_order' => false,
1628
            'query_mode' => false,
1629
            'force_all_words' => false,
1630
            'start_passage_id' => 1,
1631
            'load_files' => false,
1632
            'html_strip_mode' => 'index',
1633
            'allow_empty' => false,
1634
            'passage_boundary' => 'none',
1635
            'emit_zones' => false,
1636
            'load_files_scattered' => false
1637
        ), $opts);
1638
1639
        /////////////////
1640
        // build request
1641
        /////////////////
1642
1643
        // v.1.2 req
1644
        $flags = 1; // remove spaces
1645
        if ($opts['exact_phrase']) {
1646
            $flags |= 2;
1647
        }
1648
        if ($opts['single_passage']) {
1649
            $flags |= 4;
1650
        }
1651
        if ($opts['use_boundaries']) {
1652
            $flags |= 8;
1653
        }
1654
        if ($opts['weight_order']) {
1655
            $flags |= 16;
1656
        }
1657
        if ($opts['query_mode']) {
1658
            $flags |= 32;
1659
        }
1660
        if ($opts['force_all_words']) {
1661
            $flags |= 64;
1662
        }
1663
        if ($opts['load_files']) {
1664
            $flags |= 128;
1665
        }
1666
        if ($opts['allow_empty']) {
1667
            $flags |= 256;
1668
        }
1669
        if ($opts['emit_zones']) {
1670
            $flags |= 512;
1671
        }
1672
        if ($opts['load_files_scattered']) {
1673
            $flags |= 1024;
1674
        }
1675
        $req = pack('NN', 0, $flags); // mode=0, flags=$flags
1676
        $req .= pack('N', strlen($index)) . $index; // req index
1677
        $req .= pack('N', strlen($words)) . $words; // req words
1678
1679
        // options
1680
        $req .= pack('N', strlen($opts['before_match'])) . $opts['before_match'];
1681
        $req .= pack('N', strlen($opts['after_match'])) . $opts['after_match'];
1682
        $req .= pack('N', strlen($opts['chunk_separator'])) . $opts['chunk_separator'];
1683
        $req .= pack('NN', (int)$opts['limit'], (int)$opts['around']);
1684
        // v.1.2
1685
        $req .= pack('NNN', (int)$opts['limit_passages'], (int)$opts['limit_words'], (int)$opts['start_passage_id']);
1686
        $req .= pack('N', strlen($opts['html_strip_mode'])) . $opts['html_strip_mode'];
1687
        $req .= pack('N', strlen($opts['passage_boundary'])) . $opts['passage_boundary'];
1688
1689
        // documents
1690
        $req .= pack('N', count($docs));
1691
        foreach ($docs as $doc) {
1692
            assert(is_string($doc));
1693
            $req .= pack('N', strlen($doc)) . $doc;
1694
        }
1695
1696
        ////////////////////////////
1697
        // send query, get response
1698
        ////////////////////////////
1699
1700
        $len = strlen($req);
1701
        $req = pack('nnN', self::SEARCHD_COMMAND_EXCERPT, self::VER_COMMAND_EXCERPT, $len) . $req; // add header
1702
        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_EXCERPT))) {
1703
            $this->mbPop();
1704
            return false;
1705
        }
1706
1707
        //////////////////
1708
        // parse response
1709
        //////////////////
1710
1711
        $pos = 0;
1712
        $res = array();
1713
        $rlen = strlen($response);
1714
        $count = count($docs);
1715
        while ($count--) {
1716
            list(, $len) = unpack('N*', substr($response, $pos, 4));
1717
            $pos += 4;
1718
1719
            if ($pos + $len > $rlen) {
1720
                $this->error = 'incomplete reply';
1721
                $this->mbPop();
1722
                return false;
1723
            }
1724
            $res[] = $len ? substr($response, $pos, $len) : '';
1725
            $pos += $len;
1726
        }
1727
1728
        $this->mbPop();
1729
        return $res;
1730
    }
1731
1732
1733
    /////////////////////////////////////////////////////////////////////////////
1734
    // keyword generation
1735
    /////////////////////////////////////////////////////////////////////////////
1736
1737
    /**
1738
     * Connect to searchd server, and generate keyword list for a given query returns false on failure,
1739
     * an array of words on success
1740
     *
1741
     * @param string $query
1742
     * @param string $index
1743
     * @param bool $hits
1744
     *
1745
     * @return array|bool
1746
     */
1747
    public function buildKeywords($query, $index, $hits)
1748
    {
1749
        assert(is_string($query));
1750
        assert(is_string($index));
1751
        assert(is_bool($hits));
1752
1753
        $this->mbPush();
1754
1755
        if (($fp = $this->connect()) === false) {
1756
            $this->mbPop();
1757
            return false;
1758
        }
1759
1760
        /////////////////
1761
        // build request
1762
        /////////////////
1763
1764
        // v.1.0 req
1765
        $req  = pack('N', strlen($query)) . $query; // req query
1766
        $req .= pack('N', strlen($index)) . $index; // req index
1767
        $req .= pack('N', (int)$hits);
1768
1769
        ////////////////////////////
1770
        // send query, get response
1771
        ////////////////////////////
1772
1773
        $len = strlen($req);
1774
        $req = pack('nnN', self::SEARCHD_COMMAND_KEYWORDS, self::VER_COMMAND_KEYWORDS, $len) . $req; // add header
1775
        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_KEYWORDS))) {
1776
            $this->mbPop();
1777
            return false;
1778
        }
1779
1780
        //////////////////
1781
        // parse response
1782
        //////////////////
1783
1784
        $pos = 0;
1785
        $res = array();
1786
        $rlen = strlen($response);
1787
        list(, $nwords) = unpack('N*', substr($response, $pos, 4));
1788
        $pos += 4;
1789
        for ($i = 0; $i < $nwords; $i++) {
1790
            list(, $len) = unpack('N*', substr($response, $pos, 4));
1791
            $pos += 4;
1792
            $tokenized = $len ? substr($response, $pos, $len) : '';
1793
            $pos += $len;
1794
1795
            list(, $len) = unpack('N*', substr($response, $pos, 4));
1796
            $pos += 4;
1797
            $normalized = $len ? substr($response, $pos, $len) : '';
1798
            $pos += $len;
1799
1800
            $res[] = array(
1801
                'tokenized' => $tokenized,
1802
                'normalized' => $normalized
1803
            );
1804
1805
            if ($hits) {
1806
                list($ndocs, $nhits) = array_values(unpack('N*N*', substr($response, $pos, 8)));
1807
                $pos += 8;
1808
                $res[$i]['docs'] = $ndocs;
1809
                $res[$i]['hits'] = $nhits;
1810
            }
1811
1812
            if ($pos > $rlen) {
1813
                $this->error = 'incomplete reply';
1814
                $this->mbPop();
1815
                return false;
1816
            }
1817
        }
1818
1819
        $this->mbPop();
1820
        return $res;
1821
    }
1822
1823
    /**
1824
     * @param string $string
1825
     *
1826
     * @return string
1827
     */
1828
    public function escapeString($string)
1829
    {
1830
        $from = array('\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', '<');
1831
        $to   = array('\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=', '\<');
1832
1833
        return str_replace($from, $to, $string);
1834
    }
1835
1836
    /////////////////////////////////////////////////////////////////////////////
1837
    // attribute updates
1838
    /////////////////////////////////////////////////////////////////////////////
1839
1840
    /**
1841
     * Batch update given attributes in given rows in given indexes
1842
     * Returns amount of updated documents (0 or more) on success, or -1 on failure
1843
     *
1844
     * @param string $index
1845
     * @param array $attrs
1846
     * @param array $values
1847
     * @param bool $mva
1848
     * @param bool $ignore_non_existent
1849
     *
1850
     * @return int
1851
     */
1852
    public function updateAttributes($index, array $attrs, array $values, $mva = false, $ignore_non_existent = false)
1853
    {
1854
        // verify everything
1855
        assert(is_string($index));
1856
        assert(is_bool($mva));
1857
        assert(is_bool($ignore_non_existent));
1858
1859
        foreach ($attrs as $attr) {
1860
            assert(is_string($attr));
1861
        }
1862
1863
        foreach ($values as $id => $entry) {
1864
            assert(is_numeric($id));
1865
            assert(is_array($entry));
1866
            assert(count($entry) == count($attrs));
1867
            foreach ($entry as $v) {
1868
                if ($mva) {
1869
                    assert(is_array($v));
1870
                    foreach ($v as $vv) {
1871
                        assert(is_int($vv));
1872
                    }
1873
                } else {
1874
                    assert(is_int($v));
1875
                }
1876
            }
1877
        }
1878
1879
        // build request
1880
        $this->mbPush();
1881
        $req = pack('N', strlen($index)) . $index;
1882
1883
        $req .= pack('N', count($attrs));
1884
        $req .= pack('N', $ignore_non_existent ? 1 : 0);
1885
        foreach ($attrs as $attr) {
1886
            $req .= pack('N', strlen($attr)) . $attr;
1887
            $req .= pack('N', $mva ? 1 : 0);
1888
        }
1889
1890
        $req .= pack('N', count($values));
1891
        foreach ($values as $id => $entry) {
1892
            $req .= pack64IntUnsigned($id);
1893
            foreach ($entry as $v) {
1894
                $req .= pack('N', $mva ? count($v) : $v);
1895
                if ($mva) {
1896
                    foreach ($v as $vv) {
1897
                        $req .= pack('N', $vv);
1898
                    }
1899
                }
1900
            }
1901
        }
1902
1903
        // connect, send query, get response
1904
        if (($fp = $this->connect()) === false) {
1905
            $this->mbPop();
1906
            return -1;
1907
        }
1908
1909
        $len = strlen($req);
1910
        $req = pack('nnN', self::SEARCHD_COMMAND_UPDATE, self::VER_COMMAND_UPDATE, $len) . $req; // add header
1911
        if (!$this->send($fp, $req, $len + 8)) {
1912
            $this->mbPop();
1913
            return -1;
1914
        }
1915
1916
        if (!($response = $this->getResponse($fp, self::VER_COMMAND_UPDATE))) {
1917
            $this->mbPop();
1918
            return -1;
1919
        }
1920
1921
        // parse response
1922
        list(, $updated) = unpack('N*', substr($response, 0, 4));
1923
        $this->mbPop();
1924
        return $updated;
1925
    }
1926
1927
    /////////////////////////////////////////////////////////////////////////////
1928
    // persistent connections
1929
    /////////////////////////////////////////////////////////////////////////////
1930
1931
    /**
1932
     * @return bool
1933
     */
1934
    public function open()
1935
    {
1936
        if ($this->socket !== false) {
1937
            $this->error = 'already connected';
1938
            return false;
1939
        }
1940
        if (($fp = $this->connect()) === false)
1941
            return false;
1942
1943
        // command, command version = 0, body length = 4, body = 1
1944
        $req = pack('nnNN', self::SEARCHD_COMMAND_PERSIST, 0, 4, 1);
1945
        if (!$this->send($fp, $req, 12)) {
1946
            return false;
1947
        }
1948
1949
        $this->socket = $fp;
1950
        return true;
1951
    }
1952
1953
    /**
1954
     * @return bool
1955
     */
1956
    public function close()
1957
    {
1958
        if ($this->socket === false) {
1959
            $this->error = 'not connected';
1960
            return false;
1961
        }
1962
1963
        fclose($this->socket);
1964
        $this->socket = false;
1965
1966
        return true;
1967
    }
1968
1969
    //////////////////////////////////////////////////////////////////////////
1970
    // status
1971
    //////////////////////////////////////////////////////////////////////////
1972
1973
    /**
1974
     * @param bool $session
1975
     *
1976
     * @return array|bool
1977
     */
1978
    public function status($session = false)
1979
    {
1980
        assert(is_bool($session));
1981
1982
        $this->mbPush();
1983
        if (($fp = $this->connect()) === false) {
1984
            $this->mbPop();
1985
            return false;
1986
        }
1987
1988
        // len=4, body=1
1989
        $req = pack('nnNN', self::SEARCHD_COMMAND_STATUS, self::VER_COMMAND_STATUS, 4, $session ? 0 : 1);
1990
        if (!$this->send($fp, $req, 12) || !($response = $this->getResponse($fp, self::VER_COMMAND_STATUS))) {
1991
            $this->mbPop();
1992
            return false;
1993
        }
1994
1995
        $res = substr($response, 4); // just ignore length, error handling, etc
0 ignored issues
show
Unused Code introduced by
$res is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1996
        $p = 0;
1997
        list($rows, $cols) = array_values(unpack('N*N*', substr($response, $p, 8)));
1998
        $p += 8;
1999
2000
        $res = array();
2001
        for ($i = 0; $i < $rows; $i++) {
2002
            for ($j = 0; $j < $cols; $j++) {
2003
                list(, $len) = unpack('N*', substr($response, $p, 4));
2004
                $p += 4;
2005
                $res[$i][] = substr($response, $p, $len);
2006
                $p += $len;
2007
            }
2008
        }
2009
2010
        $this->mbPop();
2011
        return $res;
2012
    }
2013
2014
    //////////////////////////////////////////////////////////////////////////
2015
    // flush
2016
    //////////////////////////////////////////////////////////////////////////
2017
2018
    /**
2019
     * @return int
2020
     */
2021
    public function flushAttributes()
2022
    {
2023
        $this->mbPush();
2024
        if (($fp = $this->connect()) === false) {
2025
            $this->mbPop();
2026
            return -1;
2027
        }
2028
2029
        $req = pack('nnN', self::SEARCHD_COMMAND_FLUSH_ATTRS, self::VER_COMMAND_FLUSH_ATTRS, 0); // len=0
2030
        if (!$this->send($fp, $req, 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_FLUSH_ATTRS))) {
2031
            $this->mbPop();
2032
            return -1;
2033
        }
2034
2035
        $tag = -1;
2036
        if (strlen($response) == 4) {
2037
            list(, $tag) = unpack('N*', $response);
2038
        } else {
2039
            $this->error = 'unexpected response length';
2040
        }
2041
2042
        $this->mbPop();
2043
        return $tag;
2044
    }
2045
}
2046