MatchBuilder::before()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Foolz\SphinxQL;
4
5
/**
6
 * Query Builder class for Match statements.
7
 */
8
class MatchBuilder
9
{
10
    /**
11
     * The last compiled query.
12
     *
13
     * @var string
14
     */
15
    protected $last_compiled;
16
17
    /**
18
     * List of match operations.
19
     *
20
     * @var array
21
     */
22
    protected $tokens = array();
23
24
    /**
25
     * The owning SphinxQL object; used for escaping text.
26
     *
27
     * @var SphinxQL
28
     */
29
    protected $sphinxql;
30
31
    /**
32
     * @param SphinxQL $sphinxql
33
     */
34
    public function __construct(SphinxQL $sphinxql)
35
    {
36
        $this->sphinxql = $sphinxql;
37
    }
38
39
    /**
40
     * Match text or sub expression.
41
     *
42
     * Examples:
43
     *    $match->match('test');
44
     *    // test
45
     *
46
     *    $match->match('test case');
47
     *    // (test case)
48
     *
49
     *    $match->match(function ($m) {
50
     *        $m->match('a')->orMatch('b');
51
     *    });
52
     *    // (a | b)
53
     *
54
     *    $sub = new MatchBuilder($sphinxql);
55
     *    $sub->match('a')->orMatch('b');
56
     *    $match->match($sub);
57
     *    // (a | b)
58
     *
59
     * @param string|MatchBuilder|\Closure $keywords The text or expression to match.
60
     *
61
     * @return $this
62
     */
63
    public function match($keywords = null)
64
    {
65
        if ($keywords !== null) {
66
            $this->tokens[] = array('MATCH' => $keywords);
67
        }
68
69
        return $this;
70
    }
71
72
    /**
73
     * Provide an alternation match.
74
     *
75
     * Examples:
76
     *    $match->match('test')->orMatch();
77
     *    // test |
78
     *
79
     *    $match->match('test')->orMatch('case');
80
     *    // test | case
81
     *
82
     * @param string|MatchBuilder|\Closure $keywords The text or expression to alternatively match.
83
     *
84
     * @return $this
85
     */
86
    public function orMatch($keywords = null)
87
    {
88
        $this->tokens[] = array('OPERATOR' => '| ');
89
        $this->match($keywords);
90
91
        return $this;
92
    }
93
94
    /**
95
     * Provide an optional match.
96
     *
97
     * Examples:
98
     *    $match->match('test')->maybe();
99
     *    // test MAYBE
100
     *
101
     *    $match->match('test')->maybe('case');
102
     *    // test MAYBE case
103
     *
104
     * @param string|MatchBuilder|\Closure $keywords The text or expression to optionally match.
105
     *
106
     * @return $this
107
     */
108
    public function maybe($keywords = null)
109
    {
110
        $this->tokens[] = array('OPERATOR' => 'MAYBE ');
111
        $this->match($keywords);
112
113
        return $this;
114
    }
115
116
    /**
117
     * Do not match a keyword.
118
     *
119
     * Examples:
120
     *    $match->not()->match('test');
121
     *    // -test
122
     *
123
     *    $match->not('test');
124
     *    // -test
125
     *
126
     * @param string $keyword The word not to match.
127
     *
128
     * @return $this
129
     */
130
    public function not($keyword = null)
131
    {
132
        $this->tokens[] = array('OPERATOR' => '-');
133
        $this->match($keyword);
134
135
        return $this;
136
    }
137
138
    /**
139
     * Specify which field(s) to search.
140
     *
141
     * Examples:
142
     *    $match->field('*')->match('test');
143
     *    // @* test
144
     *
145
     *    $match->field('title')->match('test');
146
     *    // @title test
147
     *
148
     *    $match->field('body', 50)->match('test');
149
     *    // @body[50] test
150
     *
151
     *    $match->field('title', 'body')->match('test');
152
     *    // @(title,body) test
153
     *
154
     *    $match->field(['title', 'body'])->match('test');
155
     *    // @(title,body) test
156
     *
157
     *    $match->field('@relaxed')->field('nosuchfield')->match('test');
158
     *    // @@relaxed @nosuchfield test
159
     *
160
     * @param string|array $fields Field or fields to search.
161
     * @param int          $limit  Maximum position limit in field a match is allowed at.
162
     *
163
     * @return $this
164
     */
165
    public function field($fields, $limit = null)
166
    {
167
        if (is_string($fields)) {
168
            $fields = func_get_args();
169
            $limit = null;
170
        }
171
        if (!is_string(end($fields))) {
172
            $limit = array_pop($fields);
173
        }
174
        $this->tokens[] = array(
175
            'FIELD'  => '@',
176
            'fields' => $fields,
177
            'limit'  => $limit,
178
        );
179
180
        return $this;
181
    }
182
183
    /**
184
     * Specify which field(s) not to search.
185
     *
186
     * Examples:
187
     *    $match->ignoreField('title')->match('test');
188
     *    // @!title test
189
     *
190
     *    $match->ignoreField('title', 'body')->match('test');
191
     *    // @!(title,body) test
192
     *
193
     *    $match->ignoreField(['title', 'body'])->match('test');
194
     *    // @!(title,body) test
195
     *
196
     * @param string|array $fields Field or fields to ignore during search.
197
     *
198
     * @return $this
199
     */
200
    public function ignoreField($fields)
201
    {
202
        if (is_string($fields)) {
203
            $fields = func_get_args();
204
        }
205
        $this->tokens[] = array(
206
            'FIELD'  => '@!',
207
            'fields' => $fields,
208
            'limit'  => null,
209
        );
210
211
        return $this;
212
    }
213
214
    /**
215
     * Match an exact phrase.
216
     *
217
     * Example:
218
     *    $match->phrase('test case');
219
     *    // "test case"
220
     *
221
     * @param string $keywords The phrase to match.
222
     *
223
     * @return $this
224
     */
225
    public function phrase($keywords)
226
    {
227
        $this->tokens[] = array('PHRASE' => $keywords);
228
229
        return $this;
230
    }
231
232
    /**
233
     * Provide an optional phrase.
234
     *
235
     * Example:
236
     *    $match->phrase('test case')->orPhrase('another case');
237
     *    // "test case" | "another case"
238
     *
239
     * @param string $keywords The phrase to match.
240
     *
241
     * @return $this
242
     */
243
    public function orPhrase($keywords)
244
    {
245
        $this->tokens[] = array('OPERATOR' => '| ');
246
        $this->phrase($keywords);
247
248
        return $this;
249
    }
250
251
    /**
252
     * Match if keywords are close enough.
253
     *
254
     * Example:
255
     *    $match->proximity('test case', 5);
256
     *    // "test case"~5
257
     *
258
     * @param string $keywords The words to match.
259
     * @param int    $distance The upper limit on separation between words.
260
     *
261
     * @return $this
262
     */
263
    public function proximity($keywords, $distance)
264
    {
265
        $this->tokens[] = array(
266
            'PROXIMITY' => $distance,
267
            'keywords'  => $keywords,
268
        );
269
270
        return $this;
271
    }
272
273
    /**
274
     * Match if enough keywords are present.
275
     *
276
     * Examples:
277
     *    $match->quorum('this is a test case', 3);
278
     *    // "this is a test case"/3
279
     *
280
     *    $match->quorum('this is a test case', 0.5);
281
     *    // "this is a test case"/0.5
282
     *
283
     * @param string    $keywords  The words to match.
284
     * @param int|float $threshold The minimum number or percent of words that must match.
285
     *
286
     * @return $this
287
     */
288
    public function quorum($keywords, $threshold)
289
    {
290
        $this->tokens[] = array(
291
            'QUORUM'   => $threshold,
292
            'keywords' => $keywords,
293
        );
294
295
        return $this;
296
    }
297
298
    /**
299
     * Assert keywords or expressions must be matched in order.
300
     *
301
     * Examples:
302
     *    $match->match('test')->before();
303
     *    // test <<
304
     *
305
     *    $match->match('test')->before('case');
306
     *    // test << case
307
     *
308
     * @param string|Match|\Closure $keywords The text or expression that must come after.
309
     *
310
     * @return $this
311
     */
312
    public function before($keywords = null)
313
    {
314
        $this->tokens[] = array('OPERATOR' => '<< ');
315
        $this->match($keywords);
316
317
        return $this;
318
    }
319
320
    /**
321
     * Assert a keyword must be matched exactly as written.
322
     *
323
     * Examples:
324
     *    $match->match('test')->exact('cases');
325
     *    // test =cases
326
     *
327
     *    $match->match('test')->exact()->phrase('specific cases');
328
     *    // test ="specific cases"
329
     *
330
     * @param string $keyword The word that must be matched exactly.
331
     *
332
     * @return $this
333
     */
334
    public function exact($keyword = null)
335
    {
336
        $this->tokens[] = array('OPERATOR' => '=');
337
        $this->match($keyword);
338
339
        return $this;
340
    }
341
342
    /**
343
     * Boost the IDF score of a keyword.
344
     *
345
     * Examples:
346
     *    $match->match('test')->boost(1.2);
347
     *    // test^1.2
348
     *
349
     *    $match->match('test')->boost('case', 1.2);
350
     *    // test case^1.2
351
     *
352
     * @param string $keyword The word to modify the score of.
353
     * @param float  $amount  The amount to boost the score.
354
     *
355
     * @return $this
356
     */
357
    public function boost($keyword, $amount = null)
358
    {
359
        if ($amount === null) {
360
            $amount = $keyword;
361
        } else {
362
            $this->match($keyword);
363
        }
364
        $this->tokens[] = array('BOOST' => $amount);
365
366
        return $this;
367
    }
368
369
    /**
370
     * Assert keywords or expressions must be matched close to each other.
371
     *
372
     * Examples:
373
     *    $match->match('test')->near(3);
374
     *    // test NEAR/3
375
     *
376
     *    $match->match('test')->near('case', 3);
377
     *    // test NEAR/3 case
378
     *
379
     * @param string|Match|\Closure $keywords The text or expression to match nearby.
380
     * @param int                   $distance Maximum distance to the match.
381
     *
382
     * @return $this
383
     */
384
    public function near($keywords, $distance = null)
385
    {
386
        $this->tokens[] = array('NEAR' => $distance ?: $keywords);
387
        if ($distance !== null) {
388
            $this->match($keywords);
389
        }
390
391
        return $this;
392
    }
393
394
    /**
395
     * Assert matches must be in the same sentence.
396
     *
397
     * Examples:
398
     *    $match->match('test')->sentence();
399
     *    // test SENTENCE
400
     *
401
     *    $match->match('test')->sentence('case');
402
     *    // test SENTENCE case
403
     *
404
     * @param string|Match|\Closure $keywords The text or expression that must be in the sentence.
405
     *
406
     * @return $this
407
     */
408
    public function sentence($keywords = null)
409
    {
410
        $this->tokens[] = array('OPERATOR' => 'SENTENCE ');
411
        $this->match($keywords);
412
413
        return $this;
414
    }
415
416
    /**
417
     * Assert matches must be in the same paragraph.
418
     *
419
     * Examples:
420
     *    $match->match('test')->paragraph();
421
     *    // test PARAGRAPH
422
     *
423
     *    $match->match('test')->paragraph('case');
424
     *    // test PARAGRAPH case
425
     *
426
     * @param string|Match|\Closure $keywords The text or expression that must be in the paragraph.
427
     *
428
     * @return $this
429
     */
430
    public function paragraph($keywords = null)
431
    {
432
        $this->tokens[] = array('OPERATOR' => 'PARAGRAPH ');
433
        $this->match($keywords);
434
435
        return $this;
436
    }
437
438
    /**
439
     * Assert matches must be in the specified zone(s).
440
     *
441
     * Examples:
442
     *    $match->zone('th');
443
     *    // ZONE:(th)
444
     *
445
     *    $match->zone(['h3', 'h4']);
446
     *    // ZONE:(h3,h4)
447
     *
448
     *    $match->zone('th', 'test');
449
     *    // ZONE:(th) test
450
     *
451
     * @param string|array          $zones    The zone or zones to search.
452
     * @param string|Match|\Closure $keywords The text or expression that must be in these zones.
453
     *
454
     * @return $this
455
     */
456
    public function zone($zones, $keywords = null)
457
    {
458
        if (is_string($zones)) {
459
            $zones = array($zones);
460
        }
461
        $this->tokens[] = array('ZONE' => $zones);
462
        $this->match($keywords);
463
464
        return $this;
465
    }
466
467
468
    /**
469
     * Assert matches must be in the same instance of the specified zone.
470
     *
471
     * Examples:
472
     *    $match->zonespan('th');
473
     *    // ZONESPAN:(th)
474
     *
475
     *    $match->zonespan('th', 'test');
476
     *    // ZONESPAN:(th) test
477
     *
478
     * @param string                $zone     The zone to search.
479
     * @param string|Match|\Closure $keywords The text or expression that must be in this zone.
480
     *
481
     * @return $this
482
     */
483
    public function zonespan($zone, $keywords = null)
484
    {
485
        $this->tokens[] = array('ZONESPAN' => $zone);
486
        $this->match($keywords);
487
488
        return $this;
489
    }
490
491
    /**
492
     * Build the match expression.
493
     *
494
     * @return $this
495
     */
496
    public function compile()
497
    {
498
        $query = '';
499
        foreach ($this->tokens as $token) {
500
            if (key($token) == 'MATCH') {
501
                if ($token['MATCH'] instanceof Expression) {
502
                    $query .= $token['MATCH']->value().' ';
503
                } elseif ($token['MATCH'] instanceof MatchBuilder) {
504
                    $query .= '('.$token['MATCH']->compile()->getCompiled().') ';
505
                } elseif ($token['MATCH'] instanceof \Closure) {
506
                    $sub = new static($this->sphinxql);
507
                    call_user_func($token['MATCH'], $sub);
508
                    $query .= '('.$sub->compile()->getCompiled().') ';
509
                } elseif (strpos($token['MATCH'], ' ') === false) {
510
                    $query .= $this->sphinxql->escapeMatch($token['MATCH']).' ';
511
                } else {
512
                    $query .= '('.$this->sphinxql->escapeMatch($token['MATCH']).') ';
513
                }
514
            } elseif (key($token) == 'OPERATOR') {
515
                $query .= $token['OPERATOR'];
516
            } elseif (key($token) == 'FIELD') {
517
                $query .= $token['FIELD'];
518
                if (count($token['fields']) == 1) {
519
                    $query .= $token['fields'][0];
520
                } else {
521
                    $query .= '('.implode(',', $token['fields']).')';
522
                }
523
                if ($token['limit']) {
524
                    $query .= '['.$token['limit'].']';
525
                }
526
                $query .= ' ';
527
            } elseif (key($token) == 'PHRASE') {
528
                $query .= '"'.$this->sphinxql->escapeMatch($token['PHRASE']).'" ';
529
            } elseif (key($token) == 'PROXIMITY') {
530
                $query .= '"'.$this->sphinxql->escapeMatch($token['keywords']).'"~';
531
                $query .= $token['PROXIMITY'].' ';
532
            } elseif (key($token) == 'QUORUM') {
533
                $query .= '"'.$this->sphinxql->escapeMatch($token['keywords']).'"/';
534
                $query .= $token['QUORUM'].' ';
535
            } elseif (key($token) == 'BOOST') {
536
                $query = rtrim($query).'^'.$token['BOOST'].' ';
537
            } elseif (key($token) == 'NEAR') {
538
                $query .= 'NEAR/'.$token['NEAR'].' ';
539
            } elseif (key($token) == 'ZONE') {
540
                $query .= 'ZONE:('.implode(',', $token['ZONE']).') ';
541
            } elseif (key($token) == 'ZONESPAN') {
542
                $query .= 'ZONESPAN:('.$token['ZONESPAN'].') ';
543
            }
544
        }
545
        $this->last_compiled = trim($query);
546
547
        return $this;
548
    }
549
550
    /**
551
     * Returns the latest compiled match expression.
552
     *
553
     * @return string The last compiled match expression.
554
     */
555
    public function getCompiled()
556
    {
557
        return $this->last_compiled;
558
    }
559
}
560