Completed
Pull Request — master (#153)
by Adrian
02:32
created

Percolate::into()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
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 string
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
        $this->filters = $this->clearString($filter);
240
        return $this;
241
    }
242
243
    /**
244
     * Add insert query
245
     *
246
     * @param string $query
247
     * @param bool $noEscape
248
     *
249
     * @return $this
250
     * @throws SphinxQLException
251
     */
252
    public function insert($query, $noEscape = false)
253
    {
254
        $this->clear();
255
256
        if (empty($query)) {
257
            throw new SphinxQLException('Query can\'t be empty');
258
        }
259
        if (!$noEscape) {
260
            $query = $this->escapeString($query);
261
        }
262
        $this->query = $query;
263
        $this->type = 'insert';
264
265
        return $this;
266
    }
267
268
    /**
269
     * Generate array for insert, from setted class parameters
270
     *
271
     * @return array
272
     */
273
    private function generateInsert()
274
    {
275
        $insertArray = ['query' => $this->query];
276
277
        if (!empty($this->tags)) {
278
            $insertArray['tags'] = $this->tags;
279
        }
280
281
        if (!empty($this->filters)) {
282
            $insertArray['filters'] = $this->filters;
283
        }
284
285
        return $insertArray;
286
    }
287
288
    /**
289
     * Executs query and clear class parameters
290
     *
291
     * @return Drivers\ResultSetInterface
292
     * @throws SphinxQLException
293
     */
294
    public function execute()
295
    {
296
297
        if ($this->type == 'insert') {
298
            return $this->sphinxQL
299
                ->insert()
300
                ->into($this->index)
301
                ->set($this->generateInsert())
302
                ->execute();
303
        }
304
305
        return $this->sphinxQL
306
            ->query("CALL PQ ('" .
307
                $this->index . "', " . $this->getDocuments() . " " . $this->getOptions() . ")")
308
            ->execute();
309
    }
310
311
    /**
312
     * Set one option for CALL PQ
313
     *
314
     * @param string $key
315
     * @param int $value
316
     *
317
     * @return $this
318
     * @throws SphinxQLException
319
     */
320
    private function setOption($key, $value)
321
    {
322
        $value = intval($value);
323
        if (!in_array($key, [
324
            self::OPTION_DOCS_JSON,
325
            self::OPTION_DOCS,
326
            self::OPTION_VERBOSE,
327
            self::OPTION_QUERY
328
        ])) {
329
            throw new SphinxQLException('Unknown option');
330
        }
331
332
        if ($value != 0 && $value != 1) {
333
            throw new SphinxQLException('Option value can be only 1 or 0');
334
        }
335
336
        if ($key == self::OPTION_DOCS_JSON) {
337
            $this->throwExceptions = 1;
338
        }
339
340
        $this->options[$key] = $value;
341
        return $this;
342
    }
343
344
    /**
345
     * Set document parameter for CALL PQ
346
     *
347
     * @param array|string $documents
348
     * @return $this
349
     * @throws SphinxQLException
350
     */
351
    public function documents($documents)
352
    {
353
        if (empty($documents)) {
354
            throw new SphinxQLException('Document can\'t be empty');
355
        }
356
        $this->documents = $documents;
357
358
        return $this;
359
    }
360
361
    /**
362
     * Set options for CALL PQ
363
     *
364
     * @param array $options
365
     * @return $this
366
     * @throws SphinxQLException
367
     */
368
    public function options(array $options)
369
    {
370
        foreach ($options as $option => $value) {
371
            $this->setOption($option, $value);
372
        }
373
        return $this;
374
    }
375
376
377
    /**
378
     * Get and prepare options for CALL PQ
379
     *
380
     * @return string string
381
     */
382
    protected function getOptions()
383
    {
384
        $options = '';
385
        if (!empty($this->options)) {
386
            foreach ($this->options as $option => $value) {
387
                $options .= ', ' . $value . ' ' . $option;
388
            }
389
        }
390
391
        return $options;
392
    }
393
394
    /**
395
     * Check is array associative
396
     * @param array $arr
397
     * @return bool
398
     */
399
    private function isAssocArray(array $arr)
400
    {
401
        if (array() === $arr) {
402
            return false;
403
        }
404
        return array_keys($arr) !== range(0, count($arr) - 1);
405
    }
406
407
    /**
408
     * Get documents for CALL PQ. If option setted JSON - returns json_encoded
409
     *
410
     * Now selection of supported types work automatically. You don't need set
411
     * OPTION_DOCS_JSON to 1 or 0. But if you will set this option,
412
     * automatically enables exceptions on unsupported types
413
     *
414
     *
415
     * 1) If expect json = 0:
416
     *      a) doc can be 'catch me'
417
     *      b) doc can be multiple ['catch me if can','catch me']
418
     *
419
     * 2) If expect json = 1:
420
     *      a) doc can be jsonOBJECT {"subject": "document about orange"}
421
     *      b) docs can be jsonARRAY of jsonOBJECTS [{"subject": "document about orange"}, {"doc": "document about orange"}]
422
     *      c) docs can be phpArray of jsonObjects ['{"subject": "document about orange"}', '{"doc": "document about orange"}']
423
     *      d) doc can be associate array ['subject'=>'document about orange']
424
     *      e) docs can be array of associate arrays [['subject'=>'document about orange'], ['doc'=>'document about orange']]
425
     *
426
     *
427
     *
428
     *
429
     * @return string
430
     * @throws SphinxQLException
431
     */
