Completed
Push — master ( 7f247a...38af16 )
by Peter
02:17
created

SphinxClient::getLastError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
//
4
// $Id$
5
//
6
7
//
8
// Copyright (c) 2001-2015, Andrew Aksyonoff
9
// Copyright (c) 2008-2015, Sphinx Technologies Inc
10
// All rights reserved
11
//
12
// This program is free software; you can redistribute it and/or modify
13
// it under the terms of the GNU Library General Public License. You should
14
// have received a copy of the LGPL license along with this program; if you
15
// did not, you can find it at http://www.gnu.org/
16
//
17
// WARNING!!!
18
//
19
// As of 2015, we strongly recommend to use either SphinxQL or REST APIs
20
// rather than the native SphinxAPI.
21
//
22
// While both the native SphinxAPI protocol and the existing APIs will
23
// continue to exist, and perhaps should not even break (too much), exposing
24
// all the new features via multiple different native API implementations
25
// is too much of a support complication for us.
26
//
27
// That said, you're welcome to overtake the maintenance of any given
28
// official API, and remove this warning ;)
29
//
30
31
/////////////////////////////////////////////////////////////////////////////
32
// PHP version of Sphinx searchd client (PHP API)
33
/////////////////////////////////////////////////////////////////////////////
34
35
/// known searchd commands
36
define('SEARCHD_COMMAND_SEARCH',     0);
37
define('SEARCHD_COMMAND_EXCERPT',    1);
38
define('SEARCHD_COMMAND_UPDATE',     2);
39
define('SEARCHD_COMMAND_KEYWORDS',   3);
40
define('SEARCHD_COMMAND_PERSIST',    4);
41
define('SEARCHD_COMMAND_STATUS',     5);
42
define('SEARCHD_COMMAND_FLUSHATTRS', 7);
43
44
/// current client-side command implementation versions
45
define('VER_COMMAND_SEARCH',     0x11E);
46
define('VER_COMMAND_EXCERPT',    0x104);
47
define('VER_COMMAND_UPDATE',     0x103);
48
define('VER_COMMAND_KEYWORDS',   0x100);
49
define('VER_COMMAND_STATUS',     0x101);
50
define('VER_COMMAND_QUERY',      0x100);
51
define('VER_COMMAND_FLUSHATTRS', 0x100);
52
53
/// known searchd status codes
54
define('SEARCHD_OK',      0);
55
define('SEARCHD_ERROR',   1);
56
define('SEARCHD_RETRY',   2);
57
define('SEARCHD_WARNING', 3);
58
59
/// known match modes
60
define('SPH_MATCH_ALL',       0);
61
define('SPH_MATCH_ANY',       1);
62
define('SPH_MATCH_PHRASE',    2);
63
define('SPH_MATCH_BOOLEAN',   3);
64
define('SPH_MATCH_EXTENDED',  4);
65
define('SPH_MATCH_FULLSCAN',  5);
66
define('SPH_MATCH_EXTENDED2', 6); // extended engine V2 (TEMPORARY, WILL BE REMOVED)
67
68
/// known ranking modes (ext2 only)
69
define('SPH_RANK_PROXIMITY_BM25', 0); ///< default mode, phrase proximity major factor and BM25 minor one
70
define('SPH_RANK_BM25',           1); ///< statistical mode, BM25 ranking only (faster but worse quality)
71
define('SPH_RANK_NONE',           2); ///< no ranking, all matches get a weight of 1
72
define('SPH_RANK_WORDCOUNT',      3); ///< simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts
73
define('SPH_RANK_PROXIMITY',      4);
74
define('SPH_RANK_MATCHANY',       5);
75
define('SPH_RANK_FIELDMASK',      6);
76
define('SPH_RANK_SPH04',          7);
77
define('SPH_RANK_EXPR',           8);
78
define('SPH_RANK_TOTAL',          9);
79
80
/// known sort modes
81
define('SPH_SORT_RELEVANCE',     0);
82
define('SPH_SORT_ATTR_DESC',     1);
83
define('SPH_SORT_ATTR_ASC',      2);
84
define('SPH_SORT_TIME_SEGMENTS', 3);
85
define('SPH_SORT_EXTENDED',      4);
86
define('SPH_SORT_EXPR',          5);
87
88
/// known filter types
89
define('SPH_FILTER_VALUES',     0);
90
define('SPH_FILTER_RANGE',      1);
91
define('SPH_FILTER_FLOATRANGE', 2);
92
define('SPH_FILTER_STRING',     3);
93
94
/// known attribute types
95
define('SPH_ATTR_INTEGER',   1);
96
define('SPH_ATTR_TIMESTAMP', 2);
97
define('SPH_ATTR_ORDINAL',   3);
98
define('SPH_ATTR_BOOL',      4);
99
define('SPH_ATTR_FLOAT',     5);
100
define('SPH_ATTR_BIGINT',    6);
101
define('SPH_ATTR_STRING',    7);
102
define('SPH_ATTR_FACTORS',   1001);
103
define('SPH_ATTR_MULTI',     0x40000001);
104
define('SPH_ATTR_MULTI64',   0x40000002);
105
106
/// known grouping functions
107
define('SPH_GROUPBY_DAY',      0);
108
define('SPH_GROUPBY_WEEK',     1);
109
define('SPH_GROUPBY_MONTH',    2);
110
define('SPH_GROUPBY_YEAR',     3);
111
define('SPH_GROUPBY_ATTR',     4);
112
define('SPH_GROUPBY_ATTRPAIR', 5);
113
114
// important properties of PHP's integers:
115
//  - always signed (one bit short of PHP_INT_SIZE)
116
//  - conversion from string to int is saturated
117
//  - float is double
118
//  - div converts arguments to floats
119
//  - mod converts arguments to ints
120
121
// the packing code below works as follows:
122
//  - when we got an int, just pack it
123
//    if performance is a problem, this is the branch users should aim for
124
//
125
//  - otherwise, we got a number in string form
126
//    this might be due to different reasons, but we assume that this is
127
//    because it didn't fit into PHP int
128
//
129
//  - factor the string into high and low ints for packing
130
//    - if we have bcmath, then it is used
131
//    - if we don't, we have to do it manually (this is the fun part)
132
//
133
//    - x64 branch does factoring using ints
134
//    - x32 (ab)uses floats, since we can't fit unsigned 32-bit number into an int
135
//
136
// unpacking routines are pretty much the same.
137
//  - return ints if we can
138
//  - otherwise format number into a string
139
140
/// pack 64-bit signed
141
function sphPackI64($v)
142
{
143
    assert(is_numeric($v));
144
145
    // x64
146
    if (PHP_INT_SIZE >= 8) {
147
        $v = (int)$v;
148
        return pack('NN', $v >> 32, $v & 0xFFFFFFFF);
149
    }
150
151
    // x32, int
152
    if (is_int($v)) {
153
        return pack('NN', $v < 0 ? -1 : 0, $v);
154
    }
155
156
    // x32, bcmath
157
    if (function_exists('bcmul')) {
158
        if (bccomp($v, 0) == -1) {
159
            $v = bcadd('18446744073709551616', $v);
160
        }
161
        $h = bcdiv($v, '4294967296', 0);
162
        $l = bcmod($v, '4294967296');
163
        return pack('NN', (float)$h, (float)$l); // conversion to float is intentional; int would lose 31st bit
164
    }
165
166
    // x32, no-bcmath
167
    $p = max(0, strlen($v) - 13);
168
    $lo = abs((float)substr($v, $p));
169
    $hi = abs((float)substr($v, 0, $p));
170
171
    $m = $lo + $hi * 1316134912.0; // (10 ^ 13) % (1 << 32) = 1316134912
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
172
    $q = floor($m / 4294967296.0);
173
    $l = $m - ($q * 4294967296.0);
174
    $h = $hi * 2328.0 + $q; // (10 ^ 13) / (1 << 32) = 2328
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
175
176
    if ($v < 0) {
177
        if ($l == 0) {
178
            $h = 4294967296.0 - $h;
179
        } else {
180
            $h = 4294967295.0 - $h;
181
            $l = 4294967296.0 - $l;
182
        }
183
    }
184
    return pack('NN', $h, $l);
185
}
186
187
/// pack 64-bit unsigned
188
function sphPackU64($v)
189
{
190
    assert(is_numeric($v));
191
192
    // x64
193
    if (PHP_INT_SIZE >= 8) {
194
        assert($v >= 0);
195
196
        // x64, int
197
        if (is_int($v)) {
198
            return pack('NN', $v >> 32, $v & 0xFFFFFFFF);
199
        }
200
201
        // x64, bcmath
202 View Code Duplication
        if (function_exists('bcmul')) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
203
            $h = bcdiv($v, 4294967296, 0);
204
            $l = bcmod($v, 4294967296);
205
            return pack('NN', $h, $l);
206
        }
207
208
        // x64, no-bcmath
209
        $p = max(0, strlen($v) - 13);
210
        $lo = (int)substr($v, $p);
211
        $hi = (int)substr($v, 0, $p);
212
213
        $m = $lo + $hi * 1316134912;
214
        $l = $m % 4294967296;
215
        $h = $hi * 2328 + (int)($m / 4294967296);
216
217
        return pack('NN', $h, $l);
218
    }
