Completed
Pull Request — master (#102)
by Дмитрий
03:31
created

SphinxQL   D

Complexity

Total Complexity 147

Size/Duplication

Total Lines 1376
Duplicated Lines 7.12 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 47
Bugs 9 Features 12
Metric Value
wmc 147
c 47
b 9
f 12
lcom 1
cbo 5
dl 98
loc 1376
rs 4

60 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 4 1
A getConnection() 0 4 1
A expr() 0 4 1
A getResult() 0 4 1
A getCompiled() 0 4 1
A transactionBegin() 0 4 1
A transactionCommit() 0 4 1
A transactionRollback() 0 4 1
F compileSelect() 53 132 27
A match() 0 10 4
A __construct() 0 4 1
A execute() 0 5 1
A executeBatch() 0 14 3
A enqueue() 0 10 2
A getQueue() 0 13 3
A getQueuePrev() 0 4 1
A setQueuePrev() 0 6 1
C compile() 0 23 7
A compileQuery() 0 6 1
C compileMatch() 0 39 8
B compileWhere() 0 19 7
B compileFilterCondition() 0 28 5
B compileInsert() 3 32 6
B compileUpdate() 0 34 5
A compileDelete() 0 17 3
A query() 0 7 1
A select() 0 13 2
A insert() 0 7 1
A replace() 0 7 1
A update() 0 8 1
A delete() 0 7 1
B from() 0 12 5
A where() 15 15 2
A groupBy() 0 6 1
A withinGroupOrderBy() 0 6 1
A having() 15 15 2
A orderBy() 0 6 1
A limit() 12 12 2
A offset() 0 6 1
A option() 0 6 1
A into() 0 6 1
A columns() 0 10 2
A values() 0 10 2
A value() 0 11 3
A set() 0 8 2
A facet() 0 6 1
A setHalfEscapeChars() 0 8 2
A compileEscapeChars() 0 9 2
A escapeMatch() 0 8 2
B halfEscapeMatch() 0 26 3
A setFullEscapeChars() 0 8 2
A reset() 0 21 1
A resetWhere() 0 6 1
A resetMatch() 0 6 1
A resetGroupBy() 0 6 1
A resetWithinGroupOrderBy() 0 6 1
A resetHaving() 0 6 1
A resetOrderBy() 0 6 1
A resetOptions() 0 6 1
A setSelect() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SphinxQL 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 SphinxQL, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Foolz\SphinxQL;
4
use Foolz\SphinxQL\Drivers\ConnectionInterface;
5
use Foolz\SphinxQL\Exception\SphinxQLException;
6
use Foolz\SphinxQL\Drivers\MultiResultSetInterface;
7
use Foolz\SphinxQL\Drivers\ResultSetInterface;
8
9
/**
10
 * Query Builder class for SphinxQL statements.
11
 * @package Foolz\SphinxQL
12
 */
13
class SphinxQL
14
{
15
    /**
16
     * A non-static connection for the current SphinxQL object
17
     *
18
     * @var ConnectionInterface
19
     */
20
    protected $connection = null;
21
22
    /**
23
     * The last result object.
24
     *
25
     * @var array
26
     */
27
    protected $last_result = null;
28
29
    /**
30
     * The last compiled query.
31
     *
32
     * @var string
33
     */
34
    protected $last_compiled = null;
35
36
    /**
37
     * The last chosen method (select, insert, replace, update, delete).
38
     *
39
     * @var string
40
     */
41
    protected $type = null;
42
43
    /**
44
     * An SQL query that is not yet executed or "compiled"
45
     *
46
     * @var string
47
     */
48
    protected $query = null;
49
50
    /**
51
     * Array of select elements that will be comma separated.
52
     *
53
     * @var array
54
     */
55
    protected $select = array();
56
57
    /**
58
     * From in SphinxQL is the list of indexes that will be used
59
     *
60
     * @var array
61
     */
62
    protected $from = array();
63
64
    /**
65
     * The list of where and parenthesis, must be inserted in order
66
     *
67
     * @var array
68
     */
69
    protected $where = array();
70
71
    /**
72
     * The list of matches for the MATCH function in SphinxQL
73
     *
74
     * @var array
75
     */
76
    protected $match = array();
77
78
    /**
79
     * GROUP BY array to be comma separated
80
     *
81
     * @var array
82
     */
83
    protected $group_by = array();
84
85
    /**
86
     * ORDER BY array
87
     *
88
     * @var array
89
     */
90
    protected $within_group_order_by = array();
91
92
    /**
93
     * The list of where and parenthesis, must be inserted in order
94
     *
95
     * @var array
96
     */
97
    protected $having = array();
98
99
    /**
100
     * ORDER BY array
101
     *
102
     * @var array
103
     */
104
    protected $order_by = array();
105
106
    /**
107
     * When not null it adds an offset
108
     *
109
     * @var null|int
110
     */
111
    protected $offset = null;
112
113
    /**
114
     * When not null it adds a limit
115
     *
116
     * @var null|int
117
     */
118
    protected $limit = null;
119
120
    /**
121
     * Value of INTO query for INSERT or REPLACE
122
     *
123
     * @var null|string
124
     */
125
    protected $into = null;
126
127
    /**
128
     * Array of columns for INSERT or REPLACE
129
     *
130
     * @var array
131
     */
132
    protected $columns = array();
133
134
    /**
135
     * Array OF ARRAYS of values for INSERT or REPLACE
136
     *
137
     * @var array
138
     */
139
    protected $values = array();
140
141
    /**
142
     * Array arrays containing column and value for SET in UPDATE
143
     *
144
     * @var array
145
     */
146
    protected $set = array();
147
148
    /**
149
     * Array of OPTION specific to SphinxQL
150
     *
151
     * @var array
152
     */
153
    protected $options = array();
154
155
    /**
156
     * Array of FACETs
157
     *
158
     * @var Facet[]
159
     */
160
    protected $facets = array();
161
162
    /**
163
     * The reference to the object that queued itself and created this object
164
     *
165
     * @var null|SphinxQL
166
     */
167
    protected $queue_prev = null;
168
169
    /**
170
     * An array of escaped characters for escapeMatch()
171
     * @var array
172
     */
173
    protected $escape_full_chars = array(
174
        '\\' => '\\\\',
175
        '(' => '\(',
176
        ')' => '\)',
177
        '|' => '\|',
178
        '-' => '\-',
179
        '!' => '\!',
180
        '@' => '\@',
181
        '~' => '\~',
182
        '"' => '\"',
183
        '&' => '\&',
184
        '/' => '\/',
185
        '^' => '\^',
186
        '$' => '\$',
187
        '=' => '\=',
188
        '<' => '\<',
189
    );
190
191
    /**
192
     * An array of escaped characters for fullEscapeMatch()
193
     * @var array
194
     */
195
    protected $escape_half_chars = array(
196
        '\\' => '\\\\',
197
        '(' => '\(',
198
        ')' => '\)',
199
        '!' => '\!',
200
        '@' => '\@',
201
        '~' => '\~',
202
        '&' => '\&',
203
        '/' => '\/',
204
        '^' => '\^',
205
        '$' => '\$',
206
        '=' => '\=',
207
        '<' => '\<',
208
    );
209
210
    public function __construct(ConnectionInterface $connection = null, $static = false)
0 ignored issues
show
Unused Code introduced by
The parameter $static is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
211
    {
212
        $this->connection = $connection;
213
    }
214
215
    /**
216
     * Creates and setups a SphinxQL object
217
     *
218
     * @param ConnectionInterface $connection
219
     *
220
     * @return SphinxQL
221
     */
222
    public static function create(ConnectionInterface $connection)
223
    {
224
        return new static($connection);
225
    }
226
227
    /**
228
     * Returns the currently attached connection
229
     *
230
     * @returns ConnectionInterface
231
     */
232
    public function getConnection()
233
    {
234
        return $this->connection;
235
    }
236
237
    /**
238
     * Avoids having the expressions escaped
239
     *
240
     * Examples:
241
     *    $query->where('time', '>', SphinxQL::expr('CURRENT_TIMESTAMP'));
242
     *    // WHERE time > CURRENT_TIMESTAMP
243
     *
244
     * @param string $string The string to keep unaltered
245
     *
246
     * @return Expression The new Expression
247
     */
248
    public static function expr($string = '')
249
    {
250
        return new Expression($string);
251
    }
252
253
    /**
254
     * Runs the query built
255
     *
256
     * @return ResultSetInterface The result of the query
257
     */
258
    public function execute()
259
    {
260
        // pass the object so execute compiles it by itself
261
        return $this->last_result = $this->getConnection()->query($this->compile()->getCompiled());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getConnection()->...mpile()->getCompiled()) of type object<Foolz\SphinxQL\Drivers\ResultSetInterface> is incompatible with the declared type array of property $last_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
262
    }
263
264
    /**
265
     * Executes a batch of queued queries
266
     *
267
     * @return MultiResultSetInterface The array of results
268
     * @throws SphinxQLException In case no query is in queue
269
     */
270
    public function executeBatch()
271
    {
272
        if (count($this->getQueue()) == 0) {
273
            throw new SphinxQLException('There is no Queue present to execute.');
274
        }
275
276
        $queue = array();
277
278
        foreach ($this->getQueue() as $query) {
279
            $queue[] = $query->compile()->getCompiled();
280
        }
281
282
        return $this->last_result = $this->getConnection()->multiQuery($queue);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getConnection()->multiQuery($queue) of type object<Foolz\SphinxQL\Dr...ultiResultSetInterface> is incompatible with the declared type array of property $last_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
283
    }
284
285
    /**
286
     * Enqueues the current object and returns a new one or the supplied one
287
     *
288
     * @param SphinxQL|null $next
289
     *
290
     * @return SphinxQL A new SphinxQL object with the current object referenced
291
     */
292
    public function enqueue(SphinxQL $next = null)
293
    {
294
        if ($next === null) {
295
            $next = new static($this->getConnection());
296
        }
297
298
        $next->setQueuePrev($this);
299
300
        return $next;
301
    }
302
303
    /**
304
     * Returns the ordered array of enqueued objects
305
     *
306
     * @return SphinxQL[] The ordered array of enqueued objects
307
     */
308
    public function getQueue()
309
    {
310
        $queue = array();
311
        $curr = $this;
312
313
        do {
314
            if ($curr->type != null) {
315
                $queue[] = $curr;
316
            }
317
        } while ($curr = $curr->getQueuePrev());
318
319
        return array_reverse($queue);
320
    }
321
322
    /**
323
     * Gets the enqueued object
324
     *
325
     * @return SphinxQL|null
326
     */
327
    public function getQueuePrev()
328
    {
329
        return $this->queue_prev;
330
    }
331
332
    /**
333
     * Sets the reference to the enqueued object
334
     *
335
     * @param SphinxQL $query The object to set as previous
336
     *
337
     * @return SphinxQL
338
     */
339
    public function setQueuePrev($query)
340
    {
341
        $this->queue_prev = $query;
342
343
        return $this;
344
    }
345
346
    /**
347
     * Returns the result of the last query
348
     *
349
     * @return array The result of the last query
350
     */
351
    public function getResult()
352
    {
353
        return $this->last_result;
354
    }
355
356
    /**
357
     * Returns the latest compiled query
358
     *
359
     * @return string The last compiled query
360
     */
361
    public function getCompiled()
362
    {
363
        return $this->last_compiled;
364
    }
365
366
    /**
367
     * Begins transaction
368
     */
369
    public function transactionBegin()
370
    {
371
        $this->getConnection()->query('BEGIN');
372
    }
373
374
    /**
375
     * Commits transaction
376
     */
377
    public function transactionCommit()
378
    {
379
        $this->getConnection()->query('COMMIT');
380
    }
381
382
    /**
383
     * Rollbacks transaction
384
     */
385
    public function transactionRollback()
386
    {
387
        $this->getConnection()->query('ROLLBACK');
388
    }
389
390
    /**
391
     * Runs the compile function
392
     *
393
     * @return SphinxQL
394
     */
395
    public function compile()
396
    {
397
        switch ($this->type) {
398
            case 'select':
399
                $this->compileSelect();
400
                break;
401
            case 'insert':
402
            case 'replace':
403
                $this->compileInsert();
404
                break;
405
            case 'update':
406
                $this->compileUpdate();
407
                break;
408
            case 'delete':
409
                $this->compileDelete();
410
                break;
411
            case 'query':
412
                $this->compileQuery();
413
                break;
414
        }
415
416
        return $this;
417
    }
418
419
    public function compileQuery()
420
    {
421
        $this->last_compiled = $this->query;
422
423
        return $this;
424
    }
425
426
    /**
427
     * Compiles the MATCH part of the queries
428
     * Used by: SELECT, DELETE, UPDATE
429
     *
430
     * @return string The compiled MATCH
431
     */
432
    public function compileMatch()
433
    {
434
        $query = '';
435
436
        if (!empty($this->match)) {
437
            $query .= 'WHERE MATCH(';
438
439
            $matched = array();
440
441
            foreach ($this->match as $match) {
442
                $pre = '';
443
                if ($match['column'] instanceof \Closure) {
444
                    $sub = new Match($this);
445
                    call_user_func($match['column'], $sub);
446
                    $pre .= $sub->compile()->getCompiled();
447
                } elseif ($match['column'] instanceof Match) {
448
                    $pre .= $match['column']->compile()->getCompiled();
449
                } elseif (empty($match['column'])) {
450
                    $pre .= '';
451
                } elseif (is_array($match['column'])) {
452
                    $pre .= '@('.implode(',', $match['column']).') ';
453
                } else {
454
                    $pre .= '@'.$match['column'].' ';
455
                }
456
457
                if ($match['half']) {
458
                    $pre .= $this->halfEscapeMatch($match['value']);
459
                } else {
460
                    $pre .= $this->escapeMatch($match['value']);
461
                }
462
463
                $matched[] = '('.$pre.')';
464
            }
465
466
            $matched = implode(' ', $matched);
467
            $query .= $this->getConnection()->escape(trim($matched)).') ';
468
        }
469
        return $query;
470
    }
471
472
    /**
473
     * Compiles the WHERE part of the queries
474
     * It interacts with the MATCH() and of course isn't usable stand-alone
475
     * Used by: SELECT, DELETE, UPDATE
476
     *
477
     * @return string The compiled WHERE
478
     */
479
    public function compileWhere()
480
    {
481
        $query = '';
482
483
        if (empty($this->match) && !empty($this->where)) {
484
            $query .= 'WHERE ';
485
        }
486
487
        if (!empty($this->where)) {
488
            foreach ($this->where as $key => $where) {
489
                if ($key > 0 || !empty($this->match)) {
490
                    $query .= 'AND ';
491
                }
492
                $query .= $this->compileFilterCondition($where);
493
            }
494
        }
495
496
        return $query;
497
    }
498
499
    public function compileFilterCondition($filter)
500
    {
501
        $query = '';
502
503
        if (!empty($filter)) {
504
            if (strtoupper($filter['operator']) === 'BETWEEN') {
505
                $query .= $this->getConnection()->quoteIdentifier($filter['column']);
506
                $query .= ' BETWEEN ';
507
                $query .= $this->getConnection()->quote($filter['value'][0]).' AND '
508
                    .$this->getConnection()->quote($filter['value'][1]).' ';
509
            } else {
510
                // id can't be quoted!
511
                if ($filter['column'] === 'id') {
512
                    $query .= 'id ';
513
                } else {
514
                    $query .= $this->getConnection()->quoteIdentifier($filter['column']).' ';
515
                }
516
517
                if (in_array(strtoupper($filter['operator']), array('IN', 'NOT IN'), true)) {
518
                    $query .= strtoupper($filter['operator']).' ('.implode(', ', $this->getConnection()->quoteArr($filter['value'])).') ';
519
                } else {
520
                    $query .= $filter['operator'].' '.$this->getConnection()->quote($filter['value']).' ';
521
                }
522
            }
523
        }
524
525
        return $query;
526
    }
527
528
    /**
529
     * Compiles the statements for SELECT
530
     *
531
     * @return SphinxQL
532
     */
533
    public function compileSelect()
534
    {
535
        $query = '';
536
537
        if ($this->type == 'select') {
538
            $query .= 'SELECT ';
539
540 View Code Duplication
            if (!empty($this->select)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
541
                $query .= implode(', ', $this->getConnection()->quoteIdentifierArr($this->select)).' ';
542
            } else {
543
                $query .= '* ';
544
            }
545
        }
546
547
        if (!empty($this->from)) {
548
            if ($this->from instanceof \Closure) {
549
                $sub = new static($this->getConnection());
550
                call_user_func($this->from, $sub);
551
                $query .= 'FROM ('.$sub->compile()->getCompiled().') ';
552
            } elseif ($this->from instanceof SphinxQL) {
553
                $query .= 'FROM ('.$this->from->compile()->getCompiled().') ';
554
            } else {
555
                $query .= 'FROM '.implode(', ', $this->getConnection()->quoteIdentifierArr($this->from)).' ';
556
            }
557
        }
558
559
        $query .= $this->compileMatch().$this->compileWhere();
560
561 View Code Duplication
        if (!empty($this->group_by)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
562
            $query .= 'GROUP BY '.implode(', ', $this->getConnection()->quoteIdentifierArr($this->group_by)).' ';
563
        }
564
565 View Code Duplication
        if (!empty($this->within_group_order_by)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
566
            $query .= 'WITHIN GROUP ORDER BY ';
567
568
            $order_arr = array();
569
570
            foreach ($this->within_group_order_by as $order) {
571
                $order_sub = $this->getConnection()->quoteIdentifier($order['column']).' ';
572
573
                if ($order['direction'] !== null) {
574
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
575
                }
576
577
                $order_arr[] = $order_sub;
578
            }
579
580
            $query .= implode(', ', $order_arr).' ';
581
        }
582
583
        if (!empty($this->having)) {
584
            $query .= 'HAVING '.$this->compileFilterCondition($this->having);
585
        }
586
587 View Code Duplication
        if (!empty($this->order_by)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
588
            $query .= 'ORDER BY ';
589
590
            $order_arr = array();
591
592
            foreach ($this->order_by as $order) {
593
                $order_sub = $this->getConnection()->quoteIdentifier($order['column']).' ';
594
595
                if ($order['direction'] !== null) {
596
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
597
                }
598
599
                $order_arr[] = $order_sub;
600
            }
601
602
            $query .= implode(', ', $order_arr).' ';
603
        }
604
605 View Code Duplication
        if ($this->limit !== null || $this->offset !== null) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
606
            if ($this->offset === null) {
607
                $this->offset = 0;
608
            }
609
610
            if ($this->limit === null) {
611
                $this->limit = 9999999999999;
612
            }
613
614
            $query .= 'LIMIT '.((int) $this->offset).', '.((int) $this->limit).' ';
615
        }
616
617
        if (!empty($this->options)) {
618
            $options = array();
619
620
            foreach ($this->options as $option) {
621
                if ($option['value'] instanceof Expression) {
622
                    $option['value'] = $option['value']->value();
623
                } elseif (is_array($option['value'])) {
624
                    array_walk(
625
                        $option['value'],
626
                        function (&$val, $key) {
627
                            $val = $key.'='.$val;
628
                        }
629
                    );
630
                    $option['value'] = '('.implode(', ', $option['value']).')';
631
                } else {
632
                    $option['value'] = $this->getConnection()->quote($option['value']);
633
                }
634
635
                $options[] = $this->getConnection()->quoteIdentifier($option['name'])
636
                    .' = '.$option['value'];
637
            }
638
639
            $query .= 'OPTION '.implode(', ', $options).' ';
640
        }
641
642
        if (!empty($this->facets)) {
643
            $facets = array();
644
645
            foreach ($this->facets as $facet) {
646
                // dynamically set the own SphinxQL connection if the Facet doesn't own one
647
                if ($facet->getConnection() === null) {
648
                    $facet->setConnection($this->getConnection());
649
                    $facets[] = $facet->getFacet();
650
                    // go back to the status quo for reuse
651
                    $facet->setConnection();
652
                } else {
653
                    $facets[] = $facet->getFacet();
654
                }
655
            }
656
657
            $query .= implode(' ', $facets);
658
        }
659
660
        $query = trim($query);
661
        $this->last_compiled = $query;
662
663
        return $this;
664
    }
