Issues (74)

Query/Select/AbstractSelectQuery.php (1 issue)

Severity
1
<?php
2
3
namespace Mdiyakov\DoctrineSolrBundle\Query\Select;
4
5
use Mdiyakov\DoctrineSolrBundle\Exception\SchemaConfigException;
6
use Mdiyakov\DoctrineSolrBundle\Schema\Field\DocumentFieldInterface;
7
use Solarium\Client;
8
use Mdiyakov\DoctrineSolrBundle\Exception\QueryException;
9
use Mdiyakov\DoctrineSolrBundle\Schema\Field\ConfigEntityField;
10
use Mdiyakov\DoctrineSolrBundle\Schema\Field\Entity\Field;
11
use Mdiyakov\DoctrineSolrBundle\Schema\Schema;
12
use Solarium\QueryType\Select\Result\Result;
13
14
abstract class AbstractSelectQuery
15
{
16
    /**
17
     * @var Client
18
     */
19
    private $client;
20
21
    /**
22
     * @var Schema
23
     */
24
    private $schema;
25
26
    /**
27
     * @var int
28
     */
29
    private $limit = 100;
30
31
    /**
32
     * @var int
33
     */
34
    private $offset = 0;
35
36
    /**
37
     * @var string[]
38
     */
39
    private $addAndCondition = [];
40
41
    /**
42
     * @var string[]
43
     */
44
    private $addOrCondition = [];
45
46
    /**
47
     * @var string[]
48
     */
49
    private $requiredDocumentFieldsNames = [];
50
51
52
    /**
53
     * @var \Solarium\QueryType\Select\Query\Query
54
     */
55
    private $solrQuery;
56
57
    /**
58
     * @var bool
59
     */
60
    private $isPhrase = false;
61
62
    /**
63
     * @var string[]
64
     */
65
    protected $discriminatorConditions = [];
66
67
    /**
68
     * @param Client $client
69
     * @param Schema $schema
70
     */
71
    public function __construct(Client $client, Schema $schema)
72
    {
73
        $this->client = $client;
74
        $this->schema = $schema;
75
76
        /** @var ConfigEntityField $discriminatorConfigField */
77
        $discriminatorConfigField = $this->getSchema()->getDiscriminatorConfigField();
78
        $entityPrimaryKeyField = $this->getSchema()->getEntityPrimaryKeyField();
79
80
        $this->requiredDocumentFieldsNames = [
81
            $discriminatorConfigField->getDocumentFieldName(),
82
            $entityPrimaryKeyField->getDocumentFieldName()
83
        ];
84
85
        $this->createSolrQuery();
86
        $this->initDiscriminatorConditions();
87
    }
88
89
    /**
90
     * @return object[]
91
     */
92
    public function getResult()
93
    {
94
        $query = $this->getSolrQuery()
95
            ->setQuery(
96
                $this->getQueryString()
97
            )
98
            ->setRows($this->getLimit())
99
            ->setStart($this->getOffset());
100
101
        if ($this->isPhrase) {
102
            $query->setQuery(
103
                $query->getHelper()->qparser('complexphrase') .
104
                $query->getQuery()
105
            );
106
        }
107
108
        /** @var Result $response */
109
        $response = $this->getClient()->execute($query);
110
111
        $documents = $response->getDocuments();
112
        if (empty($documents)) {
113
            return [];
114
        }
115
116
        return $this->transformResponse($response);
117
    }
118
119
    /**
120
     * @param Result $result
121
     * @return object[]
122
     */
123
    abstract protected function transformResponse(Result $result);
124
125
    abstract protected function initDiscriminatorConditions();
126
127
    /**
128
     * @return \Solarium\QueryType\Select\Query\Query
129
     */
130
    protected function getSolrQuery()
131
    {
132
        return $this->solrQuery;
133
    }
134
135
    /**
136
     * @param array|string $entityFieldNames
137
     */
138
    public function select($entityFieldNames)
139
    {
140
        if (!is_array($entityFieldNames)) {
141
            $entityFieldNames = [$entityFieldNames];
142
        }
143
144
        foreach($entityFieldNames as $entityFieldName) {
145
            $this->getField($entityFieldName);
146
        }
147
148
        $this->getSolrQuery()->setFields(array_merge($entityFieldNames, $this->requiredDocumentFieldsNames));
149
    }
150
151
    /**
152
     * @param string $searchTerm
153
     * @param bool $isNegative
154
     * @param bool $wildcard
155
     * @return AbstractSelectQuery
156
     */
157
    public function addAllFieldOrWhere($searchTerm, $isNegative = false, $wildcard = false)
158
    {
159
        $this->reset();
160
        $searchTerm = $this->prepareSearchTerm($searchTerm, $wildcard);
161
        $fields = $this->getSchema()->getFields();
162
        foreach ($fields as $field) {
163
            $this->addOrCondition($field, $searchTerm, $wildcard, $isNegative);
164
        }
165
166
        return $this;
167
    }
168
169
    /**
170
     * @param string $entityFieldName
171
     * @param string $from
172
     * @param string $to
173
     * @param bool|false $exclusiveFrom
174
     * @param bool|false $exclusiveTo
175
     * @param bool $isNegative
176
     * @return $this
177
     */
178
    public function addRangeOrWhere($entityFieldName, $from = '*', $to = '*', $exclusiveFrom = false, $exclusiveTo = false, $isNegative = false)
179
    {
180
        $from = $this->prepareSearchTerm($from);
181
        $to = $this->prepareSearchTerm($to);
182
        if ($from && $to) {
183
            $field = $this->prepareField($entityFieldName, false);
184
            $this->addRangeOrCondition($field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative);
185
        }
186
187
        return $this;
188
    }
189
190
191
    /**
192
     * @param string $entityFieldName
193
     * @param string $from
194
     * @param string $to
195
     * @param bool|false $exclusiveFrom
196
     * @param bool|false $exclusiveTo
197
     * @param bool $isNegative
198
     * @return $this
199
     */
200
    public function addRangeAndWhere($entityFieldName, $from = '*', $to = '*', $exclusiveFrom = false, $exclusiveTo = false, $isNegative = false)
201
    {
202
        $from = $this->prepareSearchTerm($from);
203
        $to = $this->prepareSearchTerm($to);
204
        if ($from && $to) {
205
            $field = $this->prepareField($entityFieldName, false);
206
            $this->addRangeAndCondition($field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative);
207
        }
208
209
        return $this;
210
    }
211
212
    /**
213
     * @param string $entityFieldName
214
     * @param string $searchTerm
215
     * @param bool $isNegative
216
     * @param int $distance
217
     * @return AbstractSelectQuery
218
     */
219
    public function addFuzzyOrWhere($entityFieldName, $searchTerm, $isNegative = false, $distance = 1)
220
    {
221
        $searchTerm = $this->prepareSearchTerm($searchTerm);
222
        if ($searchTerm) {
223
            $field = $this->prepareField($entityFieldName, false);
224
            $this->addFuzzyOrCondition($field, $searchTerm, $isNegative, $distance);
225
        }
226
227
        return $this;
228
    }
229
230
    /**
231
     * @param string $entityFieldName
232
     * @param string|number $searchTerm
233
     * @param bool|false $isNegative
234
     * @param int $distance
235
     * @return AbstractSelectQuery
236
     */
237
    public function addFuzzyAndWhere($entityFieldName, $searchTerm, $isNegative = false, $distance = 1)
238
    {
239
        $searchTerm = $this->prepareSearchTerm($searchTerm);
240
        if ($searchTerm) {
241
            $field = $this->prepareField($entityFieldName, false);
242
            $this->addFuzzyAndCondition($field, $searchTerm, $isNegative, $distance);
243
        }
244
245
        return $this;
246
    }
247
248
    /**
249
     * @param string $entityFieldName
250
     * @param string $searchTerm
251
     * @param bool $isNegative
252
     * @param bool $wildcard
253
     * @return AbstractSelectQuery
254
     */
255
    public function addOrWhere($entityFieldName, $searchTerm, $isNegative = false, $wildcard = false)
256
    {
257
        $searchTerm = $this->prepareSearchTerm($searchTerm, $wildcard);
258
        if ($searchTerm) {
259
            $field = $this->prepareField($entityFieldName, false);
260
            $this->addOrCondition($field, $searchTerm, $wildcard, $isNegative);
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * @param $entityFieldName
268
     * @param $searchTerm
269
     * @param bool|false $isNegative
270
     * @param bool|false $wildcard
271
     * @return AbstractSelectQuery
272
     */
273
    public function addAndWhere($entityFieldName, $searchTerm, $isNegative = false, $wildcard = false)
274
    {
275
        $searchTerm = $this->prepareSearchTerm($searchTerm, $wildcard);
276
        if ($searchTerm) {
277
            $field = $this->prepareField($entityFieldName, false);
278
            $this->addAndCondition($field, $searchTerm, $wildcard, $isNegative);
279
        }
280
281
        return $this;
282
    }
283
284
    /**
285
     * @param string $configFieldName
286
     * @param string $searchTerm
287
     * @param bool $isNegative
288
     * @param bool|false $wildcard
289
     * @return AbstractSelectQuery
290
     */
291
    public function addConfigFieldOrWhere($configFieldName, $searchTerm, $isNegative = false, $wildcard = false)
292
    {
293
        $searchTerm = $this->prepareSearchTerm($searchTerm, $wildcard);
294
        if ($searchTerm) {
295
            $field = $this->prepareField($configFieldName, true);
296
            $this->addOrCondition($field, $searchTerm, $wildcard, $isNegative);
297
        }
298
299
        return $this;
300
    }
301
302
    /**
303
     * @return string
304
     */
305
    public function getQueryString()
306
    {
307
        $discriminatorConditions = join(' OR ', $this->discriminatorConditions);
308
        if (count(array_merge($this->addOrCondition, $this->addAndCondition)) === 0) {
309
            return $discriminatorConditions;
310
        }
311
312
        $orCondition = join(' OR ', $this->addOrCondition);
313
        $andCondition = join(' AND ', $this->addAndCondition);
314
315
316
        if ($orCondition && $andCondition) {
317
            $result = sprintf(
318
                '(%s AND %s) AND (%s)',
319
                $orCondition,
320
                $andCondition,
321
                $discriminatorConditions
322
            );
323
        } else {
324
            $conditions = $orCondition ?: $andCondition;
325
            $result = sprintf('(%s) AND (%s)', $conditions, $discriminatorConditions);
326
        }
327
328
        return $result;
329
330
    }
331
332
    /**
333
     * @return AbstractSelectQuery
334
     */
335
    public function groupConditionsAsOr()
336
    {
337
        $this->addOrCondition = [ $this->buildGroupCondition() ];
338
        $this->addAndCondition = [];
339
340
        return $this;
341
    }
342
343
    /**
344
     * @return AbstractSelectQuery
345
     */
346
    public function groupConditionsAsAnd()
347
    {
348
        $this->addAndCondition = [ $this->buildGroupCondition() ];
349
        $this->addOrCondition = [];
350
351
        return $this;
352
    }
353
354
    /**
355
     * @return int
356
     */
357
    public function getLimit()
358
    {
359
        return $this->limit;
360
    }
361
362
    /**
363
     * @param int $limit
364
     * @return AbstractSelectQuery
365
     */
366
    public function setLimit($limit)
367
    {
368
        $this->limit = $limit;
369
370
        return $this;
371
    }
372
373
    /**
374
     * @return int
375
     */
376
    public function getOffset()
377
    {
378
        return $this->offset;
379
    }
380
381
    /**
382
     * @param int $offset
383
     * @return AbstractSelectQuery
384
     */
385
    public function setOffset($offset)
386
    {
387
        $this->offset = $offset;
388
389
        return $this;
390
    }
391
392
    /**
393
     * @return AbstractSelectQuery
394
     */
395
    public function reset()
396
    {
397
        $this->isPhrase = false;
398
        $this->addAndCondition = [];
399
        $this->addOrCondition = [];
400
        $this->offset = 0;
401
        $this->limit = 100;
402
        $this->createSolrQuery();
403
404
        return $this;
405
    }
406
407
    /**
408
     * @return Client
409
     */
410
    protected function getClient()
411
    {
412
        return $this->client;
413
    }
414
415
    /**
416
     * @return Schema
417
     */
418
    protected function getSchema()
419
    {
420
        return $this->schema;
421
    }
422
423
    /**
424
     * @param string $fieldName
425
     * @param bool|false $isConfigField
426
     * @return DocumentFieldInterface
427
     */
428
    private function prepareField($fieldName, $isConfigField = false)
429
    {
430
        if (!is_string($fieldName)) {
0 ignored issues
show
The condition is_string($fieldName) is always true.
Loading history...
431
            throw new QueryException('FieldName argument must be a string');
432
        }
433
434
        if ($isConfigField) {
435
            $field = $this->schema->getConfigFieldByName($fieldName);
436
        } else {
437
            $field = $this->getField($fieldName);
438
        }
439
440
        return $field;
441
    }
442
443
    /**
444
     * @param DocumentFieldInterface $field
445
     * @param string $searchTerm
446
     * @param bool $wildcard
447
     * @param bool $isNegative
448
     */
449
    private function addOrCondition(DocumentFieldInterface $field, $searchTerm, $wildcard, $isNegative)
450
    {
451
        $condition = $this->buildFieldCondition($field, $searchTerm, $wildcard, $isNegative);
452
        $this->addOrCondition[] = $condition;
453
    }
454
455
    /**
456
     * @param DocumentFieldInterface $field
457
     * @param string $searchTerm
458
     * @param bool $wildcard
459
     * @param bool $isNegative
460
     */
461
    private function addAndCondition(DocumentFieldInterface $field, $searchTerm, $wildcard, $isNegative)
462
    {
463
        $condition = $this->buildFieldCondition($field, $searchTerm, $wildcard, $isNegative);
464
        $this->addAndCondition[] = $condition;
465
    }
466
467
    /**
468
     * @param DocumentFieldInterface $field
469
     * @param string $searchTerm
470
     * @param bool $wildcard
471
     * @param bool $isNegative
472
     * @return string
473
     */
474
    private function buildFieldCondition(DocumentFieldInterface $field, $searchTerm, $wildcard, $isNegative)
475
    {
476
        $fieldPartFormat = '%s:';
477
        $valuePartFormat = '"%s"';
478
        $isPhrase = (strpos($searchTerm, ' ') > 0);
479
        $this->isPhrase = $isPhrase ?: $this->isPhrase;
480
481
        if ($wildcard && !$isPhrase) {
482
            $valuePartFormat = '%s';
483
        }
484
485
        if ($field->getPriority()) {
486
            $format = $fieldPartFormat . sprintf('(%s)^%%s', $valuePartFormat);
487
            $condition = sprintf($format, $field->getDocumentFieldName(), $searchTerm, $field->getPriority());
488
        } else {
489
            $condition = sprintf($fieldPartFormat . $valuePartFormat , $field->getDocumentFieldName(), $searchTerm);
490
        }
491
492
        $condition = $isNegative ? $this->buildNegativeCondition($condition) : $condition;
493
494
        return $condition;
495
    }
496
497
    /**
498
     * @param DocumentFieldInterface $field
499
     * @param string $from
500
     * @param string $to
501
     * @param bool $exclusiveFrom
502
     * @param bool  $exclusiveTo
503
     * @param bool $isNegative
504
     * @return string
505
     */
506
    private function buildRangeCondition(DocumentFieldInterface $field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative)
507
    {
508
        $fieldPartFormat = '%s:';
509
        $valuePartFormat = $exclusiveFrom ? '{' : '[';
510
        $valuePartFormat .= '%s TO %s';
511
        $valuePartFormat .= $exclusiveTo ? '}' : ']';
512
        $condition = $fieldPartFormat . $valuePartFormat;
513
514
515
        $condition = $isNegative ?  $this->buildNegativeCondition($condition) : $condition;
516
517
        return sprintf(
518
            $condition,
519
            $field->getDocumentFieldName(),
520
            $from,
521
            $to
522
        );
523
    }
524
525
    /**
526
     * @param $condition
527
     * @return string
528
     */
529
    private function buildNegativeCondition($condition)
530
    {
531
        return sprintf('(*:* AND -%s)', $condition);
532
    }
533
534
    /**
535
     * @param DocumentFieldInterface $field
536
     * @param string $searchTerm
537
     * @param bool $isNegative
538
     * @param int $distance
539
     * @return string
540
     */
541
    private function buildFuzzyCondition(DocumentFieldInterface $field, $searchTerm, $isNegative, $distance)
542
    {
543
        $fieldPartFormat = '%s:';
544
        if (strpos($searchTerm, ' ') > 0) {
545
            $parts = explode(' ', $searchTerm);
546
            $formattedParts = [];
547
            foreach ($parts as $part)  {
548
                $formattedParts[] = sprintf('%s~%u', $part, $distance);
549
            }
550
            $valuePartFormat = '"%s"';
551
            $this->isPhrase = true;
552
            $searchTerm = join(' ', $formattedParts);
553
        } else {
554
            $valuePartFormat = '%s';
555
            $searchTerm = sprintf('%s~%u', $searchTerm, $distance);
556
        }
557
558
        $condition = $fieldPartFormat . $valuePartFormat;
559
        $condition = $isNegative ?  $this->buildNegativeCondition($condition) : $condition;
560
561
        return sprintf(
562
            $condition,
563
            $field->getDocumentFieldName(),
564
            $searchTerm
565
        );
566
    }
567
568
    private function buildGroupCondition()
569
    {
570
        $currentOrConditions = join(' OR ', $this->addOrCondition);
571
        $currentAndConditions = join(' AND ', $this->addAndCondition);
572
573
        if ($currentOrConditions && $currentAndConditions) {
574
            $groupedCondition = sprintf('(%s AND %s)', $currentOrConditions, $currentAndConditions);
575
        } else {
576
            $conditions =  $currentOrConditions ?: $currentAndConditions;
577
            $groupedCondition = sprintf('(%s)', $conditions);
578
        }
579
580
        return $groupedCondition;
581
    }
582
583
    /**
584
     * @param DocumentFieldInterface $field
585
     * @param string $from
586
     * @param string $to
587
     * @param bool $exclusiveFrom
588
     * @param bool $exclusiveTo
589
     * @param bool $isNegative
590
     */
591
    private function addRangeOrCondition(DocumentFieldInterface $field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative)
592
    {
593
        $this->addOrCondition[] = $this->buildRangeCondition($field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative);
594
    }
595
596
    /**
597
     * @param DocumentFieldInterface $field
598
     * @param string $from
599
     * @param string $to
600
     * @param bool $exclusiveFrom
601
     * @param bool $exclusiveTo
602
     * @param bool $isNegative
603
     */
604
    private function addRangeAndCondition(DocumentFieldInterface $field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative)
605
    {
606
        $this->addAndCondition[] = $this->buildRangeCondition($field, $from, $to, $exclusiveFrom, $exclusiveTo, $isNegative);
607
    }
608
609
    /**
610
     * @param DocumentFieldInterface $field
611
     * @param string $searchTerm
612
     * @param bool $isNegative
613
     * @param int $distance
614
     */
615
    private function addFuzzyOrCondition(DocumentFieldInterface $field, $searchTerm, $isNegative, $distance)
616
    {
617
        $this->addOrCondition[] = $this->buildFuzzyCondition($field, $searchTerm, $isNegative, $distance);
618
    }
619
620
    /**
621
     * @param DocumentFieldInterface $field
622
     * @param string $searchTerm
623
     * @param bool $isNegative
624
     * @param int $distance
625
     */
626
    private function addFuzzyAndCondition(DocumentFieldInterface $field, $searchTerm, $isNegative, $distance)
627
    {
628
        $this->addAndCondition[] = $this->buildFuzzyCondition($field, $searchTerm, $isNegative, $distance);
629
    }
630
631
    /**
632
     * @param string $entityFieldName
633
     * @return Field
634
     * @throws SchemaConfigException
635
     */
636
    private function getField($entityFieldName)
637
    {
638
        return $this->getSchema()->getFieldByEntityFieldName($entityFieldName);
639
    }
640
641
    /**
642
     * @param $searchTerm
643
     * @param bool $wildcard
644
     * @return mixed
645
     * @throws QueryException
646
     */
647
    private function prepareSearchTerm($searchTerm, $wildcard = false)
648
    {
649
        if (!is_scalar($searchTerm)) {
650
            throw new QueryException('SearchTerm argument must be a scalar');
651
        }
652
653
        $specialSymbols = ['+','-','&&','||','!','(',')','{','}','[',']','^','"','~',':','/'];
654
        $escapedSpecialSymbols = ['\+','\-','\&&','\||','\!','\(','\)','\{','\}','\[','\]','\^','\"','\~','\:','\/'];
655
656
        if (!$wildcard) {
657
            $specialSymbols = array_merge($specialSymbols, ['*','?']);
658
            $escapedSpecialSymbols = array_merge($escapedSpecialSymbols, ['\*','\?']);
659
        }
660
661
        $searchTerm = preg_replace('/[^a-zA-Z\s0-9-_=+.?*!:)(\]\[ ]/', '', $searchTerm);
662
663
        return str_replace($specialSymbols, $escapedSpecialSymbols, $searchTerm);
664
    }
665
666
667
    private function createSolrQuery()
668
    {
669
        $this->solrQuery = $this->client->createSelect()
670
            ->setFields($this->requiredDocumentFieldsNames);
671
    }
672
673
}