219
220
    // x32, int
221
    if (is_int($v)) {
222
        return pack('NN', 0, $v);
223
    }
224
225
    // x32, bcmath
226 View Code Duplication
    if (function_exists('bcmul')) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
227
        $h = bcdiv($v, '4294967296', 0);
228
        $l = bcmod($v, '4294967296');
229
        return pack('NN', (float)$h, (float)$l); // conversion to float is intentional; int would lose 31st bit
230
    }
231
232
    // x32, no-bcmath
233
    $p = max(0, strlen($v) - 13);
234
    $lo = (float)substr($v, $p);
235
    $hi = (float)substr($v, 0, $p);
236
237
    $m = $lo + $hi * 1316134912.0;
238
    $q = floor($m / 4294967296.0);
239
    $l = $m - ($q * 4294967296.0);
240
    $h = $hi * 2328.0 + $q;
241
242
    return pack('NN', $h, $l);
243
}
244
245
// unpack 64-bit unsigned
246
function sphUnpackU64($v)
247
{
248
    list($hi, $lo) = array_values(unpack('N*N*', $v));
249
250
    if (PHP_INT_SIZE >= 8) {
251
        if ($hi < 0) { // because php 5.2.2 to 5.2.5 is totally fucked up again
252
            $hi += 1 << 32;
253
        }
254
        if ($lo < 0) {
255
            $lo += 1 << 32;
256
        }
257
258
        // x64, int
259
        if ($hi <= 2147483647) {
260
            return ($hi << 32) + $lo;
261
        }
262
263
        // x64, bcmath
264
        if (function_exists('bcmul')) {
265
            return bcadd($lo, bcmul($hi, '4294967296'));
266
        }
267
268
        // x64, no-bcmath
269
        $C = 100000;
270
        $h = ((int)($hi / $C) << 32) + (int)($lo / $C);
271
        $l = (($hi % $C) << 32) + ($lo % $C);
272
        if ($l > $C) {
273
            $h += (int)($l / $C);
274
            $l  = $l % $C;
275
        }
276
277
        if ($h == 0) {
278
            return $l;
279
        }
280
        return sprintf('%d%05d', $h, $l);
281
    }
282
283
    // x32, int
284
    if ($hi == 0) {
285
        if ($lo > 0) {
286
            return $lo;
287
        }
288
        return sprintf('%u', $lo);
289
    }
290
291
    $hi = sprintf('%u', $hi);
292
    $lo = sprintf('%u', $lo);
293
294
    // x32, bcmath
295
    if (function_exists('bcmul')) {
296
        return bcadd($lo, bcmul($hi, '4294967296'));
297
    }
298
299
    // x32, no-bcmath
300
    $hi = (float)$hi;
301
    $lo = (float)$lo;
302
303
    $q = floor($hi / 10000000.0);
304
    $r = $hi - $q * 10000000.0;
305
    $m = $lo + $r * 4967296.0;
306
    $mq = floor($m / 10000000.0);
307
    $l = $m - $mq * 10000000.0;
308
    $h = $q * 4294967296.0 + $r * 429.0 + $mq;
309
310
    $h = sprintf('%.0f', $h);
311
    $l = sprintf('%07.0f', $l);
312
    if ($h == '0') {
313
        return sprintf('%.0f', (float)$l);
314
    }
315
    return $h . $l;
316
}
317
318
// unpack 64-bit signed
319
function sphUnpackI64($v)
320
{
321
    list($hi, $lo) = array_values(unpack('N*N*', $v));
322
323
    // x64
324
    if (PHP_INT_SIZE >= 8) {
325
        if ($hi < 0) { // because php 5.2.2 to 5.2.5 is totally fucked up again
326
            $hi += 1 << 32;
327
        }
328
        if ($lo < 0) {
329
            $lo += 1 << 32;
330
        }
331
332
        return ($hi << 32) + $lo;
333
    }
334
335
    if ($hi == 0) { // x32, int
336
        if ($lo > 0) {
337
            return $lo;
338
        }
339
        return sprintf('%u', $lo);
340
    } elseif ($hi == -1) { // x32, int
341
        if ($lo < 0) {
342
            return $lo;
343
        }
344
        return sprintf('%.0f', $lo - 4294967296.0);
345
    }
346
347
    $neg = '';
348
    $c = 0;
349
    if ($hi < 0) {
350
        $hi = ~$hi;
351
        $lo = ~$lo;
352
        $c = 1;
353
        $neg = '-';
354
    }
355
356
    $hi = sprintf('%u', $hi);
357
    $lo = sprintf('%u', $lo);
358
359
    // x32, bcmath
360
    if (function_exists('bcmul')) {
361
        return $neg . bcadd(bcadd($lo, bcmul($hi, '4294967296')), $c);
362
    }
363
364
    // x32, no-bcmath
365
    $hi = (float)$hi;
366
    $lo = (float)$lo;
367
368
    $q = floor($hi / 10000000.0);
369
    $r = $hi - $q * 10000000.0;
370
    $m = $lo + $r * 4967296.0;
371
    $mq = floor($m / 10000000.0);
372
    $l = $m - $mq * 10000000.0 + $c;
373
    $h = $q * 4294967296.0 + $r * 429.0 + $mq;
374
    if ($l == 10000000) {
375
        $l = 0;
376
        $h += 1;
377
    }
378
379
    $h = sprintf('%.0f', $h);
380
    $l = sprintf('%07.0f', $l);
381
    if ($h == '0') {
382
        return $neg . sprintf('%.0f', (float)$l);
383
    }
384
    return $neg . $h . $l;
385
}
386
387
388
function sphFixUint($value)
389
{
390
    if (PHP_INT_SIZE >= 8) {
391
        // x64 route, workaround broken unpack() in 5.2.2+
392
        if ($value < 0) {
393
            $value += 1 << 32;
394
        }
395
        return $value;
396
    } else {
397
        // x32 route, workaround php signed/unsigned braindamage
398
        return sprintf('%u', $value);
399
    }
400
}
401
402
function sphSetBit($flag, $bit, $on)
403
{
404 4
    if ($on) {
405 4
        $flag |= 1 << $bit;
406 4
    } else {
407
        $reset = 16777215 ^ (1 << $bit);
408
        $flag = $flag & $reset;
409
    }
410 4
    return $flag;
411
}
412
413
414
/// sphinx searchd client class
415
class SphinxClient
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
416
{
417
    protected $host = 'localhost'; ///< searchd host (default is 'localhost')
418
    protected $port = 9312; ///< searchd port (default is 9312)
419
    protected $offset = 0; ///< how many records to seek from result-set start (default is 0)
420
    protected $limit = 20; ///< how many records to return from result-set starting at offset (default is 20)
421
    protected $mode = SPH_MATCH_EXTENDED2; ///< query matching mode (default is SPH_MATCH_EXTENDED2)
422
    protected $weights = array(); ///< per-field weights (default is 1 for all fields)
423
    protected $sort = SPH_SORT_RELEVANCE; ///< match sorting mode (default is SPH_SORT_RELEVANCE)
424
    protected $sort_by = ''; ///< attribute to sort by (defualt is '')
425
    protected $min_id = 0; ///< min ID to match (default is 0, which means no limit)
426
    protected $max_id = 0; ///< max ID to match (default is 0, which means no limit)
427
    protected $filters = array(); ///< search filters
428
    protected $group_by = ''; ///< group-by attribute name
429
    protected $group_func = SPH_GROUPBY_DAY; ///< group-by function (to pre-process group-by attribute value with)
430
    protected $group_sort = '@group desc'; ///< group-by sorting clause (to sort groups in result set with)
431
    protected $group_distinct = ''; ///< group-by count-distinct attribute
432
    protected $max_matches = 1000; ///< max matches to retrieve
433
    protected $cutoff = 0; ///< cutoff to stop searching at (default is 0)
434
    protected $retry_count = 0; ///< distributed retries count
435
    protected $retry_delay = 0; ///< distributed retries delay
436
    protected $anchor = array(); ///< geographical anchor point
437
    protected $index_weights = array(); ///< per-index weights
438
    protected $ranker = SPH_RANK_PROXIMITY_BM25; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25)
439
    protected $rank_expr = ''; ///< ranking mode expression (for SPH_RANK_EXPR)
440
    protected $max_query_time = 0; ///< max query time, milliseconds (default is 0, do not limit)
441
    protected $field_weights = array(); ///< per-field-name weights
