Passed
Pull Request — master (#153)
by
unknown
01:48
created

Percolate::getDocuments()   C

Complexity

Conditions 21
Paths 43

Size

Total Lines 78
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 38
nc 43
nop 0
dl 0
loc 78
rs 5.1035
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Foolz\SphinxQL;
4
5
use Foolz\SphinxQL\Drivers\ConnectionInterface;
6
use Foolz\SphinxQL\Exception\SphinxQLException;
7
8
/**
9
 * Query Builder class for Percolate Queries.
10
 *
11
 * ### INSERT ###
12
 *
13
 * $query = (new Percolate($conn))
14
 *    ->insert('full text query terms', noEscape = false)       // Allowed only one insert per query (Symbol @ indicates field in sphinx.conf)
15
 *                                                                 No escape tag cancels characters shielding (default on)
16
 *    ->into('pq')                                              // Index for insert
17
 *    ->tags(['tag1','tag2'])                                   // Adding tags. Can be array ['tag1','tag2'] or string delimited by coma
18
 *    ->filter('price>3')                                       // Adding filter (Allowed only one)
19
 *    ->execute();
20
 *
21
 *
22
 * ### CALL PQ ###
23
 *
24
 *
25
 * $query = (new Percolate($conn))
26
 *    ->callPQ()
27
 *    ->from('pq')                                              // Index for call pq
28
 *    ->documents(['multiple documents', 'go this way'])        // see getDocuments function
29
 *    ->options([                                               // See https://docs.manticoresearch.com/latest/html/searching/percolate_query.html#call-pq
30
 *          Percolate::OPTION_VERBOSE => 1,
31
 *          Percolate::OPTION_DOCS_JSON => 1
32
 *    ])
33
 *    ->execute();
34
 *
35
 *
36
 */
37
class Percolate
38
{
39
40
    /**
41
     * @var ConnectionInterface
42
     */
43
    protected $connection;
44
45
    /**
46
     * Documents for CALL PQ
47
     *
48
     * @var array|string
49
     */
50
    protected $documents;
51
52
    /**
53
     * Index name
54
     *
55
     * @var string
56
     */
57
    protected $index;
58
59
    /**
60
     * Insert query
61
     *
62
     * @var string
63
     */
64
    protected $query;
65
66
    /**
67
     * Options for CALL PQ
68
     * @var array
69
     */
70
    protected $options = [self::OPTION_DOCS_JSON => 1];
71
72
    /**
73
     * @var array
74
     */
75
    protected $filters = [];
76
77
    /**
78
     * Query type (call | insert)
79
     *
80
     * @var string
81
     */
82
    protected $type = 'call';
83
84
    /** INSERT STATEMENT  **/
85
86
    protected $tags = [];
87
88
    /**
89
     * Throw exceptions flag.
90
     * Activates if option OPTION_DOCS_JSON setted
91
     *
92
     * @var int
93
     */
94
    protected $throwExceptions = 0;
95
    /**
96
     * @var array
97
     */
98
    protected $escapeChars = [
99
        '\\' => '\\\\',
100
        '-' => '\\-',
101
        '~' => '\\~',
102
        '<' => '\\<',
103
        '"' => '\\"',
104
        "'" => "\\'",
105
        '/' => '\\/',
106
        '!' => '\\!'
107
    ];
108
109
    /** @var SphinxQL */
110
    protected $sphinxQL;
111
112
    /**
113
     * CALL PQ option constants
114
     */
115
    const OPTION_DOCS_JSON = 'as docs_json';
116
    const OPTION_DOCS = 'as docs';
117
    const OPTION_VERBOSE = 'as verbose';
118
    const OPTION_QUERY = 'as query';
119
120
    /**
121
     * @param ConnectionInterface $connection
122
     */
123
    public function __construct(ConnectionInterface $connection)
124
    {
125
        $this->connection = $connection;
126
        $this->sphinxQL = new SphinxQL($this->connection);
127
128
    }
129
130
131
    /**
132
     * Clear all fields after execute
133
     */
134
    private function clear()
135
    {
136
        $this->documents = null;
137
        $this->index = null;
138
        $this->query = null;
139
        $this->options = [self::OPTION_DOCS_JSON => 1];
140
        $this->type = 'call';
141
        $this->filters = [];
142
        $this->tags = [];
143
    }
144
145
    /**
146
     * Analog into function
147
     * Sets index name for query
148
     *
149
     * @param string $index
150
     *
151
     * @return $this
152
     * @throws SphinxQLException
153
     */
154
    public function from($index)
155
    {
156
        if (empty($index)) {
157
            throw new SphinxQLException('Index can\'t be empty');
158
        }
159
160
        $this->index = trim($index);
161
        return $this;
162
    }
163
164
    /**
165
     * Analog from function
166
     * Sets index name for query
167
     *
168
     * @param string $index
169
     *
170
     * @return $this
171
     * @throws SphinxQLException
172
     */
173
    public function into($index)
174
    {
175
        if (empty($index)) {
176
            throw new SphinxQLException('Index can\'t be empty');
177
        }
178
        $this->index = trim($index);
179
        return $this;
180
    }
181
182
    /**
183
     * Replacing bad chars
184
     *
185
     * @param string $query
186
     *
187
     * @return string mixed
188
     */
189
    protected function escapeString($query)
190
    {
191
        return str_replace(
192
            array_keys($this->escapeChars),
193
            array_values($this->escapeChars),
194
            $query);
195
    }
196
197
198
    /**
199
     * @param $query
200
     * @return mixed
201
     */
202
    protected function clearString($query)
203
    {
204
        return str_replace(
205
            array_keys(array_merge($this->escapeChars, ['@' => ''])),
206
            ['', '', '', '', '', '', '', '', '', ''],
207
            $query);
208
    }
209
210
    /**
211
     * Adding tags for insert query
212
     *
213
     * @param array|string $tags
214
     *
215
     * @return $this
216
     */
217
    public function tags($tags)
218
    {
219
        if (is_array($tags)) {
220
            $tags = array_map([$this, 'escapeString'], $tags);
221
            $tags = implode(',', $tags);
222
        } else {
223
            $tags = $this->escapeString($tags);
224
        }
225
        $this->tags = $tags;
226
        return $this;
227
    }
228
229
    /**
230
     * Add filter for insert query
231
     *
232
     * @param string $filter
233
     * @return $this
234
     *
235
     * @throws SphinxQLException
236
     */
237
    public function filter($filter)
238
    {
239
        $filters = explode(',', $filter);
240
        if (!empty($filters[1])) {
241
            throw new SphinxQLException(
242
                'Allow only one filter. If there is a comma in the text, it must be shielded');
243
        }
244
        $this->filters = $this->clearString($filter);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->clearString($filter) of type string is incompatible with the declared type array of property $filters.

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

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

Loading history...
245
        return $this;
246
    }
247
248
    /**
249
     * Add insert query
250
     *
251
     * @param string $query
252
     * @param bool $noEscape
253
     *
254
     * @return $this
255
     * @throws SphinxQLException
256
     */
257
    public function insert($query, $noEscape = false)
258
    {
259
        $this->clear();
260
261
        if (empty($query)) {
262
            throw new SphinxQLException('Query can\'t be empty');
263
        }
264
        if (!$noEscape) {
265
            $query = $this->escapeString($query);
266
        }
267
        $this->query = $query;
268
        $this->type = 'insert';
269
270
        return $this;
271
    }
272
273
    /**
274
     * Generate array for insert, from setted class parameters
275
     *
276
     * @return array
277
     */
278
    private function generateInsert()
279
    {
280
        $insertArray = ['query' => $this->query];
281
282
        if (!empty($this->tags)) {
283
            $insertArray['tags'] = $this->tags;
284
        }
285
286
        if (!empty($this->filters)) {
287
            $insertArray['filters'] = $this->filters;
288
        }
289
290
        return $insertArray;
291
    }
292
293
    /**
294
     * Executs query and clear class parameters
295
     *
296
     * @return Drivers\ResultSetInterface
297
     * @throws SphinxQLException
298
     */
299
    public function execute()
300
    {
301
302
        if ($this->type == 'insert') {
303
            return $this->sphinxQL
304
                ->insert()
305
                ->into($this->index)
306
                ->set($this->generateInsert())
307
                ->execute();
308
        }
309
310
        return $this->sphinxQL
311
            ->query("CALL PQ ('" .
312
                $this->index . "', " . $this->getDocuments() . " " . $this->getOptions() . ")")
313
            ->execute();
314
    }
315
316
    /**
317
     * Set one option for CALL PQ
318
     *
319
     * @param string $key
320
     * @param int $value
321
     *
322
     * @return $this
323
     * @throws SphinxQLException
324
     */
325
    private function setOption($key, $value)
326
    {
327
        $value = intval($value);
328
        if (!in_array($key, [
329
            self::OPTION_DOCS_JSON,
330
            self::OPTION_DOCS,
331
            self::OPTION_VERBOSE,
332
            self::OPTION_QUERY
333
        ])) {
334
            throw new SphinxQLException('Unknown option');
335
        }
336
337
        if ($value != 0 && $value != 1) {
338
            throw new SphinxQLException('Option value can be only 1 or 0');
339
        }
340
341
        if ($key == self::OPTION_DOCS_JSON) {
342
            $this->throwExceptions = 1;
343
        }
344
345
        $this->options[$key] = $value;
346
        return $this;
347
    }
348
349
    /**
350
     * Set document parameter for CALL PQ
351
     *
352
     * @param array|string $documents
353
     * @return $this
354
     * @throws SphinxQLException
355
     */
356
    public function documents($documents)
357
    {
358
        if (empty($documents)) {
359
            throw new SphinxQLException('Document can\'t be empty');
360
        }
361
        $this->documents = $documents;
362
363
        return $this;
364
    }
365
366
    /**
367
     * Set options for CALL PQ
368
     *
369
     * @param array $options
370
     * @return $this
371
     * @throws SphinxQLException
372
     */
373
    public function options(array $options)
374
    {
375
        foreach ($options as $option => $value) {
376
            $this->setOption($option, $value);
377
        }
378
        return $this;
379
    }
380
381
382
    /**
383
     * Get and prepare options for CALL PQ
384
     *
385
     * @return string string
386
     */
387
    protected function getOptions()
388
    {
389
        $options = '';
390
        if (!empty($this->options)) {
391
            foreach ($this->options as $option => $value) {
392
                $options .= ', ' . $value . ' ' . $option;
393
            }
394
        }
395
396
        return $options;
397
    }
398
399
    /**
400
     * Check is array associative
401
     * @param array $arr
402
     * @return bool
403
     */
404
    private function isAssocArray(array $arr)
405
    {
406
        if (array() === $arr) {
407
            return false;
408
        }
409
        return array_keys($arr) !== range(0, count($arr) - 1);
410
    }
411
412
    /**
413
     * Get documents for CALL PQ. If option setted JSON - returns json_encoded
414
     *
415
     * Now selection of supported types work automatically. You don't need set
416
     * OPTION_DOCS_JSON to 1 or 0. But if you will set this option,
417
     * automatically enables exceptions on unsupported types
418
     *
419
     *
420
     * 1) If expect json = 0:
421
     *      a) doc can be 'catch me'
422
     *      b) doc can be multiple ['catch me if can','catch me']
423
     *
424
     * 2) If expect json = 1:
425
     *      a) doc can be jsonOBJECT {"subject": "document about orange"}
426
     *      b) docs can be jsonARRAY of jsonOBJECTS [{"subject": "document about orange"}, {"doc": "document about orange"}]
427
     *      c) docs can be phpArray of jsonObjects ['{"subject": "document about orange"}', '{"doc": "document about orange"}']
428
     *      d) doc can be associate array ['subject'=>'document about orange']
429
     *      e) docs can be array of associate arrays [['subject'=>'document about orange'], ['doc'=>'document about orange']]
430
     *
431
     *
432
     *
433
     *
434
     * @return string
435
     * @throws SphinxQLException
436
     */
437
    protected function getDocuments()
438
    {
439
        if (!empty($this->documents)) {
440
441
            if ($this->throwExceptions) {
442
443
                if ($this->options[self::OPTION_DOCS_JSON]) {
444
445
                    if (!is_array($this->documents)) {
446
                        $json = $this->checkJson($this->documents);
447
                        if (!$json) {
448
                            throw new SphinxQLException('Documents must be in json format');
449
                        }
450
                    } else {
451
                        if (!$this->isAssocArray($this->documents) && !is_array($this->documents[0])) {
452
                            throw new SphinxQLException('Documents array must be associate');
453
                        }
454
                    }
455
                }
456
            }
457
458
            if (is_array($this->documents)) {
459
460
                // If input is phpArray with json like
461
                // ->documents(['{"body": "body of doc 1", "title": "title of doc 1"}',
1 ignored issue
show
Unused Code Comprehensibility introduced by
72% 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...
462
                //             '{"subject": "subject of doc 3"}'])
463
                //
464
                // Checking first symbol of first array value. If [ or { - call checkJson
465
466
                if (!empty($this->documents[0]) && !is_array($this->documents[0]) &&
467
                    ($this->documents[0][0] == '[' || $this->documents[0][0] == '{')) {
468
469
                    $json = $this->checkJson($this->documents);
0 ignored issues
show
Bug introduced by
$this->documents of type array is incompatible with the type string expected by parameter $json of Foolz\SphinxQL\Percolate::checkJson(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

469
                    $json = $this->checkJson(/** @scrutinizer ignore-type */ $this->documents);
Loading history...
470
                    if ($json) {
471
                        $this->options[self::OPTION_DOCS_JSON] = 1;
472
                        return $json;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $json returns the type true which is incompatible with the documented return type string.
Loading history...
473
                    }
474
475
                } else {
476
                    if (!$this->isAssocArray($this->documents)) {
477
478
                        // if incoming single array like ['catch me if can', 'catch me']
1 ignored issue
show
Unused Code Comprehensibility introduced by
42% 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...
479
480
                        if (is_string($this->documents[0])) {
481
                            $this->options[self::OPTION_DOCS_JSON] = 0;
482
                            return '(' . $this->quoteString(implode('\', \'', $this->documents)) . ')';
483
                        }
484
485
                        // if doc is array of associate arrays [['foo'=>'bar'], ['foo1'=>'bar1']]
1 ignored issue
show
Unused Code Comprehensibility introduced by
52% 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...
486
                        if (!empty($this->documents[0]) && $this->isAssocArray($this->documents[0])) {
487
                            $this->options[self::OPTION_DOCS_JSON] = 1;
488
                            return $this->convertArrayForQuery($this->documents);
489
                        }
490
491
                    } else {
492
                        if ($this->isAssocArray($this->documents)) {
493
                            // Id doc is associate array ['foo'=>'bar']
1 ignored issue
show
Unused Code Comprehensibility introduced by
38% 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...
494
                            $this->options[self::OPTION_DOCS_JSON] = 1;
495
                            return $this->quoteString(json_encode($this->documents));
496
                        }
497
                    }
498
                }
499
500
            } else {
501
                if (is_string($this->documents)) {
0 ignored issues
show
introduced by
The condition is_string($this->documents) is always true.
Loading history...
502
503
                    $json = $this->checkJson($this->documents);
504
                    if ($json) {
505
                        $this->options[self::OPTION_DOCS_JSON] = 1;
506
                        return $json;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $json returns the type true which is incompatible with the documented return type string.
Loading history...
507
                    }
508
509
                    $this->options[self::OPTION_DOCS_JSON] = 0;
510
                    return $this->quoteString($this->documents);
511
                }
512
            }
513
        }
514
        throw new SphinxQLException('Documents can\'t be empty');
515
    }
516
517
518
    /**
519
     * Set type
520
     *
521
     * @return $this
522
     */
523
    public function callPQ()
524
    {
525
        $this->clear();
526
        $this->type = 'call';
527
        return $this;
528
    }
529
530
531
    /**
532
     * Check string is valid json
533
     *
534
     * @param string $json
535
     * @return bool
536
     */
537
    private function checkJson($json)
538
    {
539
        if (is_array($json)) {
0 ignored issues
show
introduced by
The condition is_array($json) is always false.
Loading history...
540
            if (is_array($json[0])) {
541
                return false;
542
            }
543
            $return = [];
544
            foreach ($json as $item) {
545
                $return[] = $this->checkJson($item);
546
            }
547
            return '(' . implode(', ', $return) . ')';
548
        }
549
        $array = json_decode($json, true);
550
551
        if (json_last_error() == JSON_ERROR_NONE) { // if json
552
            if (!empty($array[0])) { // If docs is jsonARRAY of jsonOBJECTS
553
                return $this->convertArrayForQuery($array);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->convertArrayForQuery($array) returns the type string which is incompatible with the documented return type boolean.
Loading history...
554
            }
555
            // If docs is jsonOBJECT
556
            return $this->quoteString($json);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->quoteString($json) returns the type string which is incompatible with the documented return type boolean.
Loading history...
557
        }
558
        return false;
559
    }
560
561
562
    /**
563
     * Converts array of associate arrays to valid for query statement
564
     * ('jsonOBJECT1', 'jsonOBJECT2' ...)
565
     *
566
     *
567
     * @param array $array
568
     * @return string
569
     */
570
    private function convertArrayForQuery(array $array)
571
    {
572
        $documents = [];
573
        foreach ($array as $document) {
574
            $documents[] = json_encode($document);
575
        }
576
577
        return '(' . $this->quoteString(implode('\', \'', $documents)) . ')';
578
    }
579
580
581
    /**
582
     * Adding single quotes to string
583
     *
584
     * @param string $string
585
     * @return string
586
     */
587
    private function quoteString($string)
588
    {
589
        return '\'' . $string . '\'';
590
    }
591
592
593
    /**
594
     * Get last compiled query
595
     *
596
     * @return string
597
     */
598
    public function getLastQuery()
599
    {
600
        return $this->sphinxQL->getCompiled();
601
    }
602
}