432
    protected function getDocuments()
433
    {
434
        if (!empty($this->documents)) {
435
436
            if ($this->throwExceptions) {
437
438
                if ($this->options[self::OPTION_DOCS_JSON]) {
439
440
                    if (!is_array($this->documents)) {
441
                        $json = $this->prepareFromJson($this->documents);
442
                        if (!$json) {
443
                            throw new SphinxQLException('Documents must be in json format');
444
                        }
445
                    } else {
446
                        if (!$this->isAssocArray($this->documents) && !is_array($this->documents[0])) {
447
                            throw new SphinxQLException('Documents array must be associate');
448
                        }
449
                    }
450
                }
451
            }
452
453
            if (is_array($this->documents)) {
454
455
                // If input is phpArray with json like
456
                // ->documents(['{"body": "body of doc 1", "title": "title of doc 1"}',
457
                //             '{"subject": "subject of doc 3"}'])
458
                //
459
                // Checking first symbol of first array value. If [ or { - call checkJson
460
461
                if (!empty($this->documents[0]) && !is_array($this->documents[0]) &&
462
                    ($this->documents[0][0] == '[' || $this->documents[0][0] == '{')) {
463
464
                    $json = $this->prepareFromJson($this->documents);
465
                    if ($json) {
466
                        $this->options[self::OPTION_DOCS_JSON] = 1;
467
                        return $json;
468
                    }
469
470
                } else {
471
                    if (!$this->isAssocArray($this->documents)) {
472
473
                        // if incoming single array like ['catch me if can', 'catch me']
474
475
                        if (is_string($this->documents[0])) {
476
                            $this->options[self::OPTION_DOCS_JSON] = 0;
477
                            return '(' . $this->quoteString(implode('\', \'', $this->documents)) . ')';
478
                        }
479
480
                        // if doc is array of associate arrays [['foo'=>'bar'], ['foo1'=>'bar1']]
481
                        if (!empty($this->documents[0]) && $this->isAssocArray($this->documents[0])) {
482
                            $this->options[self::OPTION_DOCS_JSON] = 1;
483
                            return $this->convertArrayForQuery($this->documents);
484
                        }
485
486
                    } else {
487
                        if ($this->isAssocArray($this->documents)) {
488
                            // Id doc is associate array ['foo'=>'bar']
489
                            $this->options[self::OPTION_DOCS_JSON] = 1;
490
                            return $this->quoteString(json_encode($this->documents));
491
                        }
492
                    }
493
                }
494
495
            } else {
496
                if (is_string($this->documents)) {
0 ignored issues
show
introduced by
The condition is_string($this->documents) is always true.
Loading history...
497
498
                    $json = $this->prepareFromJson($this->documents);
499
                    if ($json) {
500
                        $this->options[self::OPTION_DOCS_JSON] = 1;
501
                        return $json;
502
                    }
503
504
                    $this->options[self::OPTION_DOCS_JSON] = 0;
505
                    return $this->quoteString($this->documents);
506
                }
507
            }
508
        }
509
        throw new SphinxQLException('Documents can\'t be empty');
510
    }
511
512
513
    /**
514
     * Set type
515
     *
516
     * @return $this
517
     */
518
    public function callPQ()
519
    {
520
        $this->clear();
521
        $this->type = 'call';
522
        return $this;
523
    }
524
525
526
    /**
527
     * Prepares documents for insert in valid format.
528
     * $data can be jsonArray of jsonObjects,
529
     * phpArray of jsonObjects, valid json or string
530
     *
531
     * @param string|array $data
532
     *
533
     * @return bool|string
534
     */
535
    private function prepareFromJson($data)
536
    {
537
        if (is_array($data)) {
538
            if (is_array($data[0])) {
539
                return false;
540
            }
541
            $return = [];
542
            foreach ($data as $item) {
543
                $return[] = $this->prepareFromJson($item);
544
            }
545
546
            return '(' . implode(', ', $return) . ')';
547
        }
548
        $array = json_decode($data, true);
549
550
        if (json_last_error() == JSON_ERROR_NONE) { // if json
551
            if ( ! empty($array[0])) { // If docs is jsonARRAY of jsonOBJECTS
552
                return $this->convertArrayForQuery($array);
553
            }
554
555
            // If docs is jsonOBJECT
556
            return $this->quoteString($data);
557
        }
558
559
        return false;
560
    }
561
562
563
    /**
564
     * Converts array of associate arrays to valid for query statement
565
     * ('jsonOBJECT1', 'jsonOBJECT2' ...)
566
     *
567
     *
568
     * @param array $array
569
     * @return string
570
     */
571
    private function convertArrayForQuery(array $array)
572
    {
573
        $documents = [];
574
        foreach ($array as $document) {
575
            $documents[] = json_encode($document);
576
        }
577
578
        return '(' . $this->quoteString(implode('\', \'', $documents)) . ')';
579
    }
580
581
582
    /**
583
     * Adding single quotes to string
584
     *
585
     * @param string $string
586
     * @return string
587
     */
588
    private function quoteString($string)
589
    {
590
        return '\'' . $string . '\'';
591
    }
592
593
594
    /**
595
     * Get last compiled query
596
     *
597
     * @return string
598
     */
599
    public function getLastQuery()
600
    {
601
        return $this->sphinxQL->getCompiled();
602
    }
603
}