442
    protected $overrides = array(); ///< per-query attribute values overrides
443
    protected $select = '*'; ///< select-list (attributes or expressions, with optional aliases)
444
    protected $query_flags = 0; ///< per-query various flags
445
    protected $predicted_time = 0; ///< per-query max_predicted_time
446
    protected $outer_order_by = ''; ///< outer match sort by
447
    protected $outer_offset = 0; ///< outer offset
448
    protected $outer_limit = 0; ///< outer limit
449
    protected $has_outer = false;
450
451
    protected $error = ''; ///< last error message
452
    protected $warning = ''; ///< last warning message
453
    protected $conn_error = false; ///< connection error vs remote error flag
454
455
    protected $reqs = array(); ///< requests array for multi-query
456
    protected $mbenc = ''; ///< stored mbstring encoding
457
    protected $array_result = false; ///< whether $result['matches'] should be a hash or an array
458
    protected $timeout = 0; ///< connect timeout
459
460
    protected $path = false;
461
    protected $socket = false;
462
463
    /////////////////////////////////////////////////////////////////////////////
464
    // common stuff
465
    /////////////////////////////////////////////////////////////////////////////
466
467 4
    public function __construct()
468
    {
469 4
        $this->query_flags = sphSetBit(0, 6, true); // default idf=tfidf_normalized
470 4
    }
471
472
    public function __destruct()
473
    {
474
        if ($this->socket !== false) {
475
            fclose($this->socket);
476
        }
477
    }
478
479
    /// get last error message (string)
480 1
    public function getLastError()
481
    {
482 1
        return $this->error;
483
    }
484
485
    /// get last warning message (string)
486 1
    public function getLastWarning()
487
    {
488 1
        return $this->warning;
489
    }
490
491
    /// get last error flag (to tell network connection errors from searchd errors or broken responses)
492 1
    public function isConnectError()
493
    {
494 1
        return $this->conn_error;
495
    }
496
497
    /// set searchd host name (string) and port (integer)
498
    public function setServer($host, $port = 0)
499
    {
500
        assert(is_string($host));
501
        if ($host[0] == '/') {
502
            $this->path = 'unix://' . $host;
0 ignored issues
show
Documentation Bug introduced by
The property $path was declared of type boolean, but 'unix://' . $host is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
503
            return;
504
        }
505
        if (substr($host, 0, 7) == 'unix://') {
506
            $this->path = $host;
0 ignored issues
show
Documentation Bug introduced by
The property $path was declared of type boolean, but $host is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
507
            return;
508
        }
509
510
        $this->host = $host;
511
        $port = intval($port);
512
        assert(0 <= $port && $port < 65536);
513
        $this->port = $port == 0 ? 9312 : $port;
514
        $this->path = '';
0 ignored issues
show
Documentation Bug introduced by
The property $path was declared of type boolean, but '' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
515
    }
516
517
    /// set server connection timeout (0 to remove)
518
    public function setConnectTimeout($timeout)
519
    {
520
        assert(is_numeric($timeout));
521
        $this->timeout = $timeout;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timeout can also be of type double or string. However, the property $timeout 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...
522
    }
523
524
525
    protected function send($handle, $data, $length)
526
    {
527
        if (feof($handle) || fwrite($handle, $data, $length) !== $length) {
528
            $this->error = 'connection unexpectedly closed (timed out?)';
529
            $this->conn_error = true;
530
            return false;
531
        }
532
        return true;
533
    }
534
535
    /////////////////////////////////////////////////////////////////////////////
536
537
    /// enter mbstring workaround mode
538
    protected function mbPush()
539
    {
540
        $this->mbenc = '';
541
        if (ini_get('mbstring.func_overload') & 2) {
542
            $this->mbenc = mb_internal_encoding();
543
            mb_internal_encoding('latin1');
544
        }
545
    }
546
547
    /// leave mbstring workaround mode
548
    protected function mbPop()
549
    {
550
        if ($this->mbenc) {
551
            mb_internal_encoding($this->mbenc);
552
        }
553
    }
554
555
    /// connect to searchd server
556
    protected function connect()
557
    {
558
        if ($this->socket !== false) {
559
            // we are in persistent connection mode, so we have a socket
560
            // however, need to check whether it's still alive
561
            if (!@feof($this->socket)) {
562
                return $this->socket;
563
            }
564
565
            // force reopen
566
            $this->socket = false;
567
        }
568
569
        $errno = 0;
570
        $errstr = '';
571
        $this->conn_error = false;
572
573
        if ($this->path) {
574
            $host = $this->path;
575
            $port = 0;
576
        } else {
577
            $host = $this->host;
578
            $port = $this->port;
579
        }
580
581
        if ($this->timeout <= 0) {
582
            $fp = @fsockopen($host, $port, $errno, $errstr);
583
        } else {
584
            $fp = @fsockopen($host, $port, $errno, $errstr, $this->timeout);
585
        }
586
587
        if (!$fp) {
588
            if ($this->path) {
589
                $location = $this->path;
590
            } else {
591
                $location = "{$this->host}:{$this->port}";
592
            }
593
594
            $errstr = trim($errstr);
595
            $this->error = "connection to $location failed (errno=$errno, msg=$errstr)";
596
            $this->conn_error = true;
597
            return false;
598
        }
599
600
        // send my version
601
        // this is a subtle part. we must do it before (!) reading back from searchd.
602
        // because otherwise under some conditions (reported on FreeBSD for instance)
603
        // TCP stack could throttle write-write-read pattern because of Nagle.
604
        if (!$this->send($fp, pack('N', 1), 4)) {
605
            fclose($fp);
606
            $this->error = 'failed to send client protocol version';
607
            return false;
608
        }
609
610
        // check version
611
        list(, $v) = unpack('N*', fread($fp, 4));
612
        $v = (int)$v;
613
        if ($v < 1) {
614
            fclose($fp);
615
            $this->error = "expected searchd protocol version 1+, got version '$v'";
616
            return false;
617
        }
618
619
        return $fp;
620
    }
621
622
    /// get and check response packet from searchd server
623
    protected function getResponse($fp, $client_ver)
624
    {
625
        $response = '';
626
        $len = 0;
627
628
        $header = fread($fp, 8);
629
        if (strlen($header) == 8) {
630
            list($status, $ver, $len) = array_values(unpack('n2a/Nb', $header));
631
            $left = $len;
632
            while ($left > 0 && !feof($fp)) {
633
                $chunk = fread($fp, min(8192, $left));
634
                if ($chunk) {
635
                    $response .= $chunk;
636
                    $left -= strlen($chunk);
637
                }
638
            }
639
        }
640
        if ($this->socket === false) {
641
            fclose($fp);
642
        }
643
644
        // check response
645
        $read = strlen($response);
646
        if (!$response || $read != $len) {
647
            $this->error = $len
648
                ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)"
0 ignored issues
show
Bug introduced by
The variable $status does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $ver does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
649
                : 'received zero-sized searchd response';
650
            return false;
651
        }
652
653
        // check status
654
        if ($status == SEARCHD_WARNING) {
655
            list(, $wlen) = unpack('N*', substr($response, 0, 4));
656
            $this->warning = substr($response, 4, $wlen);
657
            return substr($response, 4 + $wlen);
658
        }
659
        if ($status == SEARCHD_ERROR) {
660
            $this->error = 'searchd error: ' . substr($response, 4);
661
            return false;
662
        }
663
        if ($status == SEARCHD_RETRY) {
664
            $this->error = 'temporary searchd error: ' . substr($response, 4);
665
            return false;
666
        }
667
        if ($status != SEARCHD_OK) {
668
            $this->error = "unknown status code '$status'";
669
            return false;
670
        }
671
672
        // check version
673
        if ($ver < $client_ver) {
674
            $this->warning = sprintf(
675
                'searchd command v.%d.%d older than client\'s v.%d.%d, some options might not work',
676
                $ver >> 8,
677
                $ver & 0xff,
678
                $client_ver >> 8,
679
                $client_ver & 0xff
680
            );
681
        }
682
683
        return $response;
684
    }
685
686
    /////////////////////////////////////////////////////////////////////////////
687
    // searching
688
    /////////////////////////////////////////////////////////////////////////////
689
690
    /// set offset and count into result set,
691
    /// and optionally set max-matches and cutoff limits
692
    public function setLimits($offset, $limit, $max = 0, $cutoff = 0)
693
    {
694
        assert(is_int($offset));
695
        assert(is_int($limit));
696
        assert($offset >= 0);
697
        assert($limit > 0);
698
        assert($max >= 0);
699
        $this->offset = $offset;
700
        $this->limit = $limit;
701
        if ($max > 0) {
702
            $this->max_matches = $max;
703
        }
704
        if ($cutoff > 0) {
705
            $this->cutoff = $cutoff;
706
        }
707
    }