665
666
    /**
667
     * Compiles the statements for INSERT or REPLACE
668
     *
669
     * @return SphinxQL
670
     */
671
    public function compileInsert()
672
    {
673
        if ($this->type == 'insert') {
674
            $query = 'INSERT ';
675
        } else {
676
            $query = 'REPLACE ';
677
        }
678
679
        if ($this->into !== null) {
680
            $query .= 'INTO '.$this->into.' ';
681
        }
682
683 View Code Duplication
        if (!empty($this->columns)) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
684
            $query .= '('.implode(', ', $this->getConnection()->quoteIdentifierArr($this->columns)).') ';
685
        }
686
687
        if (!empty($this->values)) {
688
            $query .= 'VALUES ';
689
            $query_sub = '';
690
691
            foreach ($this->values as $value) {
692
                $query_sub[] = '('.implode(', ', $this->getConnection()->quoteArr($value)).')';
693
            }
694
695
            $query .= implode(', ', $query_sub);
696
        }
697
698
        $query = trim($query);
699
        $this->last_compiled = $query;
700
701
        return $this;
702
    }
703
704
    /**
705
     * Compiles the statements for UPDATE
706
     *
707
     * @return SphinxQL
708
     */
709
    public function compileUpdate()
710
    {
711
        $query = 'UPDATE ';
712
713
        if ($this->into !== null) {
714
            $query .= $this->into.' ';
715
        }
716
717
        if (!empty($this->set)) {
718
            $query .= 'SET ';
719
720
            $query_sub = array();
721
722
            foreach ($this->set as $column => $value) {
723
                // MVA support
724
                if (is_array($value)) {
725
                    $query_sub[] = $this->getConnection()->quoteIdentifier($column)
726
                        .' = ('.implode(', ', $this->getConnection()->quoteArr($value)).')';
727
                } else {
728
                    $query_sub[] = $this->getConnection()->quoteIdentifier($column)
729
                        .' = '.$this->getConnection()->quote($value);
730
                }
731
            }
732
733
            $query .= implode(', ', $query_sub).' ';
734
        }
735
736
        $query .= $this->compileMatch().$this->compileWhere();
737
738
        $query = trim($query);
739
        $this->last_compiled = $query;
740
741
        return $this;
742
    }
