Completed
Push — master ( 7e8e79...1f43e8 )
by Hung
02:20 queued 20s
created

Match   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 533
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 46
lcom 1
cbo 2
dl 0
loc 533
rs 8.3999
c 1
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A match() 0 7 2
A orMatch() 0 6 1
A maybe() 0 6 1
A not() 0 6 1
A field() 0 16 3
A ignoreField() 0 12 2
A phrase() 0 5 1
A orPhrase() 0 6 1
A proximity() 0 8 1
A quorum() 0 8 1
A before() 0 6 1
A exact() 0 6 1
A boost() 0 10 2
A near() 0 8 3
A sentence() 0 6 1
A paragraph() 0 6 1
A zone() 0 9 2
A zonespan() 0 6 1
C compile() 0 52 18
A getCompiled() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Match often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Match, and based on these observations, apply Extract Interface, too.

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