708
709
    /// set maximum query time, in milliseconds, per-index
710
    /// integer, 0 means 'do not limit'
711
    public function setMaxQueryTime($max)
712
    {
713
        assert(is_int($max));
714
        assert($max >= 0);
715
        $this->max_query_time = $max;
716
    }
717
718
    /// set matching mode
719
    public function setMatchMode($mode)
720
    {
721
        trigger_error(
722
            'DEPRECATED: Do not call this method or, even better, use SphinxQL instead of an API',
723
            E_USER_DEPRECATED
724
        );
725
        assert(
726
            $mode == SPH_MATCH_ALL ||
727
            $mode == SPH_MATCH_ANY ||
728
            $mode == SPH_MATCH_PHRASE ||
729
            $mode == SPH_MATCH_BOOLEAN ||
730
            $mode == SPH_MATCH_EXTENDED ||
731
            $mode == SPH_MATCH_FULLSCAN ||
732
            $mode == SPH_MATCH_EXTENDED2
733
        );
734
        $this->mode = $mode;
735
    }
736
737
    /// set ranking mode
738
    public function setRankingMode($ranker, $rankexpr='')
739
    {
740
        assert($ranker === 0 || $ranker >= 1 && $ranker < SPH_RANK_TOTAL);
741
        assert(is_string($rankexpr));
742
        $this->ranker = $ranker;
743
        $this->rank_expr = $rankexpr;
744
    }
745
746
    /// set matches sorting mode
747
    public function setSortMode($mode, $sortby = '')
748
    {
749
        assert (
750
            $mode == SPH_SORT_RELEVANCE ||
751
            $mode == SPH_SORT_ATTR_DESC ||
752
            $mode == SPH_SORT_ATTR_ASC ||
753
            $mode == SPH_SORT_TIME_SEGMENTS ||
754
            $mode == SPH_SORT_EXTENDED ||
755
            $mode == SPH_SORT_EXPR
756
        );
757
        assert(is_string($sortby));
758
        assert($mode == SPH_SORT_RELEVANCE || strlen($sortby) > 0);
759
760
        $this->sort = $mode;
761
        $this->sort_by = $sortby;
762
    }
763
764
    /**
765
     * Bind per-field weights by order
766
     *
767
     * @deprecated use setFieldWeights() instead
768
     */
769 1
    public function setWeights()
770
    {
771 1
        throw new \RuntimeException('This method is now deprecated; please use setFieldWeights instead');
772
    }
773
774
    /// bind per-field weights by name
775 View Code Duplication
    public function setFieldWeights($weights)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
776
    {
777
        assert(is_array($weights));
778
        foreach ($weights as $name => $weight) {
779
            assert(is_string($name));
780
            assert(is_int($weight));
781
        }
782
        $this->field_weights = $weights;
783
    }
784
785
    /// bind per-index weights by name
786 View Code Duplication
    public function setIndexWeights($weights)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
787
    {
788
        assert(is_array($weights));
789
        foreach ($weights as $index => $weight) {
790
            assert(is_string($index));
791
            assert(is_int($weight));
792
        }
793
        $this->index_weights = $weights;
794
    }
795
796
    /// set IDs range to match
797
    /// only match records if document ID is beetwen $min and $max (inclusive)
798
    public function setIDRange($min, $max)
799
    {
800
        assert(is_numeric($min));
801
        assert(is_numeric($max));
802
        assert($min <= $max);
803
        $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...
804
        $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...
805
    }
806
807
    /// set values set filter
808
    /// only match records where $attribute value is in given set
809
    public function setFilter($attribute, $values, $exclude = false)
810
    {
811
        assert(is_string($attribute));
812
        assert(is_array($values));
813
        assert(count($values));
814
815
        if (is_array($values) && count($values)) {
816
            foreach ($values as $value) {
817
                assert(is_numeric($value));
818
            }
819
820
            $this->filters[] = array(
821
                'type' => SPH_FILTER_VALUES,
822
                'attr' => $attribute,
823
                'exclude' => $exclude,
824
                'values' => $values
825
            );
826
        }
827
    }
828
829
    /// set string filter
830
    /// only match records where $attribute value is equal
831
    public function setFilterString($attribute, $value, $exclude = false)
832
    {
833
        assert(is_string($attribute));
834
        assert(is_string($value));
835
        $this->filters[] = array(
836
            'type' => SPH_FILTER_STRING,
837
            'attr' => $attribute,
838
            'exclude' => $exclude,
839
            'value' => $value
840
        );
841
    }    
842
843
    /// set range filter
844
    /// only match records if $attribute value is beetwen $min and $max (inclusive)
845 View Code Duplication
    public function setFilterRange($attribute, $min, $max, $exclude = false)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
846
    {
847
        assert(is_string($attribute));
848
        assert(is_numeric($min));
849
        assert(is_numeric($max));
850
        assert($min <= $max);
851
852
        $this->filters[] = array(
853
            'type' => SPH_FILTER_RANGE,
854
            'attr' => $attribute,
855
            'exclude' => $exclude,
856
            'min' => $min,
857
            'max' => $max
858
        );
859
    }
860
861
    /// set float range filter
862
    /// only match records if $attribute value is beetwen $min and $max (inclusive)
863 View Code Duplication
    public function setFilterFloatRange($attribute, $min, $max, $exclude = false)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
864
    {
865
        assert(is_string($attribute));
866
        assert(is_float($min));
867
        assert(is_float($max));
868
        assert($min <= $max);
869
870
        $this->filters[] = array(
871
            'type' => SPH_FILTER_FLOATRANGE,
872
            'attr' => $attribute,
873
            'exclude' => $exclude,
874
            'min' => $min,
875
            'max' => $max
876
        );
877
    }
878
879
    /// setup anchor point for geosphere distance calculations
880
    /// required to use @geodist in filters and sorting
881
    /// latitude and longitude must be in radians
882
    public function setGeoAnchor($attrlat, $attrlong, $lat, $long)
883
    {
884
        assert(is_string($attrlat));
885
        assert(is_string($attrlong));
886
        assert(is_float($lat));
887
        assert(is_float($long));
888
889
        $this->anchor = array(
890
            'attrlat' => $attrlat,
891
            'attrlong' => $attrlong,
892
            'lat' => $lat,
893
            'long' => $long
894
        );
895
    }
896
897
    /// set grouping attribute and function
898
    public function setGroupBy($attribute, $func, $groupsort = '@group desc')
899
    {
900
        assert(is_string($attribute));
901
        assert(is_string($groupsort));
902
        assert(
903
            $func == SPH_GROUPBY_DAY ||
904
            $func == SPH_GROUPBY_WEEK ||
905
            $func == SPH_GROUPBY_MONTH ||
906
            $func == SPH_GROUPBY_YEAR ||
907
            $func == SPH_GROUPBY_ATTR ||
908
            $func == SPH_GROUPBY_ATTRPAIR
909
        );
910
911
        $this->group_by = $attribute;
912
        $this->group_func = $func;
913
        $this->group_sort = $groupsort;
914
    }
915
916
    /// set count-distinct attribute for group-by queries
917
    public function setGroupDistinct($attribute)
918
    {
919
        assert(is_string($attribute));
920
        $this->group_distinct = $attribute;
921
    }
922
923
    /// set distributed retries count and delay
924
    public function setRetries($count, $delay = 0)
925
    {
926
        assert(is_int($count) && $count >= 0);
927
        assert(is_int($delay) && $delay >= 0);
928
        $this->retry_count = $count;
929
        $this->retry_delay = $delay;
930
    }
931
932
    /// set result set format (hash or array; hash by default)
933
    /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
934
    public function setArrayResult($arrayresult)
935
    {
936
        assert(is_bool($arrayresult));
937
        $this->array_result = $arrayresult;
938
    }
939
940
    /// set attribute values override
941
    /// there can be only one override per attribute
942
    /// $values must be a hash that maps document IDs to attribute values
943
    public function setOverride($attrname, $attrtype, $values)
944
    {
945
        trigger_error(
946
            'DEPRECATED: Do not call this method. Use SphinxQL REMAP() function instead.',
947
            E_USER_DEPRECATED
948
        );
949
        assert(is_string($attrname));
950
        assert(in_array($attrtype, array(
951
            SPH_ATTR_INTEGER,
952
            SPH_ATTR_TIMESTAMP,
953
            SPH_ATTR_BOOL,
954
            SPH_ATTR_FLOAT,
955
            SPH_ATTR_BIGINT
956
        )));
957
        assert(is_array($values));
958
959
        $this->overrides[$attrname] = array(
960
            'attr' => $attrname,
961
            'type' => $attrtype,
962
            'values' => $values
963
        );
964
    }
965
966
    /// set select-list (attributes or expressions), SQL-like syntax
967
    public function setSelect($select)
