Completed
Push — master ( 802733...f10121 )
by Dan
03:16
created

OptionsArray::build()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 5
Bugs 3 Features 2
Metric Value
c 5
b 3
f 2
dl 0
loc 18
ccs 13
cts 13
cp 1
rs 8.8571
cc 6
eloc 12
nc 7
nop 2
crap 6
1
<?php
2
3
/**
4
 * Parses a list of options.
5
 *
6
 * @package    SqlParser
7
 * @subpackage Components
8
 */
9
namespace SqlParser\Components;
10
11
use SqlParser\Component;
12
use SqlParser\Parser;
13
use SqlParser\Token;
14
use SqlParser\TokensList;
15
16
/**
17
 * Parses a list of options.
18
 *
19
 * @category   Components
20
 * @package    SqlParser
21
 * @subpackage Components
22
 * @author     Dan Ungureanu <[email protected]>
23
 * @license    http://opensource.org/licenses/GPL-2.0 GNU Public License
24
 */
25
class OptionsArray extends Component
26
{
27
28
    /**
29
     * ArrayObj of selected options.
30
     *
31
     * @var array
32
     */
33
    public $options = array();
34
35
    /**
36
     * Constructor.
37
     *
38
     * @param array $options The array of options. Options that have a value
39
     *                       must be an array with at least two keys `name` and
40
     *                       `expr` or `value`.
41
     */
42 138
    public function __construct(array $options = array())
43
    {
44 138
        $this->options = $options;
45 138
    }
46
47
    /**
48
     * @param Parser     $parser  The parser that serves as context.
49
     * @param TokensList $list    The list of tokens that are being parsed.
50
     * @param array      $options Parameters for parsing.
51
     *
52
     * @return OptionsArray
53
     */
54 133
    public static function parse(Parser $parser, TokensList $list, array $options = array())
55
    {
56 133
        $ret = new OptionsArray();
57
58
        /**
59
         * The ID that will be assigned to duplicate options.
60
         *
61
         * @var int $lastAssignedId
62
         */
63 133
        $lastAssignedId = count($options) + 1;
64
65
        /**
66
         * The option that was processed last time.
67
         *
68
         * @var array $lastOption
69
         */
70 133
        $lastOption = null;
71
72
        /**
73
         * The index of the option that was processed last time.
74
         *
75
         * @var int $lastOptionId
76
         */
77 133
        $lastOptionId = 0;
78
79
        /**
80
         * Counts brackets.
81
         *
82
         * @var int $brackets
83
         */
84 133
        $brackets = 0;
85
86
        /**
87
         * The state of the parser.
88
         *
89
         * Below are the states of the parser.
90
         *
91
         *      0 ---------------------[ option ]----------------------> 1
92
         *
93
         *      1 -------------------[ = (optional) ]------------------> 2
94
         *
95
         *      2 ----------------------[ value ]----------------------> 0
96
         *
97
         * @var int $state
98
         */
99 133
        $state = 0;
100
101 133
        for (; $list->idx < $list->count; ++$list->idx) {
102
            /**
103
             * Token parsed at this moment.
104
             *
105
             * @var Token $token
106
             */
107 133
            $token = $list->tokens[$list->idx];
108
109
            // End of statement.
110 133
            if ($token->type === Token::TYPE_DELIMITER) {
111 36
                break;
112
            }
113
114
            // Skipping comments.
115 130
            if ($token->type === Token::TYPE_COMMENT) {
116 3
                continue;
117
            }
118
119
            // Skipping whitespace if not parsing value.
120 130
            if (($token->type === Token::TYPE_WHITESPACE) && ($brackets === 0)) {
121 121
                continue;
122
            }
123
124 130
            if ($lastOption === null) {
125 130
                $upper = strtoupper($token->token);
126 130
                if (isset($options[$upper])) {
127 72
                    $lastOption = $options[$upper];
128 72
                    $lastOptionId = is_array($lastOption) ?
129 72
                        $lastOption[0] : $lastOption;
130 72
                    $state = 0;
131
132
                    // Checking for option conflicts.
133
                    // For example, in `SELECT` statements the keywords `ALL`
134
                    // and `DISTINCT` conflict and if used together, they
135
                    // produce an invalid query.
136
                    //
137
                    // Usually, tokens can be identified in the array by the
138
                    // option ID, but if conflicts occur, a generated option ID
139
                    // is used.
140
                    //
141
                    // The first pseudo duplicate ID is the maximum value of the
142
                    // real options (e.g.  if there are 5 options, the first
143
                    // fake ID is 6).
144 72
                    if (isset($ret->options[$lastOptionId])) {
145 2
                        $parser->error(
146 2
                            sprintf(
147 2
                                __('This option conflicts with "%1$s".'),
148 2
                                is_array($ret->options[$lastOptionId])
149 2
                                ? $ret->options[$lastOptionId]['name']
150 2
                                : $ret->options[$lastOptionId]
151 2
                            ),
152
                            $token
153 2
                        );
154 2
                        $lastOptionId = $lastAssignedId++;
155 2
                    }
156 72
                } else {
157
                    // There is no option to be processed.
158 121
                    break;
159
                }
160 72
            }
161
162 72
            if ($state === 0) {
163 72
                if (!is_array($lastOption)) {
164
                    // This is a just keyword option without any value.
165
                    // This is the beginning and the end of it.
166 67
                    $ret->options[$lastOptionId] = $token->value;
167 67
                    $lastOption = null;
168 67
                    $state = 0;
169 72
                } elseif (($lastOption[1] === 'var') || ($lastOption[1] === 'var=')) {
170
                    // This is a keyword that is followed by a value.
171
                    // This is only the beginning. The value is parsed in state
172
                    // 1 and 2. State 1 is used to skip the first equals sign
173
                    // and state 2 to parse the actual value.
174 25
                    $ret->options[$lastOptionId] = array(
175
                        // @var string The name of the option.
176 25
                        'name' => $token->value,
177
                        // @var bool Whether it contains an equal sign.
178
                        //           This is used by the builder to rebuild it.
179 25
                        'equals' => $lastOption[1] === 'var=',
180
                        // @var string Raw value.
181 25
                        'expr' => '',
182
                        // @var string Processed value.
183 25
                        'value' => '',
184
                    );
185 25
                    $state = 1;
186 28
                } elseif ($lastOption[1] === 'expr') {
187
                    // This is a keyword that is followed by an expression.
188
                    // The expression is used by the specialized parser.
189
190
                    // Skipping this option in order to parse the expression.
191 12
                    ++$list->idx;
192 12
                    $ret->options[$lastOptionId] = array(
193
                        // @var string The name of the option.
194 12
                        'name' => $token->value,
195
                        // @var Expression The parsed expression.
196 12
                        'expr' => null,
197
                    );
198 12
                    $state = 1;
199 12
                }
200 72
            } elseif ($state === 1) {
201 28
                $state = 2;
202 28
                if ($token->token === '=') {
203 19
                    $ret->options[$lastOptionId]['equals'] = true;
204 19
                    continue;
205
                }
206 19
            }
207
208
            // This is outside the `elseif` group above because the change might
209
            // change this iteration.
210 72
            if ($state === 2) {
211 28
                if ($lastOption[1] === 'expr') {
212 12
                    $ret->options[$lastOptionId]['expr'] = Expression::parse(
213 12
                        $parser,
214 12
                        $list,
215 12
                        empty($lastOption[2]) ? array() : $lastOption[2]
216 12
                    );
217 12
                    $ret->options[$lastOptionId]['value']
218 12
                        = $ret->options[$lastOptionId]['expr']->expr;
219 12
                    $lastOption = null;
220 12
                    $state = 0;
221 12
                } else {
222 25
                    if ($token->token === '(') {
223 2
                        ++$brackets;
224 25
                    } elseif ($token->token === ')') {
225 2
                        --$brackets;
226 2
                    }
227
228 25
                    $ret->options[$lastOptionId]['expr'] .= $token->token;
229
230 25
                    if (!((($token->token === '(') && ($brackets === 1))
231 25
                        || (($token->token === ')') && ($brackets === 0)))
232 25
                    ) {
233
                        // First pair of brackets is being skipped.
234 25
                        $ret->options[$lastOptionId]['value'] .= $token->value;
235 25
                    }
236
237
                    // Checking if we finished parsing.
238 25
                    if ($brackets === 0) {
239 25
                        $lastOption = null;
240 25
                    }
241
                }
242 28
            }
243 72
        }
244
245 133
        if (empty($options['_UNSORTED'])) {
246 133
            ksort($ret->options);
247 133
        }
248
249 133
        --$list->idx;
250 133
        return $ret;
251
    }
252
253
    /**
254
     * @param OptionsArray $component The component to be built.
255
     * @param array        $options   Parameters for building.
256
     *
257
     * @return string
258
     */
259 16
    public static function build($component, array $options = array())
260
    {
261 16
        if (empty($component->options)) {
262 10
            return '';
263
        }
264
265 12
        $options = array();
266 12
        foreach ($component->options as $option) {
267 12
            if (!is_array($option)) {
268 11
                $options[] = $option;
269 11
            } else {
270 4
                $options[] = $option['name']
271 4
                    . (!empty($option['equals']) ? '=' : ' ')
272 4
                    . (!empty($option['expr']) ? $option['expr'] : $option['value']);
273
            }
274 12
        }
275 12
        return implode(' ', $options);
276
    }
277
278
    /**
279
     * Checks if it has the specified option and returns it value or true.
280
     *
281
     * @param string $key     The key to be checked.
282
     * @param bool   $getExpr Gets the expression instead of the value.
283
     *                        The value is the processed form of the expression.
284
     *
285
     * @return mixed
286
     */
287 66
    public function has($key, $getExpr = false)
288
    {
289 66
        foreach ($this->options as $option) {
290 53
            if (is_array($option)) {
291 10
                if (!strcasecmp($key, $option['name'])) {
292 7
                    return $getExpr ? $option['expr'] : $option['value'];
293
                }
294 53
            } elseif (!strcasecmp($key, $option)) {
295 47
                return true;
296
            }
297 62
        }
298 60
        return false;
299
    }
300
301
    /**
302
     * Removes the option from the array.
303
     *
304
     * @param string $key The key to be removed.
305
     *
306
     * @return bool Whether the key was found and deleted or not.
307
     */
308 1
    public function remove($key)
309
    {
310 1
        foreach ($this->options as $idx => $option) {
311 1
            if (is_array($option)) {
312
                if (!strcasecmp($key, $option['name'])) {
313
                    unset($this->options[$idx]);
314
                    return true;
315
                }
316 1
            } elseif (!strcasecmp($key, $option)) {
317 1
                unset($this->options[$idx]);
318 1
                return true;
319
            }
320 1
        }
321 1
        return false;
322
    }
323
324
    /**
325
     * Merges the specified options with these ones. Values with same ID will be
326
     * replaced.
327
     *
328
     * @param array|OptionsArray $options The options to be merged.
329
     *
330
     * @return void
331
     */
332 5
    public function merge($options)
333
    {
334 5
        if (is_array($options)) {
335 1
            $this->options = array_merge_recursive($this->options, $options);
336 5
        } elseif ($options instanceof OptionsArray) {
337 4
            $this->options = array_merge_recursive($this->options, $options->options);
338 4
        }
339 5
    }
340
341
    /**
342
     * Checks tf there are no options set.
343
     *
344
     * @return bool
345
     */
346 5
    public function isEmpty()
347
    {
348 5
        return empty($this->options);
349
    }
350
}
351