743
744
    /**
745
     * Compiles the statements for DELETE
746
     *
747
     * @return SphinxQL
748
     */
749
    public function compileDelete()
750
    {
751
        $query = 'DELETE ';
752
753
        if (!empty($this->from)) {
754
            $query .= 'FROM '.$this->from[0].' ';
755
        }
756
757
        if (!empty($this->where)) {
758
            $query .= $this->compileWhere();
759
        }
760
761
        $query = trim($query);
762
        $this->last_compiled = $query;
763
764
        return $this;
765
    }
766
767
    /**
768
     * Sets a query to be executed
769
     *
770
     * @param string $sql A SphinxQL query to execute
771
     *
772
     * @return SphinxQL
773
     */
774
    public function query($sql)
775
    {
776
        $this->type = 'query';
777
        $this->query = $sql;
778
779
        return $this;
780
    }
781
782
    /**
783
     * Select the columns
784
     *
785
     * Gets the arguments passed as $sphinxql->select('one', 'two')
786
     * Using it without arguments equals to having '*' as argument
787
     * Using it with array maps values as column names
788
     *
789
     * Examples:
790
     *    $query->select('title');
791
     *    // SELECT title
792
     *
793
     *    $query->select('title', 'author', 'date');
794
     *    // SELECT title, author, date
795
     *
796
     *    $query->select(['id', 'title']);
797
     *    // SELECT id, title
798
     *
799
     * @param array|string $columns Array or multiple string arguments containing column names
800
     *
801
     * @return SphinxQL
802
     */