968
    {
969
        assert(is_string($select));
970
        $this->select = $select;
971
    }
972
973
    public function setQueryFlag($flag_name, $flag_value)
974
    {
975
        $known_names = array(
976
            'reverse_scan',
977
            'sort_method',
978
            'max_predicted_time',
979
            'boolean_simplify',
980
            'idf',
981
            'global_idf',
982
            'low_priority'
983
        );
984
        $flags = array (
985
            'reverse_scan' => array(0, 1),
986
            'sort_method' => array('pq', 'kbuffer'),
987
            'max_predicted_time' => array(0),
988
            'boolean_simplify' => array(true, false),
989
            'idf' => array ('normalized', 'plain', 'tfidf_normalized', 'tfidf_unnormalized'),
990
            'global_idf' => array(true, false),
991
            'low_priority' => array(true, false)
992
        );
993
994
        assert(isset($flag_name, $known_names));
995
        assert(
996
            in_array($flag_value, $flags[$flag_name], true) ||
997
            ($flag_name == 'max_predicted_time' && is_int($flag_value) && $flag_value >= 0)
998
        );
999
1000
        if ($flag_name == 'reverse_scan') {
1001
            $this->query_flags = sphSetBit($this->query_flags, 0, $flag_value == 1);
1002
        }
1003
        if ($flag_name == 'sort_method') {
1004
            $this->query_flags = sphSetBit($this->query_flags, 1, $flag_value == 'kbuffer');
1005
        }
1006
        if ($flag_name == 'max_predicted_time') {
1007
            $this->query_flags = sphSetBit($this->query_flags, 2, $flag_value > 0);
1008
            $this->predicted_time = (int)$flag_value;
1009
        }
1010
        if ($flag_name == 'boolean_simplify') {
1011
            $this->query_flags = sphSetBit($this->query_flags, 3, $flag_value);
1012
        }
1013 View Code Duplication
        if ($flag_name == 'idf' && ($flag_value == 'normalized' || $flag_value == 'plain')) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1014
            $this->query_flags = sphSetBit($this->query_flags, 4, $flag_value == 'plain');
1015
        }
1016
        if ($flag_name == 'global_idf') {
1017
            $this->query_flags = sphSetBit($this->query_flags, 5, $flag_value);
1018
        }
1019 View Code Duplication
        if ($flag_name == 'idf' && ($flag_value == 'tfidf_normalized' || $flag_value == 'tfidf_unnormalized')) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1020
            $this->query_flags = sphSetBit($this->query_flags, 6, $flag_value == 'tfidf_normalized');
1021
        }
1022
        if ($flag_name == 'low_priority') {
1023
            $this->query_flags = sphSetBit($this->query_flags, 8, $flag_value);
1024
        }
1025
    }
1026
1027
    /// set outer order by parameters
1028
    public function setOuterSelect($orderby, $offset, $limit)
1029
    {
1030
        assert(is_string($orderby));
1031
        assert(is_int($offset));
1032
        assert(is_int($limit));
1033
        assert($offset >= 0);
1034
        assert($limit > 0);
1035
1036
        $this->outer_order_by = $orderby;
1037
        $this->outer_offset = $offset;
1038
        $this->outer_limit = $limit;
1039
        $this->has_outer = true;
1040
    }
1041
1042
1043
    //////////////////////////////////////////////////////////////////////////////
1044
1045
    /// clear all filters (for multi-queries)
1046
    public function resetFilters()
1047
    {
1048
        $this->filters = array();
1049
        $this->anchor = array();
1050
    }
1051
1052
    /// clear groupby settings (for multi-queries)
1053
    public function resetGroupBy()
1054
    {
1055
        $this->group_by = '';
1056
        $this->group_func = SPH_GROUPBY_DAY;
1057
        $this->group_sort = '@group desc';
1058
        $this->group_distinct = '';
1059
    }
1060
1061
    /// clear all attribute value overrides (for multi-queries)
1062
    public function resetOverrides()
1063
    {
1064
        $this->overrides = array();
1065
    }
1066
1067
    public function resetQueryFlag()
1068
    {
1069
        $this->query_flags = sphSetBit(0, 6, true); // default idf=tfidf_normalized
1070
        $this->predicted_time = 0;
1071
    }
1072
1073
    public function resetOuterSelect()
1074
    {
1075
        $this->outer_order_by = '';
1076
        $this->outer_offset = 0;
1077
        $this->outer_limit = 0;
1078
        $this->has_outer = false;
1079
    }
1080
1081
    //////////////////////////////////////////////////////////////////////////////
1082
1083
    /// connect to searchd server, run given search query through given indexes,
1084
    /// and return the search results
1085
    public function query($query, $index = '*', $comment = '')
1086
    {
1087
        assert(empty($this->reqs));
1088
1089
        $this->addQuery($query, $index, $comment);
1090
        $results = $this->runQueries();
1091
        $this->reqs = array(); // just in case it failed too early
1092
1093
        if (!is_array($results)) {
1094
            return false; // probably network error; error message should be already filled
1095
        }
1096
1097
        $this->error = $results[0]['error'];
1098
        $this->warning = $results[0]['warning'];
1099
        if ($results[0]['status'] == SEARCHD_ERROR) {
1100
            return false;
1101
        } else {
1102
            return $results[0];
1103
        }
1104
    }
1105
1106
    /// helper to pack floats in network byte order
1107
    protected function packFloat($f)
1108
    {
1109
        $t1 = pack('f', $f); // machine order
1110
        list(, $t2) = unpack('L*', $t1); // int in machine order
1111
        return pack('N', $t2);
1112
    }
1113
1114
    /// add query to multi-query batch
1115
    /// returns index into results array from RunQueries() call
1116
    public function addQuery($query, $index = '*', $comment = '')
1117
    {
1118
        // mbstring workaround
1119
        $this->mbPush();
1120
1121
        // build request
1122
        $req = pack('NNNNN', $this->query_flags, $this->offset, $this->limit, $this->mode, $this->ranker);
1123
        if ($this->ranker == SPH_RANK_EXPR) {
1124
            $req .= pack('N', strlen($this->rank_expr)) . $this->rank_expr;
1125
        }
1126
        $req .= pack('N', $this->sort); // (deprecated) sort mode
1127
        $req .= pack('N', strlen($this->sort_by)) . $this->sort_by;
1128
        $req .= pack('N', strlen($query)) . $query; // query itself
1129
        $req .= pack('N', count($this->weights)); // weights
1130
        foreach ($this->weights as $weight) {
1131
            $req .= pack('N', (int)$weight);
1132
        }
1133
        $req .= pack('N', strlen($index)) . $index; // indexes
1134
        $req .= pack('N', 1); // id64 range marker
1135
        $req .= sphPackU64($this->min_id) . sphPackU64($this->max_id); // id64 range
1136
1137
        // filters
1138
        $req .= pack('N', count($this->filters));
1139
        foreach ($this->filters as $filter) {
1140
            $req .= pack('N', strlen($filter['attr'])) . $filter['attr'];
1141
            $req .= pack('N', $filter['type']);
1142
            switch ($filter['type']) {
1143
                case SPH_FILTER_VALUES:
1144
                    $req .= pack('N', count($filter['values']));
1145
                    foreach ($filter['values'] as $value) {
1146
                        $req .= sphPackI64($value);
1147
                    }
1148
                    break;
1149
                case SPH_FILTER_RANGE:
1150
                    $req .= sphPackI64($filter['min']) . sphPackI64($filter['max']);
1151
                    break;
1152
                case SPH_FILTER_FLOATRANGE:
1153
                    $req .= $this->packFloat($filter['min']) . $this->packFloat($filter['max']);
1154
                    break;
1155
                case SPH_FILTER_STRING:
1156
                    $req .= pack('N', strlen($filter['value'])) . $filter['value'];
1157
                    break;
1158
                default:
1159
                    assert(0 && 'internal error: unhandled filter type');
1160
            }
1161
            $req .= pack('N', $filter['exclude']);
1162
        }
1163
1164
        // group-by clause, max-matches count, group-sort clause, cutoff count
1165
        $req .= pack('NN', $this->group_func, strlen($this->group_by)) . $this->group_by;
1166
        $req .= pack('N', $this->max_matches);
1167
        $req .= pack('N', strlen($this->group_sort)) . $this->group_sort;
1168
        $req .= pack('NNN', $this->cutoff, $this->retry_count, $this->retry_delay);
1169
        $req .= pack('N', strlen($this->group_distinct)) . $this->group_distinct;
1170
1171
        // anchor point
1172
        if (empty($this->anchor)) {
1173
            $req .= pack('N', 0);
1174
        } else {
1175
            $a =& $this->anchor;
1176
            $req .= pack('N', 1);
1177
            $req .= pack('N', strlen($a['attrlat'])) . $a['attrlat'];
1178
            $req .= pack('N', strlen($a['attrlong'])) . $a['attrlong'];
1179
            $req .= $this->packFloat($a['lat']) . $this->packFloat($a['long']);
1180
        }
1181
1182
        // per-index weights
1183
        $req .= pack('N', count($this->index_weights));
1184
        foreach ($this->index_weights as $idx => $weight) {
1185
            $req .= pack('N', strlen($idx)) . $idx . pack('N', $weight);
1186
        }
1187
1188
        // max query time
1189
        $req .= pack('N', $this->max_query_time);
1190
1191
        // per-field weights
1192
        $req .= pack('N', count($this->field_weights));
1193
        foreach ($this->field_weights as $field => $weight) {
1194
            $req .= pack('N', strlen($field)) . $field . pack('N', $weight);
1195
        }
1196
1197
        // comment
1198
        $req .= pack('N', strlen($comment)) . $comment;
1199
1200
        // attribute overrides
1201
        $req .= pack('N', count($this->overrides));
1202
        foreach ($this->overrides as $key => $entry) {
1203
            $req .= pack('N', strlen($entry['attr'])) . $entry['attr'];
1204
            $req .= pack('NN', $entry['type'], count($entry['values']));
1205
            foreach ($entry['values'] as $id => $val) {
1206
                assert(is_numeric($id));
1207
                assert(is_numeric($val));
1208
1209
                $req .= sphPackU64($id);
1210
                switch ($entry['type']) {
1211
                    case SPH_ATTR_FLOAT:
1212
                        $req .= $this->packFloat($val);
1213
                        break;
1214
                    case SPH_ATTR_BIGINT:
1215
                        $req .= sphPackI64($val);
1216
                        break;
1217
                    default:
1218
                        $req .= pack('N', $val);
1219
                        break;
1220
                }
1221
            }
1222
        }
1223
1224
        // select-list
1225
        $req .= pack('N', strlen($this->select)) . $this->select;
1226
1227
        // max_predicted_time
1228
        if ($this->predicted_time > 0) {
1229
            $req .= pack('N', (int)$this->predicted_time);
1230
        }
1231
1232
        $req .= pack('N', strlen($this->outer_order_by)) . $this->outer_order_by;
1233
        $req .= pack('NN', $this->outer_offset, $this->outer_limit);
1234
        if ($this->has_outer) {
1235
            $req .= pack('N', 1);
1236
        } else {
1237
            $req .= pack('N', 0);
1238
        }
1239
1240
        // mbstring workaround
1241
        $this->mbPop();
1242
1243
        // store request to requests array
1244
        $this->reqs[] = $req;
1245
        return count($this->reqs) - 1;
1246
    }
