Completed
Push — master ( d8547b...84c8ba )
by Michal
03:40
created

BufferedQuery   C

Complexity

Total Complexity 69

Size/Duplication

Total Lines 390
Duplicated Lines 3.33 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 10
Bugs 6 Features 0
Metric Value
wmc 69
c 10
b 6
f 0
lcom 1
cbo 1
dl 13
loc 390
ccs 143
cts 143
cp 1
rs 5.6445

3 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 34 1
A setDelimiter() 0 5 1
D extract() 13 267 67

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 BufferedQuery 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 BufferedQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Buffered query utilities.
5
 *
6
 * @package    SqlParser
7
 * @subpackage Utils
8
 */
9
namespace SqlParser\Utils;
10
11
use SqlParser\Context;
12
13
/**
14
 * Buffer query utilities.
15
 *
16
 * Implements a specialized lexer used to extract statements from large inputs
17
 * that are being buffered. After each statement has been extracted, a lexer or
18
 * a parser may be used.
19
 *
20
 * All comments are skipped, with one exception: MySQL commands inside `/*!`.
21
 *
22
 * @category   Lexer
23
 * @package    SqlParser
24
 * @subpackage Utils
25
 * @author     Dan Ungureanu <[email protected]>
26
 * @license    http://opensource.org/licenses/GPL-2.0 GNU Public License
27
 */