803
    public function select($columns = null)
804
    {
805
        $this->reset();
806
        $this->type = 'select';
807
808
        if (is_array($columns)) {
809
            $this->select = $columns;
810
        } else {
811
            $this->select = \func_get_args();
812
        }
813
814
        return $this;
815
    }
816
817
    /**
818
     * Activates the INSERT mode
819
     *
820
     * @return SphinxQL
821
     */
822
    public function insert()
823
    {
824
        $this->reset();
825
        $this->type = 'insert';
826
827
        return $this;
828
    }
829
830
    /**
831
     * Activates the REPLACE mode
832
     *
833
     * @return SphinxQL
834
     */
835
    public function replace()
836
    {
837
        $this->reset();
838
        $this->type = 'replace';
839
840
        return $this;
841
    }
842
843
    /**
844
     * Activates the UPDATE mode
845
     *
846
     * @param string $index The index to update into
847
     *
848
     * @return SphinxQL
849
     */
850
    public function update($index)
851
    {
852
        $this->reset();
853
        $this->type = 'update';
854
        $this->into($index);
855
856
        return $this;
857
    }
858
859
    /**
860
     * Activates the DELETE mode
861
     *
862
     * @return SphinxQL
863
     */
864
    public function delete()
865
    {
866
        $this->reset();
867
        $this->type = 'delete';
868
869
        return $this;
870
    }