1247
1248
    /// connect to searchd, run queries batch, and return an array of result sets
1249
    public function runQueries()
1250
    {
1251
        if (empty($this->reqs)) {
1252
            $this->error = 'no queries defined, issue AddQuery() first';
1253
            return false;
1254
        }
1255
1256
        // mbstring workaround
1257
        $this->mbPush();
1258
1259
        if (!($fp = $this->connect())) {
1260
            $this->mbPop();
1261
            return false;
1262
        }
1263
1264
        // send query, get response
1265
        $nreqs = count($this->reqs);
1266
        $req = join('', $this->reqs);
1267
        $len = 8 + strlen($req);
1268
        $req = pack('nnNNN', SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs) . $req; // add header
1269
1270 View Code Duplication
        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, VER_COMMAND_SEARCH))) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1271
            $this->mbPop();
1272
            return false;
1273
        }
1274
1275
        // query sent ok; we can reset reqs now
1276
        $this->reqs = array();
1277
1278
        // parse and return response
1279
        return $this->parseSearchResponse($response, $nreqs);
1280
    }
1281
1282
    /// parse and return search query (or queries) response
1283
    protected function parseSearchResponse($response, $nreqs)
1284
    {
1285
        $p = 0; // current position
1286
        $max = strlen($response); // max position for checks, to protect against broken responses
1287
1288
        $results = array();
1289
        for ($ires = 0; $ires < $nreqs && $p < $max; $ires++) {
1290
            $results[] = array();
1291
            $result =& $results[$ires];
1292
1293
            $result['error'] = '';
1294
            $result['warning'] = '';
1295
1296
            // extract status
1297
            list(, $status) = unpack('N*', substr($response, $p, 4));
1298
            $p += 4;
1299
            $result['status'] = $status;
1300
            if ($status != SEARCHD_OK) {
1301
                list(, $len) = unpack('N*', substr($response, $p, 4));
1302
                $p += 4;
1303
                $message = substr($response, $p, $len);
1304
                $p += $len;
1305
1306
                if ($status == SEARCHD_WARNING) {
1307
                    $result['warning'] = $message;
1308
                } else {
1309
                    $result['error'] = $message;
1310
                    continue;
1311
                }
1312
            }
1313
1314
            // read schema
1315
            $fields = array();
1316
            $attrs = array();
1317
1318
            list(, $nfields) = unpack('N*', substr($response, $p, 4));
1319
            $p += 4;
1320
            while ($nfields --> 0 && $p < $max) {
1321
                list(, $len) = unpack('N*', substr($response, $p, 4));
1322
                $p += 4;
1323
                $fields[] = substr($response, $p, $len);
1324
                $p += $len;
1325
            }
1326
            $result['fields'] = $fields;
1327
1328
            list(, $nattrs) = unpack('N*', substr($response, $p, 4));
1329
            $p += 4;
1330
            while ($nattrs --> 0 && $p < $max) {
1331
                list(, $len) = unpack('N*', substr($response, $p, 4));
1332
                $p += 4;
1333
                $attr = substr($response, $p, $len);
1334
                $p += $len;
1335
                list(, $type) = unpack('N*', substr($response, $p, 4));
1336
                $p += 4;
1337
                $attrs[$attr] = $type;
1338
            }
1339
            $result['attrs'] = $attrs;
1340
1341
            // read match count
1342
            list(, $count) = unpack('N*', substr($response, $p, 4));
1343
            $p += 4;
1344
            list(, $id64) = unpack('N*', substr($response, $p, 4));
1345
            $p += 4;
1346
1347
            // read matches
1348
            $idx = -1;
1349
            while ($count --> 0 && $p < $max) {
1350
                // index into result array
1351
                $idx++;
1352
1353
                // parse document id and weight
1354
                if ($id64) {
1355
                    $doc = sphUnpackU64(substr($response, $p, 8));
1356
                    $p += 8;
1357
                    list(,$weight) = unpack('N*', substr($response, $p, 4));
1358
                    $p += 4;
1359
                } else {
1360
                    list($doc, $weight) = array_values(unpack('N*N*', substr($response, $p, 8)));
1361
                    $p += 8;
1362
                    $doc = sphFixUint($doc);
1363
                }
1364
                $weight = sprintf('%u', $weight);
1365
1366
                // create match entry
1367 View Code Duplication
                if ($this->array_result) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1368
                    $result['matches'][$idx] = array('id' => $doc, 'weight' => $weight);
1369
                } else {
1370
                    $result['matches'][$doc]['weight'] = $weight;
1371
                }
1372
1373
                // parse and create attributes
1374
                $attrvals = array();
1375
                foreach ($attrs as $attr => $type) {
1376
                    // handle 64bit ints
1377
                    if ($type == SPH_ATTR_BIGINT) {
1378
                        $attrvals[$attr] = sphUnpackI64(substr($response, $p, 8));
1379
                        $p += 8;
1380
                        continue;
1381
                    }
1382
1383
                    // handle floats
1384
                    if ($type == SPH_ATTR_FLOAT) {
1385
                        list(, $uval) = unpack('N*', substr($response, $p, 4));
1386
                        $p += 4;
1387
                        list(, $fval) = unpack('f*', pack('L', $uval));
1388
                        $attrvals[$attr] = $fval;
1389
                        continue;
1390
                    }
1391
1392
                    // handle everything else as unsigned ints
1393
                    list(, $val) = unpack('N*', substr($response, $p, 4));
1394
                    $p += 4;
1395
                    if ($type == SPH_ATTR_MULTI) {
1396
                        $attrvals[$attr] = array();
1397
                        $nvalues = $val;
1398 View Code Duplication
                        while ($nvalues --> 0 && $p < $max) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1399
                            list(, $val) = unpack('N*', substr($response, $p, 4));
1400
                            $p += 4;
1401
                            $attrvals[$attr][] = sphFixUint($val);
1402
                        }
1403
                    } elseif ($type == SPH_ATTR_MULTI64) {
1404
                        $attrvals[$attr] = array();
1405
                        $nvalues = $val;
1406 View Code Duplication
                        while ($nvalues > 0 && $p < $max) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1407
                            $attrvals[$attr][] = sphUnpackI64(substr($response, $p, 8));
1408
                            $p += 8;
1409
                            $nvalues -= 2;
1410
                        }
1411
                    } elseif ($type == SPH_ATTR_STRING) {
1412
                        $attrvals[$attr] = substr($response, $p, $val);
1413
                        $p += $val;
1414
                    } elseif ($type == SPH_ATTR_FACTORS) {
1415
                        $attrvals[$attr] = substr($response, $p, $val - 4);
1416
                        $p += $val-4;
1417
                    } else {
1418
                        $attrvals[$attr] = sphFixUint($val);
1419
                    }
1420
                }
