Completed
Push — master ( ea40c3...f7acad )
by Hung
01:27
created

Match::orPhrase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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