871
872
    /**
873
     * FROM clause (Sphinx-specific since it works with multiple indexes)
874
     * func_get_args()-enabled
875
     *
876
     * @param array $array An array of indexes to use
877
     *
878
     * @return SphinxQL
879
     */
880
    public function from($array = null)
881
    {
882
        if (is_string($array)) {
883
            $this->from = \func_get_args();
884
        }
885
886
        if (is_array($array) || $array instanceof \Closure || $array instanceof SphinxQL) {
887
            $this->from = $array;
888
        }
889
890
        return $this;
891
    }
892
893
    /**
894
     * MATCH clause (Sphinx-specific)
895
     *
896
     * @param mixed    $column The column name (can be array, string, Closure, or Match)
897
     * @param string   $value  The value
898
     * @param boolean  $half  Exclude ", |, - control characters from being escaped
899
     *
900
     * @return SphinxQL
901
     */
902
    public function match($column, $value = null, $half = false)
903
    {
904
        if ($column === '*' || (is_array($column) && in_array('*', $column))) {
905
            $column = array();
906
        }
907
908
        $this->match[] = array('column' => $column, 'value' => $value, 'half' => $half);
909
910
        return $this;
911
    }
912
913
    /**
914
     * WHERE clause
915
     *
916
     * Examples:
917
     *    $query->where('column', 'value');
918
     *    // WHERE column = 'value'
919
     *
920
     *    $query->where('column', '=', 'value');
921
     *    // WHERE column = 'value'
922
     *
923
     *    $query->where('column', '>=', 'value')
924
     *    // WHERE column >= 'value'
925
     *
926
     *    $query->where('column', 'IN', array('value1', 'value2', 'value3'));
927
     *    // WHERE column IN ('value1', 'value2', 'value3')
928
     *
929
     *    $query->where('column', 'BETWEEN', array('value1', 'value2'))
930
     *    // WHERE column BETWEEN 'value1' AND 'value2'
931
     *    // WHERE example BETWEEN 10 AND 100
932
     *
933
     * @param string   $column   The column name
934
     * @param string   $operator The operator to use
935
     * @param string   $value    The value to check against
936
     *
937
     * @return SphinxQL
938
     */