1421
1422 View Code Duplication
                if ($this->array_result) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1423
                    $result['matches'][$idx]['attrs'] = $attrvals;
1424
                } else {
1425
                    $result['matches'][$doc]['attrs'] = $attrvals;
1426
                }
1427
            }
1428
1429
            list($total, $total_found, $msecs, $words) = array_values(unpack('N*N*N*N*', substr($response, $p, 16)));
1430
            $result['total'] = sprintf('%u', $total);
1431
            $result['total_found'] = sprintf('%u', $total_found);
1432
            $result['time'] = sprintf('%.3f', $msecs / 1000);
1433
            $p += 16;
1434
1435
            while ($words --> 0 && $p < $max) {
1436
                list(, $len) = unpack('N*', substr($response, $p, 4));
1437
                $p += 4;
1438
                $word = substr($response, $p, $len);
1439
                $p += $len;
1440
                list($docs, $hits) = array_values(unpack('N*N*', substr($response, $p, 8)));
1441
                $p += 8;
1442
                $result['words'][$word] = array (
1443
                    'docs' => sprintf('%u', $docs),
1444
                    'hits' => sprintf('%u', $hits)
1445
                );
1446
            }
1447
        }
1448
1449
        $this->mbPop();
1450
        return $results;
1451
    }
1452
1453
    /////////////////////////////////////////////////////////////////////////////
1454
    // excerpts generation
1455
    /////////////////////////////////////////////////////////////////////////////
1456
1457
    /// connect to searchd server, and generate exceprts (snippets)
1458
    /// of given documents for given query. returns false on failure,
1459
    /// an array of snippets on success
1460
    public function buildExcerpts($docs, $index, $words, $opts = array())
1461
    {
1462
        assert(is_array($docs));
1463
        assert(is_string($index));
1464
        assert(is_string($words));
1465
        assert(is_array($opts));
1466
1467
        $this->mbPush();
1468
1469
        if (!($fp = $this->connect())) {
1470
            $this->mbPop();
1471
            return false;
1472
        }
1473
1474
        /////////////////
1475
        // fixup options
1476
        /////////////////
1477
1478
        if (!isset($opts['before_match'])) {
1479
            $opts['before_match'] = '<b>';
1480
        }
1481
        if (!isset($opts['after_match'])) {
1482
            $opts['after_match'] = '</b>';
1483
        }
1484
        if (!isset($opts['chunk_separator'])) {
1485
            $opts['chunk_separator'] = ' ... ';
1486
        }
1487
        if (!isset($opts['limit'])) {
1488
            $opts['limit'] = 256;
1489
        }
1490
        if (!isset($opts['limit_passages'])) {
1491
            $opts['limit_passages'] = 0;
1492
        }
1493
        if (!isset($opts['limit_words'])) {
1494
            $opts['limit_words'] = 0;
1495
        }
1496
        if (!isset($opts['around'])) {
1497
            $opts['around'] = 5;
1498
        }
1499
        if (!isset($opts['exact_phrase'])) {
1500
            $opts['exact_phrase'] = false;
1501
        }
1502
        if (!isset($opts['single_passage'])) {
1503
            $opts['single_passage'] = false;
1504
        }
1505
        if (!isset($opts['use_boundaries'])) {
1506
            $opts['use_boundaries'] = false;
1507
        }
1508
        if (!isset($opts['weight_order'])) {
1509
            $opts['weight_order'] = false;
1510
        }
1511
        if (!isset($opts['query_mode'])) {
1512
            $opts['query_mode'] = false;
1513
        }
1514
        if (!isset($opts['force_all_words'])) {
1515
            $opts['force_all_words'] = false;
1516
        }
1517
        if (!isset($opts['start_passage_id'])) {
1518
            $opts['start_passage_id'] = 1;
1519
        }
1520
        if (!isset($opts['load_files'])) {
1521
            $opts['load_files'] = false;
1522
        }
1523
        if (!isset($opts['html_strip_mode'])) {
1524
            $opts['html_strip_mode'] = 'index';
1525
        }
1526
        if (!isset($opts['allow_empty'])) {
1527
            $opts['allow_empty'] = false;
1528
        }
1529
        if (!isset($opts['passage_boundary'])) {
1530
            $opts['passage_boundary'] = 'none';
1531
        }
1532
        if (!isset($opts['emit_zones'])) {
1533
            $opts['emit_zones'] = false;
1534
        }
1535
        if (!isset($opts['load_files_scattered'])) {
1536
            $opts['load_files_scattered'] = false;
1537
        }
1538
1539
1540
        /////////////////
1541
        // build request
1542
        /////////////////
1543
1544
        // v.1.2 req
1545
        $flags = 1; // remove spaces
1546
        if ($opts['exact_phrase']) {
1547
            $flags |= 2;
1548
        }
1549
        if ($opts['single_passage']) {
1550
            $flags |= 4;
1551
        }
1552
        if ($opts['use_boundaries']) {
1553
            $flags |= 8;
1554
        }
1555
        if ($opts['weight_order']) {
1556
            $flags |= 16;
1557
        }
1558
        if ($opts['query_mode']) {
1559
            $flags |= 32;
1560
        }
1561
        if ($opts['force_all_words']) {
1562
            $flags |= 64;
1563
        }
1564
        if ($opts['load_files']) {
1565
            $flags |= 128;
1566
        }
1567
        if ($opts['allow_empty']) {
1568
            $flags |= 256;
1569
        }
1570
        if ($opts['emit_zones']) {
1571
            $flags |= 512;
1572
        }
1573
        if ($opts['load_files_scattered']) {
1574
            $flags |= 1024;
1575
        }
1576
        $req = pack('NN', 0, $flags); // mode=0, flags=$flags
1577
        $req .= pack('N', strlen($index)) . $index; // req index
1578
        $req .= pack('N', strlen($words)) . $words; // req words
1579
1580
        // options
1581
        $req .= pack('N', strlen($opts['before_match'])) . $opts['before_match'];
1582
        $req .= pack('N', strlen($opts['after_match'])) . $opts['after_match'];
1583
        $req .= pack('N', strlen($opts['chunk_separator'])) . $opts['chunk_separator'];
1584
        $req .= pack('NN', (int)$opts['limit'], (int)$opts['around']);
1585
        $req .= pack('NNN', (int)$opts['limit_passages'], (int)$opts['limit_words'], (int)$opts['start_passage_id']); // v.1.2
1586
        $req .= pack('N', strlen($opts['html_strip_mode'])) . $opts['html_strip_mode'];
1587
        $req .= pack('N', strlen($opts['passage_boundary'])) . $opts['passage_boundary'];
1588
1589
        // documents
1590
        $req .= pack('N', count($docs));
1591
        foreach ($docs as $doc) {
1592
            assert(is_string($doc));
1593
            $req .= pack('N', strlen($doc)) . $doc;
1594
        }
1595
1596
        ////////////////////////////
1597
        // send query, get response
1598
        ////////////////////////////
1599
1600
        $len = strlen($req);
1601
        $req = pack('nnN', SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len) . $req; // add header
1602 View Code Duplication
        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, VER_COMMAND_EXCERPT))) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1603
            $this->mbPop();
1604
            return false;
1605
        }
1606
1607
        //////////////////
1608
        // parse response
1609
        //////////////////
1610
1611
        $pos = 0;
1612
        $res = array();
1613
        $rlen = strlen($response);
1614
        $count = count($docs);
1615
        while ($count--) {
1616
            list(, $len) = unpack('N*', substr($response, $pos, 4));
1617
            $pos += 4;
1618
1619
            if ($pos + $len > $rlen) {
1620
                $this->error = 'incomplete reply';
1621
                $this->mbPop();
1622
                return false;
1623
            }
1624
            $res[] = $len ? substr($response, $pos, $len) : '';
1625
            $pos += $len;
1626
        }
1627
1628
        $this->mbPop();
1629
        return $res;
1630
    }
1631
1632
1633
    /////////////////////////////////////////////////////////////////////////////
1634
    // keyword generation
1635
    /////////////////////////////////////////////////////////////////////////////
1636
1637
    /// connect to searchd server, and generate keyword list for a given query
1638
    /// returns false on failure,
1639
    /// an array of words on success
1640
    public function buildKeywords($query, $index, $hits)