28
class BufferedQuery
29
{
30
31
    // Constants that describe the current status of the parser.
32
33
    // A string is being parsed.
34
    const STATUS_STRING                 = 16; // 0001 0000
35
    const STATUS_STRING_SINGLE_QUOTES   = 17; // 0001 0001
36
    const STATUS_STRING_DOUBLE_QUOTES   = 18; // 0001 0010
37
    const STATUS_STRING_BACKTICK        = 20; // 0001 0100
38
39
    // A comment is being parsed.
40
    const STATUS_COMMENT                = 32; // 0010 0000
41
    const STATUS_COMMENT_BASH           = 33; // 0010 0001
42
    const STATUS_COMMENT_C              = 34; // 0010 0010
43
    const STATUS_COMMENT_SQL            = 36; // 0010 0100
44
45
    /**
46
     * The query that is being processed.
47
     *
48
     * This field can be modified just by appending to it!
49
     *
50
     * @var string
51
     */
52
    public $query = '';
53
54
    /**
55
     * The options of this parser.
56
     *
57
     * @var array
58
     */
59
    public $options = array();
60
61
    /**
62
     * The last delimiter used.
63
     *
64
     * @var string
65
     */
66
    public $delimiter;
67
68
    /**
69
     * The length of the delimiter.
70
     *
71
     * @var int
72
     */
73
    public $delimiterLen;
74
75
    /**
76
     * The current status of the parser.
77
     *
78
     * @var int
79
     */
80
    public $status;
81
82
    /**
83
     * The last incomplete query that was extracted.
84
     *
85
     * @var string
86
     */
87
    public $current = '';
88
89
    /**
90
     * Constructor.
91
     *
92
     * @param string $query   The query to be parsed.
93
     * @param array  $options The options of this parser.
94
     */
95 7
    public function __construct($query = '', array $options = array())
96
    {
97
        // Merges specified options with defaults.
98 7
        $this->options = array_merge(
99
            array(
100
101
                /**
102
                 * The starting delimiter.
103
                 *
104
                 * @var string
105
                 */
106 7
                'delimiter' => ';',
107
108
                /**
109
                 * Whether `DELIMITER` statements should be parsed.
110
                 *
111
                 * @var bool
112
                 */
113 7
                'parse_delimiter' => false,
114
115
                /**
116
                 * Whether a delimiter should be added at the end of the
117
                 * statement.
118
                 *
119
                 * @var bool
120
                 */
121 7
                'add_delimiter' => false,
122 7
            ),
123
            $options
124 7
        );
125
126 7
        $this->query = $query;
127 7
        $this->setDelimiter($this->options['delimiter']);
128 7
    }
129
130
    /**
131
     * Sets the delimiter.
132
     *
133
     * Used to update the length of it too.
134
     *
135
     * @param string $delimiter
136
     */
137 7
    public function setDelimiter($delimiter)
138
    {
139 7
        $this->delimiter = $delimiter;
140 7
        $this->delimiterLen = strlen($delimiter);
141 7
    }
142
143
    /**
144
     * Extracts a statement from the buffer.
145
     *
146
     * @param bool $end Whether the end of the buffer was reached.
147
     *
148
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
149
     */
150 7
    public function extract($end = false)
151
    {
152
        /**
153
         * The last parsed position.
154
         *
155
         * This is statically defined because it is not used outside anywhere
156
         * outside this method and there is probably a (minor) performance
157
         * improvement to it.
158
         *
159
         * @var int
160
         */
161 7
        static $i = 0;
162
163 7
        if (empty($this->query)) {
164 7
            return false;
165
        }
166
167
        /**
168
         * The length of the buffer.
169
         *
170
         * @var int $len
171
         */
172 7
        $len = strlen($this->query);
173
174
        /**
175
         * The last index of the string that is going to be parsed.
176
         *
177
         * There must be a few characters left in the buffer so the parser can
178
         * avoid confusing some symbols that may have multiple meanings.
179
         *
180
         * For example, if the buffer ends in `-` that may be an operator or the
181
         * beginning of a comment.
182
         *
183
         * Another example if the buffer ends in `DELIMITE`. The parser is going
184
         * to require a few more characters because that may be a part of the
185
         * `DELIMITER` keyword or just a column named `DELIMITE`.
186
         *
187
         * Those extra characters are required only if there is more data
188
         * expected (the end of the buffer was not reached).
189
         *
190
         * @var int $loopLen
191
         */
192 7
        $loopLen = $end ? $len : $len - 16;
193
194 7
        for (; $i < $loopLen; ++$i) {
195
            /**
196
             * Handling backslash.
197
             *
198
             * Even if the next character is a special character that should be
199
             * treated differently, because of the preceding backslash, it will
200
             * be ignored.
201
             */
202 7
            if ((($this->status & static::STATUS_COMMENT) == 0) && ($this->query[$i] === '\\')) {
203 3
                $this->current .= $this->query[$i] . $this->query[++$i];
204 3
                continue;
205
            }
206
207
            /*
208
             * Handling special parses statuses.
209
             */
210 7
            if ($this->status === static::STATUS_STRING_SINGLE_QUOTES) {
211
                // Single-quoted strings like 'foo'.
212 5
                if ($this->query[$i] === '\'') {
213 5
                    $this->status = 0;
214 5
                }
215 5
                $this->current .= $this->query[$i];
216 5
                continue;
217 7 View Code Duplication
            } elseif ($this->status === static::STATUS_STRING_DOUBLE_QUOTES) {
218
                // Double-quoted strings like "bar".
219 4
                if ($this->query[$i] === '"') {
220 4
                    $this->status = 0;
221 4
                }
222 4
                $this->current .= $this->query[$i];
223 4
                continue;
224 7
            } elseif ($this->status === static::STATUS_STRING_BACKTICK) {
225 4
                if ($this->query[$i] === '`') {
226 4
                    $this->status = 0;
227 4
                }
228 4
                $this->current .= $this->query[$i];
229 4
                continue;
230 7
            } elseif (($this->status === static::STATUS_COMMENT_BASH)
231 7
                || ($this->status === static::STATUS_COMMENT_SQL)
232 7
            ) {
233
                // Bash-like (#) or SQL-like (-- ) comments end in new line.
234 3
                if ($this->query[$i] === "\n") {
235 3
                    $this->status = 0;
236 3
                }
237 3
                continue;
238 7
            } elseif ($this->status === static::STATUS_COMMENT_C) {
239
                // C-like comments end in */.
240 3
                if (($this->query[$i - 1] === '*') && ($this->query[$i] === '/')) {
241 3
                    $this->status = 0;
242 3
                }
243 3
                continue;
244
            }
245
246
            /*
247
             * Checking if a string started.
248
             */
249 7
            if ($this->query[$i] === '\'') {
250 5
                $this->status = static::STATUS_STRING_SINGLE_QUOTES;
251 5
                $this->current .= $this->query[$i];
252 5
                continue;
253 7 View Code Duplication
            } elseif ($this->query[$i] === '"') {
254 4
                $this->status = static::STATUS_STRING_DOUBLE_QUOTES;
255 4
                $this->current .= $this->query[$i];
256 4
                continue;
257 7
            } elseif ($this->query[$i] === '`') {
258 4
                $this->status = static::STATUS_STRING_BACKTICK;
259 4
                $this->current .= $this->query[$i];
260 4
                continue;
261
            }
262
263
            /*
264
             * Checking if a comment started.
265
             */
266 7
            if ($this->query[$i] === '#') {
267 3
                $this->status = static::STATUS_COMMENT_BASH;
268 3
                continue;
269 7
            } elseif (($i + 2 < $len)
270 7
                && ($this->query[$i] === '-')
271 7
                && ($this->query[$i + 1] === '-')
272 7
                && (Context::isWhitespace($this->query[$i + 2]))
273 7
            ) {
274 3
                $this->status = static::STATUS_COMMENT_SQL;
275 3
                continue;
276 7
            } elseif (($i + 2 < $len)
277 7
                && ($this->query[$i] === '/')
278 7
                && ($this->query[$i + 1] === '*')
279 7
                && ($this->query[$i + 2] !== '!')
280 7
            ) {
281 3
                $this->status = static::STATUS_COMMENT_C;
282 3
                continue;
283
            }
284
285
            /*
286
             * Handling `DELIMITER` statement.
287
             *
288
             * The code below basically checks for
289
             *     `strtoupper(substr($this->query, $i, 9)) === 'DELIMITER'`
290
             *
291
             * This optimization makes the code about 3 times faster.
292
             *
293
             * `DELIMITER` is not being considered a keyword. The only context
294
             * it has a special meaning is when it is the beginning of a
295
             * statement. This is the reason for the last condition.
296
             */
297 7
            if (($i + 9 < $len)
298 7
                && (($this->query[$i    ] === 'D') || ($this->query[$i    ] === 'd'))
299 7
                && (($this->query[$i + 1] === 'E') || ($this->query[$i + 1] === 'e'))
300 7
                && (($this->query[$i + 2] === 'L') || ($this->query[$i + 2] === 'l'))
301 7
                && (($this->query[$i + 3] === 'I') || ($this->query[$i + 3] === 'i'))
302 7
                && (($this->query[$i + 4] === 'M') || ($this->query[$i + 4] === 'm'))
303 7
                && (($this->query[$i + 5] === 'I') || ($this->query[$i + 5] === 'i'))
304 7
                && (($this->query[$i + 6] === 'T') || ($this->query[$i + 6] === 't'))
305 7
                && (($this->query[$i + 7] === 'E') || ($this->query[$i + 7] === 'e'))
306 7
                && (($this->query[$i + 8] === 'R') || ($this->query[$i + 8] === 'r'))
307 7
                && (Context::isWhitespace($this->query[$i + 9]))
308 7
                && (trim($this->current) === '')
309 7
            ) {
310
                // Saving the current index to be able to revert any parsing
311
                // done in this block.
312 4
                $iBak = $i;
313 4
                $i += 9; // Skipping `DELIMITER`.
314
315
                // Skipping whitespaces.
316 4
                while (($i < $len) && (Context::isWhitespace($this->query[$i]))) {
317 4
                    ++$i;
318 4
                }
319
320
                // Parsing the delimiter.
321 4
                $delimiter = '';
322 4
                while (($i < $len) && (!Context::isWhitespace($this->query[$i]))) {
323 4
                    $delimiter .= $this->query[$i++];
324 4
                }
325
326
                // Checking if the delimiter definition ended.
327 4
                if (($delimiter != '')
328 4
                    && ((($i < $len) && (Context::isWhitespace($this->query[$i])))
329 1
                    || (($i === $len) && ($end)))
330 4
                ) {
331
                    // Saving the delimiter.
332 4
                    $this->setDelimiter($delimiter);
333
334
                    // Whether this statement should be returned or not.
335 4
                    $ret = '';
336 4
                    if (!empty($this->options['parse_delimiter'])) {
337
                        // Appending the `DELIMITER` statement that was just
338
                        // found to the current statement.
339 2
                        $ret = trim(
340 2
                            $this->current . ' ' . substr($this->query, $iBak, $i - $iBak)
341 2
                        );
342 2
                    }
343
344
                    // Removing the statement that was just extracted from the
345
                    // query.
346 4
                    $this->query = substr($this->query, $i);
347 4
                    $i = 0;
348
349
                    // Resetting the current statement.
350 4
                    $this->current = '';
351
352 4
                    return $ret;
353
                }
354
355
                // Incomplete statement. Reverting
356 1
                $i = $iBak;
357 1
                return false;
358
            }
359
360
            /*
361
             * Checking if the current statement finished.
362
             *
363
             * The first letter of the delimiter is being checked as an
364
             * optimization. This code is almost as fast as the one above.
365
             *
366
             * There is no point in checking if two strings match if not even
367
             * the first letter matches.
368
             */
369 7
            if (($this->query[$i] === $this->delimiter[0])
370 7
                && (($this->delimiterLen === 1)
371 4
                || (substr($this->query, $i, $this->delimiterLen) === $this->delimiter))
372 7
            ) {
373
                // Saving the statement that just ended.
374 7
                $ret = $this->current;
375
376
                // If needed, adds a delimiter at the end of the statement.
377 7
                if (!empty($this->options['add_delimiter'])) {
378 5
                    $ret .= $this->delimiter;
379 5
                }
380
381
                // Removing the statement that was just extracted from the
382
                // query.
383 7
                $this->query = substr($this->query, $i + $this->delimiterLen);
384 7
                $i = 0;
385
386
                // Resetting the current statement.
387 7
                $this->current = '';
388
389
                // Returning the statement.
390 7
                return trim($ret);
391
            }
392
393
            /*
394
             * Appending current character to current statement.
395
             */
396 7
            $this->current .= $this->query[$i];
397 7
        }
398
399 7
        if (($end) && ($i === $len)) {
400
            // If the end of the buffer was reached, the buffer is emptied and
401
            // the current statement that was extracted is returned.
402 5
            $ret = $this->current;
403
404
            // Emptying the buffer.
405 5
            $this->query = '';
406 5
            $i = 0;
407
408
            // Resetting the current statement.
409 5
            $this->current = '';
410
411
            // Returning the statement.
412 5
            return trim($ret);
413
        }
414
415 7
        return '';
416
    }
417
}
418