939 View Code Duplication
    public function where($column, $operator, $value = null)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
940
    {
941
        if ($value === null) {
942
            $value = $operator;
943
            $operator = '=';
944
        }
945
946
        $this->where[] = array(
947
            'column' => $column,
948
            'operator' => $operator,
949
            'value' => $value
950
        );
951
952
        return $this;
953
    }
954
955
    /**
956
     * GROUP BY clause
957
     * Adds to the previously added columns
958
     *
959
     * @param string $column A column to group by
960
     *
961
     * @return SphinxQL
962
     */
963
    public function groupBy($column)
964
    {
965
        $this->group_by[] = $column;
966
967
        return $this;
968
    }
969
970
    /**
971
     * WITHIN GROUP ORDER BY clause (SphinxQL-specific)
972
     * Adds to the previously added columns
973
     * Works just like a classic ORDER BY
974
     *
975
     * @param string $column    The column to group by
976
     * @param string $direction The group by direction (asc/desc)
977
     *
978
     * @return SphinxQL
979
     */
980
    public function withinGroupOrderBy($column, $direction = null)
981
    {
982
        $this->within_group_order_by[] = array('column' => $column, 'direction' => $direction);
983
984
        return $this;
985
    }
986
987
    /**
988
     * HAVING clause
989
     *
990
     * Examples:
991
     *    $sq->having('column', 'value');
992
     *    // HAVING column = 'value'
993
     *
994
     *    $sq->having('column', '=', 'value');
995
     *    // HAVING column = 'value'
996
     *
997
     *    $sq->having('column', '>=', 'value')
998
     *    // HAVING column >= 'value'
999
     *
1000
     *    $sq->having('column', 'IN', array('value1', 'value2', 'value3'));
1001
     *    // HAVING column IN ('value1', 'value2', 'value3')
1002
     *
1003
     *    $sq->having('column', 'BETWEEN', array('value1', 'value2'))
1004
     *    // HAVING column BETWEEN 'value1' AND 'value2'
1005
     *    // HAVING example BETWEEN 10 AND 100
1006
     *
1007
     * @param string   $column   The column name
1008
     * @param string   $operator The operator to use
1009
     * @param string   $value    The value to check against
1010
     *
1011
     * @return SphinxQL The current object
1012
     */