1641
    {
1642
        assert(is_string($query));
1643
        assert(is_string($index));
1644
        assert(is_bool($hits));
1645
1646
        $this->mbPush();
1647
1648
        if (!($fp = $this->connect())) {
1649
            $this->mbPop();
1650
            return false;
1651
        }
1652
1653
        /////////////////
1654
        // build request
1655
        /////////////////
1656
1657
        // v.1.0 req
1658
        $req  = pack('N', strlen($query)) . $query; // req query
1659
        $req .= pack('N', strlen($index)) . $index; // req index
1660
        $req .= pack('N', (int)$hits);
1661
1662
        ////////////////////////////
1663
        // send query, get response
1664
        ////////////////////////////
1665
1666
        $len = strlen($req);
1667
        $req = pack('nnN', SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len) . $req; // add header
1668 View Code Duplication
        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, VER_COMMAND_KEYWORDS))) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1669
            $this->mbPop();
1670
            return false;
1671
        }
1672
1673
        //////////////////
1674
        // parse response
1675
        //////////////////
1676
1677
        $pos = 0;
1678
        $res = array();
1679
        $rlen = strlen($response);
1680
        list(, $nwords) = unpack('N*', substr($response, $pos, 4));
1681
        $pos += 4;
1682
        for ($i = 0; $i < $nwords; $i++) {
1683
            list(, $len) = unpack('N*', substr($response, $pos, 4));
1684
            $pos += 4;
1685
            $tokenized = $len ? substr($response, $pos, $len) : '';
1686
            $pos += $len;
1687
1688
            list(, $len) = unpack('N*', substr($response, $pos, 4));
1689
            $pos += 4;
1690
            $normalized = $len ? substr($response, $pos, $len) : '';
1691
            $pos += $len;
1692
1693
            $res[] = array(
1694
                'tokenized' => $tokenized,
1695
                'normalized' => $normalized
1696
            );
1697
1698
            if ($hits) {
1699
                list($ndocs, $nhits) = array_values(unpack('N*N*', substr($response, $pos, 8)));
1700
                $pos += 8;
1701
                $res[$i]['docs'] = $ndocs;
1702
                $res[$i]['hits'] = $nhits;
1703
            }
1704
1705
            if ($pos > $rlen) {
1706
                $this->error = 'incomplete reply';
1707
                $this->mbPop();
1708
                return false;
1709
            }
1710
        }
1711
1712
        $this->mbPop();
1713
        return $res;
1714
    }
1715
1716
    public function escapeString($string)
1717
    {
1718
        $from = array('\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', '<');
1719
        $to   = array('\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=', '\<');
1720
1721
        return str_replace($from, $to, $string);
1722
    }
1723
1724
    /////////////////////////////////////////////////////////////////////////////
1725
    // attribute updates
1726
    /////////////////////////////////////////////////////////////////////////////
1727
1728
    /// batch update given attributes in given rows in given indexes
1729
    /// returns amount of updated documents (0 or more) on success, or -1 on failure
1730
    public function updateAttributes($index, $attrs, $values, $mva = false, $ignorenonexistent = false)
1731
    {
1732
        // verify everything
1733
        assert(is_string($index));
1734
        assert(is_bool($mva));
1735
        assert(is_bool($ignorenonexistent));
1736
1737
        assert(is_array($attrs));
1738
        foreach ($attrs as $attr) {
1739
            assert(is_string($attr));
1740
        }
1741
1742
        assert(is_array($values));
1743
        foreach ($values as $id => $entry) {
1744
            assert(is_numeric($id));
1745
            assert(is_array($entry));
1746
            assert(count($entry) == count($attrs));
1747
            foreach ($entry as $v) {
1748
                if ($mva) {
1749
                    assert(is_array($v));
1750
                    foreach ($v as $vv) {
1751
                        assert(is_int($vv));
1752
                    }
1753
                } else {
1754
                    assert(is_int($v));
1755
                }
1756
            }
1757
        }
1758
1759
        // build request
1760
        $this->mbPush();
1761
        $req = pack('N', strlen($index)) . $index;
1762
1763
        $req .= pack('N', count($attrs));
1764
        $req .= pack('N', $ignorenonexistent ? 1 : 0);
1765
        foreach ($attrs as $attr) {
1766
            $req .= pack('N', strlen($attr)) . $attr;
1767
            $req .= pack('N', $mva ? 1 : 0);
1768
        }
1769
1770
        $req .= pack('N', count($values));
1771
        foreach ($values as $id => $entry) {
1772
            $req .= sphPackU64($id);
1773
            foreach ($entry as $v) {
1774
                $req .= pack('N', $mva ? count($v) : $v);
1775
                if ($mva) {
1776
                    foreach ($v as $vv) {
1777
                        $req .= pack('N', $vv);
1778
                    }
1779
                }
1780
            }
1781
        }
1782
1783
        // connect, send query, get response
1784
        if (!($fp = $this->connect())) {
1785
            $this->mbPop();
1786
            return -1;
1787
        }
1788
1789
        $len = strlen($req);
1790
        $req = pack('nnN', SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len) . $req; // add header
1791
        if (!$this->send($fp, $req, $len + 8)) {
1792
            $this->mbPop();
1793
            return -1;
1794
        }
1795
1796
        if (!($response = $this->getResponse($fp, VER_COMMAND_UPDATE))) {
1797
            $this->mbPop();
1798
            return -1;
1799
        }
1800
1801
        // parse response
1802
        list(, $updated) = unpack('N*', substr($response, 0, 4));
1803
        $this->mbPop();
1804
        return $updated;
1805
    }
1806
1807
    /////////////////////////////////////////////////////////////////////////////
1808
    // persistent connections
1809
    /////////////////////////////////////////////////////////////////////////////
1810
1811
    public function open()
1812
    {
1813
        if ($this->socket !== false) {
1814
            $this->error = 'already connected';
1815
            return false;
1816
        }
1817
        if (!($fp = $this->connect()))
1818
            return false;
1819
1820
        // command, command version = 0, body length = 4, body = 1
1821
        $req = pack('nnNN', SEARCHD_COMMAND_PERSIST, 0, 4, 1);
1822
        if (!$this->send($fp, $req, 12)) {
1823
            return false;
1824
        }
1825
1826
        $this->socket = $fp;
1827
        return true;
1828
    }
1829
1830
    public function close()
1831
    {
1832
        if ($this->socket === false) {
1833
            $this->error = 'not connected';
1834
            return false;
1835
        }
1836
1837
        fclose($this->socket);
1838
        $this->socket = false;
1839
1840
        return true;
1841
    }
1842
1843
    //////////////////////////////////////////////////////////////////////////
1844
    // status
1845
    //////////////////////////////////////////////////////////////////////////
1846
1847
    public function status($session = false)
1848
    {
1849
        assert(is_bool($session));
1850
1851
        $this->mbPush();
1852
        if (!($fp = $this->connect())) {
1853
            $this->mbPop();
1854
            return false;
1855
        }
1856
1857
        $req = pack('nnNN', SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, $session ? 0 : 1); // len=4, body=1
1858 View Code Duplication
        if (!$this->send($fp, $req, 12) || !($response = $this->getResponse($fp, VER_COMMAND_STATUS))) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1859
            $this->mbPop();
1860
            return false;
1861
        }
1862
1863
        $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...
1864
        $p = 0;
1865
        list($rows, $cols) = array_values(unpack('N*N*', substr($response, $p, 8)));
1866
        $p += 8;
1867
1868
        $res = array();
1869
        for ($i = 0; $i < $rows; $i++) {
1870
            for ($j = 0; $j < $cols; $j++) {
1871
                list(, $len) = unpack('N*', substr($response, $p, 4));
1872
                $p += 4;
1873
                $res[$i][] = substr($response, $p, $len);
1874
                $p += $len;
1875
            }
1876
        }
1877
1878
        $this->mbPop();
1879
        return $res;
1880
    }
1881
1882
    //////////////////////////////////////////////////////////////////////////
1883
    // flush
1884
    //////////////////////////////////////////////////////////////////////////
1885
1886
    public function flushAttributes()
1887
    {
1888
        $this->mbPush();
1889
        if (!($fp = $this->connect())) {
1890
            $this->mbPop();
1891
            return -1;
1892
        }
1893
1894
        $req = pack('nnN', SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0); // len=0
1895 View Code Duplication
        if (!$this->send($fp, $req, 8) || !($response = $this->getResponse($fp, VER_COMMAND_FLUSHATTRS))) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1896
            $this->mbPop();
1897
            return -1;
1898
        }
1899
1900
        $tag = -1;
1901
        if (strlen($response) == 4) {
1902
            list(, $tag) = unpack('N*', $response);
1903
        } else {
1904
            $this->error = 'unexpected response length';
1905
        }
1906
1907
        $this->mbPop();
1908
        return $tag;
1909
    }
1910
}
1911