1013 View Code Duplication
    public function having($column, $operator, $value = null)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1014
    {
1015
        if ($value === null) {
1016
            $value = $operator;
1017
            $operator = '=';
1018
        }
1019
1020
        $this->having = array(
1021
            'column' => $column,
1022
            'operator' => $operator,
1023
            'value' => $value
1024
        );
1025
1026
        return $this;
1027
    }
1028
1029
    /**
1030
     * ORDER BY clause
1031
     * Adds to the previously added columns
1032
     *
1033
     * @param string $column    The column to order on
1034
     * @param string $direction The ordering direction (asc/desc)
1035
     *
1036
     * @return SphinxQL
1037
     */
1038
    public function orderBy($column, $direction = null)
1039
    {
1040
        $this->order_by[] = array('column' => $column, 'direction' => $direction);
1041
1042
        return $this;
1043
    }
1044
1045
    /**
1046
     * LIMIT clause
1047
     * Supports also LIMIT offset, limit
1048
     *
1049
     * @param int      $offset Offset if $limit is specified, else limit
1050
     * @param null|int $limit  The limit to set, null for no limit
1051
     *
1052
     * @return SphinxQL
1053
     */
1054 View Code Duplication
    public function limit($offset, $limit = null)
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1055
    {
1056
        if ($limit === null) {
1057
            $this->limit = (int) $offset;
1058
            return $this;
1059
        }
1060
1061
        $this->offset($offset);
1062
        $this->limit = (int) $limit;
1063
1064
        return $this;
1065
    }
1066
1067
    /**
1068
     * OFFSET clause
1069
     *
1070
     * @param int $offset The offset
1071
     *
1072
     * @return SphinxQL
1073
     */
1074
    public function offset($offset)
1075
    {
1076
        $this->offset = (int) $offset;
1077
1078
        return $this;
1079
    }
1080
1081
    /**
1082
     * OPTION clause (SphinxQL-specific)
1083
     * Used by: SELECT
1084
     *
1085
     * @param string $name  Option name
1086
     * @param string $value Option value
1087
     *
1088
     * @return SphinxQL
1089
     */
1090
    public function option($name, $value)
1091
    {
1092
        $this->options[] = array('name' => $name, 'value' => $value);
1093
1094
        return $this;
1095
    }
1096
1097
    /**
1098
     * INTO clause
1099
     * Used by: INSERT, REPLACE
1100
     *
1101
     * @param string $index The index to insert/replace into
1102
     *
1103
     * @return SphinxQL
1104
     */
1105
    public function into($index)
1106
    {
1107
        $this->into = $index;
1108
1109
        return $this;
1110
    }
1111
1112
    /**
1113
     * Set columns
1114
     * Used in: INSERT, REPLACE
1115
     * func_get_args()-enabled
1116
     *
1117
     * @param array $array The array of columns
1118
     *
1119
     * @return SphinxQL
1120
     */
1121
    public function columns($array = array())
1122
    {
1123
        if (is_array($array)) {
1124
            $this->columns = $array;
1125
        } else {
1126
            $this->columns = \func_get_args();
1127
        }
1128
1129
        return $this;
1130
    }
1131
1132
    /**
1133
     * Set VALUES
1134
     * Used in: INSERT, REPLACE
1135
     * func_get_args()-enabled
1136
     *
1137
     * @param array $array The array of values matching the columns from $this->columns()
1138
     *
1139
     * @return SphinxQL
1140
     */
1141
    public function values($array)
1142
    {
1143
        if (is_array($array)) {
1144
            $this->values[] = $array;
1145
        } else {
1146
            $this->values[] = \func_get_args();
1147
        }
1148
1149
        return $this;
1150
    }
1151
1152
    /**
1153
     * Set column and relative value
1154
     * Used in: INSERT, REPLACE
1155
     *
1156
     * @param string $column The column name
1157
     * @param string $value  The value
1158
     *
1159
     * @return SphinxQL
1160
     */
1161
    public function value($column, $value)
1162
    {
1163
        if ($this->type === 'insert' || $this->type === 'replace') {
1164
            $this->columns[] = $column;
1165
            $this->values[0][] = $value;
1166
        } else {
1167
            $this->set[$column] = $value;
1168
        }
1169
1170
        return $this;
1171
    }
1172
1173
    /**
1174
     * Allows passing an array with the key as column and value as value
1175
     * Used in: INSERT, REPLACE, UPDATE
1176
     *
1177
     * @param array $array Array of key-values
1178
     *
1179
     * @return SphinxQL
1180
     */
1181
    public function set($array)
1182
    {
1183
        foreach ($array as $key => $item) {
1184
            $this->value($key, $item);
1185
        }
1186
1187
        return $this;
1188
    }
1189
1190
    /**
1191
     * Allows passing an array with the key as column and value as value
1192
     * Used in: INSERT, REPLACE, UPDATE
1193
     *
1194
     * @param Facet $facet
1195
     * @return SphinxQL
1196
     */
1197
    public function facet($facet)
1198
    {
1199
        $this->facets[] = $facet;
1200
1201
        return $this;
1202
    }
1203
1204
    /**
1205
     * Sets the characters used for escapeMatch().
1206
     *
1207
     * @param array $array The array of characters to escape
1208
     *
1209
     * @return SphinxQL The escaped characters
1210
     */
1211
    public function setFullEscapeChars($array = array())
1212
    {
1213
        if (!empty($array)) {
1214
            $this->escape_full_chars = $this->compileEscapeChars($array);
1215
        }
1216
1217
        return $this;
1218
    }
1219
1220
    /**
1221
     * Sets the characters used for halfEscapeMatch().
1222
     *
1223
     * @param array $array The array of characters to escape
1224
     *
1225
     * @return SphinxQL The escaped characters
1226
     */
1227
    public function setHalfEscapeChars($array = array())
1228
    {
1229
        if (!empty($array)) {
1230
            $this->escape_half_chars = $this->compileEscapeChars($array);
1231
        }
1232
1233
        return $this;
1234
    }
1235
1236
    /**
1237
     * Compiles an array containing the characters and escaped characters into a key/value configuration.
1238
     *
1239
     * @param array $array The array of characters to escape
1240
     *
1241
     * @return array An array of the characters and it's escaped counterpart
1242
     */
1243
    public function compileEscapeChars($array = array())
1244
    {
1245
        $result = array();
1246
        foreach ($array as $character) {
1247
            $result[$character] = '\\'.$character;
1248
        }
1249
1250
        return $result;
1251
    }
1252
1253
    /**
1254
     * Escapes the query for the MATCH() function
1255
     *
1256
     * @param string $string The string to escape for the MATCH
1257
     *
1258
     * @return string The escaped string
1259
     */
1260
    public function escapeMatch($string)
1261
    {
1262
        if ($string instanceof Expression) {
1263
            return $string->value();
1264
        }
1265
1266
        return mb_strtolower(str_replace(array_keys($this->escape_full_chars), array_values($this->escape_full_chars), $string), 'utf8');
1267
    }
1268
1269
    /**
1270
     * Escapes the query for the MATCH() function
1271
     * Allows some of the control characters to pass through for use with a search field: -, |, "
1272
     * It also does some tricks to wrap/unwrap within " the string and prevents errors
1273
     *
1274
     * @param string $string The string to escape for the MATCH
1275
     *
1276
     * @return string The escaped string
1277
     */
1278
    public function halfEscapeMatch($string)
1279
    {
1280
        if ($string instanceof Expression) {
1281
            return $string->value();
1282
        }
1283
1284
        $string = str_replace(array_keys($this->escape_half_chars), array_values($this->escape_half_chars), $string);
1285
1286
        // this manages to lower the error rate by a lot
1287
        if (mb_substr_count($string, '"', 'utf8') % 2 !== 0) {
1288
            $string .= '"';
1289
        }
1290
1291
        $string = preg_replace('/-[\s-]*-/u', '-', $string);
1292
1293
        $from_to_preg = array(
1294
            '/([-|])\s*$/u'        => '\\\\\1',
1295
            '/\|[\s|]*\|/u'        => '|',
1296
            '/(\S+)-(\S+)/u'       => '\1\-\2',
1297
            '/(\S+)\s+-\s+(\S+)/u' => '\1 \- \2',
1298
        );
1299
1300
        $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string), 'utf8');
1301
1302
        return $string;
1303
    }
1304
1305
    /**
1306
     * Clears the existing query build for new query when using the same SphinxQL instance.
1307
     *
1308
     * @return SphinxQL
1309
     */
1310
    public function reset()
1311
    {
1312
        $this->query = null;
1313
        $this->select = array();
1314
        $this->from = array();
1315
        $this->where = array();
1316
        $this->match = array();
1317
        $this->group_by = array();
1318
        $this->within_group_order_by = array();
1319
        $this->having = array();
1320
        $this->order_by = array();
1321
        $this->offset = null;
1322
        $this->limit = null;
1323
        $this->into = null;
1324
        $this->columns = array();
1325
        $this->values = array();
1326
        $this->set = array();
1327
        $this->options = array();
1328
1329
        return $this;
1330
    }
1331
1332
    public function resetWhere()
1333
    {
1334
        $this->where = array();
1335
1336
        return $this;
1337
    }
1338
1339
    public function resetMatch()
1340
    {
1341
        $this->match = array();
1342
1343
        return $this;
1344
    }
1345
1346
    public function resetGroupBy()
1347
    {
1348
        $this->group_by = array();
1349
1350
        return $this;
1351
    }
1352
1353
    public function resetWithinGroupOrderBy()
1354
    {
1355
        $this->within_group_order_by = array();
1356
1357
        return $this;
1358
    }
1359
1360
    public function resetHaving()
1361
    {
1362
        $this->having = array();
1363
1364
        return $this;
1365
    }
1366
1367
    public function resetOrderBy()
1368
    {
1369
        $this->order_by = array();
1370
1371
        return $this;
1372
    }
1373
1374
    public function resetOptions()
1375
    {
1376
        $this->options = array();
1377
1378
        return $this;
1379
    }
1380
    
1381
    /**
1382
     * @param array $select
1383
     */
1384
    public function setSelect(array $select)
1385
    {
1386
        $this->select = $select;
1387
    }
1388
}
1389