Complex classes like OptionsArray 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 OptionsArray, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 | 146 | public function __construct(array $options = array()) |
|
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 | 141 | public static function parse(Parser $parser, TokensList $list, array $options = array()) |
|
55 | { |
||
56 | 141 | $ret = new OptionsArray(); |
|
57 | |||
58 | /** |
||
59 | * The ID that will be assigned to duplicate options. |
||
60 | * |
||
61 | * @var int $lastAssignedId |
||
62 | */ |
||
63 | 141 | $lastAssignedId = count($options) + 1; |
|
64 | |||
65 | /** |
||
66 | * The option that was processed last time. |
||
67 | * |
||
68 | * @var array $lastOption |
||
69 | */ |
||
70 | 141 | $lastOption = null; |
|
71 | |||
72 | /** |
||
73 | * The index of the option that was processed last time. |
||
74 | * |
||
75 | * @var int $lastOptionId |
||
76 | */ |
||
77 | 141 | $lastOptionId = 0; |
|
78 | |||
79 | /** |
||
80 | * Counts brackets. |
||
81 | * |
||
82 | * @var int $brackets |
||
83 | */ |
||
84 | 141 | $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 | 141 | $state = 0; |
|
100 | |||
101 | 141 | for (; $list->idx < $list->count; ++$list->idx) { |
|
102 | /** |
||
103 | * Token parsed at this moment. |
||
104 | * |
||
105 | * @var Token $token |
||
106 | */ |
||
107 | 141 | $token = $list->tokens[$list->idx]; |
|
108 | |||
109 | // End of statement. |
||
110 | 141 | if ($token->type === Token::TYPE_DELIMITER) { |
|
111 | 38 | break; |
|
112 | } |
||
113 | |||
114 | // Skipping comments. |
||
115 | 138 | if ($token->type === Token::TYPE_COMMENT) { |
|
116 | 3 | continue; |
|
117 | } |
||
118 | |||
119 | // Skipping whitespace if not parsing value. |
||
120 | 138 | if (($token->type === Token::TYPE_WHITESPACE) && ($brackets === 0)) { |
|
121 | 129 | continue; |
|
122 | } |
||
123 | |||
124 | 138 | if ($lastOption === null) { |
|
125 | 138 | $upper = strtoupper($token->token); |
|
126 | 138 | if (isset($options[$upper])) { |
|
127 | 79 | $lastOption = $options[$upper]; |
|
128 | 79 | $lastOptionId = is_array($lastOption) ? |
|
129 | 79 | $lastOption[0] : $lastOption; |
|
130 | 79 | $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 | 79 | 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 | 79 | } else { |
|
157 | // There is no option to be processed. |
||
158 | 129 | break; |
|
159 | } |
||
160 | 79 | } |
|
161 | |||
162 | 79 | if ($state === 0) { |
|
163 | 79 | 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 | 74 | $ret->options[$lastOptionId] = $token->value; |
|
167 | 74 | $lastOption = null; |
|
168 | 74 | $state = 0; |
|
169 | 79 | } 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 | 27 | $ret->options[$lastOptionId] = array( |
|
175 | // @var string The name of the option. |
||
176 | 27 | 'name' => $token->value, |
|
177 | // @var bool Whether it contains an equal sign. |
||
178 | // This is used by the builder to rebuild it. |
||
179 | 27 | 'equals' => $lastOption[1] === 'var=', |
|
180 | // @var string Raw value. |
||
181 | 27 | 'expr' => '', |
|
182 | // @var string Processed value. |
||
183 | 27 | 'value' => '', |
|
184 | ); |
||
185 | 27 | $state = 1; |
|
186 | 30 | } 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 | 14 | ++$list->idx; |
|
192 | 14 | $ret->options[$lastOptionId] = array( |
|
193 | // @var string The name of the option. |
||
194 | 14 | 'name' => $token->value, |
|
195 | // @var Expression The parsed expression. |
||
196 | 14 | 'expr' => null, |
|
197 | ); |
||
198 | 14 | $state = 1; |
|
199 | 14 | } |
|
200 | 79 | } elseif ($state === 1) { |
|
201 | 30 | $state = 2; |
|
202 | 30 | if ($token->token === '=') { |
|
203 | 20 | $ret->options[$lastOptionId]['equals'] = true; |
|
204 | 20 | continue; |
|
205 | } |
||
206 | 21 | } |
|
207 | |||
208 | // This is outside the `elseif` group above because the change might |
||
209 | // change this iteration. |
||
210 | 79 | if ($state === 2) { |
|
211 | 30 | if ($lastOption[1] === 'expr') { |
|
212 | 14 | $ret->options[$lastOptionId]['expr'] = Expression::parse( |
|
213 | 14 | $parser, |
|
214 | 14 | $list, |
|
215 | 14 | empty($lastOption[2]) ? array() : $lastOption[2] |
|
216 | 14 | ); |
|
217 | 14 | $ret->options[$lastOptionId]['value'] |
|
218 | 14 | = $ret->options[$lastOptionId]['expr']->expr; |
|
219 | 14 | $lastOption = null; |
|
220 | 14 | $state = 0; |
|
221 | 14 | } else { |
|
222 | 27 | if ($token->token === '(') { |
|
223 | 2 | ++$brackets; |
|
224 | 27 | } elseif ($token->token === ')') { |
|
225 | 2 | --$brackets; |
|
226 | 2 | } |
|
227 | |||
228 | 27 | $ret->options[$lastOptionId]['expr'] .= $token->token; |
|
229 | |||
230 | 27 | if (!((($token->token === '(') && ($brackets === 1)) |
|
231 | 27 | || (($token->token === ')') && ($brackets === 0))) |
|
232 | 27 | ) { |
|
233 | // First pair of brackets is being skipped. |
||
234 | 27 | $ret->options[$lastOptionId]['value'] .= $token->value; |
|
235 | 27 | } |
|
236 | |||
237 | // Checking if we finished parsing. |
||
238 | 27 | if ($brackets === 0) { |
|
239 | 27 | $lastOption = null; |
|
240 | 27 | } |
|
241 | } |
||
242 | 30 | } |
|
243 | 79 | } |
|
244 | |||
245 | 141 | /* |
|
246 | 141 | * We reached the end of statement without getting a value |
|
247 | 141 | * for an option for which a value was required |
|
248 | */ |
||
249 | 141 | if ($state === 1 |
|
250 | 141 | && $lastOption |
|
251 | && ($lastOption[1] == 'expr' |
||
252 | || $lastOption[1] == 'var' |
||
253 | || $lastOption[1] == 'var=') |
||
254 | ) { |
||
255 | $parser->error( |
||
256 | sprintf( |
||
257 | __('Value/Expression for the option %1$s was expected'), |
||
258 | $ret->options[$lastOptionId]['name'] |
||
259 | 20 | ), |
|
260 | $list->tokens[$list->idx - 1] |
||
261 | 20 | ); |
|
262 | 14 | } |
|
263 | |||
264 | if (empty($options['_UNSORTED'])) { |
||
265 | 15 | ksort($ret->options); |
|
266 | 15 | } |
|
267 | 15 | ||
268 | 14 | --$list->idx; |
|
269 | 14 | return $ret; |
|
270 | 6 | } |
|
271 | 6 | ||
272 | 6 | /** |
|
273 | * @param OptionsArray $component The component to be built. |
||
274 | 15 | * @param array $options Parameters for building. |
|
275 | 15 | * |
|
276 | * @return string |
||
277 | */ |
||
278 | public static function build($component, array $options = array()) |
||
279 | { |
||
280 | if (empty($component->options)) { |
||
281 | return ''; |
||
282 | } |
||
283 | |||
284 | $options = array(); |
||
285 | foreach ($component->options as $option) { |
||
286 | if (!is_array($option)) { |
||
287 | 72 | $options[] = $option; |
|
288 | } else { |
||
289 | 72 | $options[] = $option['name'] |
|
290 | 60 | . (!empty($option['equals']) ? '=' : ' ') |
|
291 | 10 | . (!empty($option['expr']) ? $option['expr'] : $option['value']); |
|
292 | 7 | } |
|
293 | } |
||
294 | 60 | return implode(' ', $options); |
|
295 | 52 | } |
|
296 | |||
297 | 68 | /** |
|
298 | 66 | * Checks if it has the specified option and returns it value or true. |
|
299 | * |
||
300 | * @param string $key The key to be checked. |
||
301 | * @param bool $getExpr Gets the expression instead of the value. |
||
302 | * The value is the processed form of the expression. |
||
303 | * |
||
304 | * @return mixed |
||
305 | */ |
||
306 | public function has($key, $getExpr = false) |
||
307 | { |
||
308 | 1 | foreach ($this->options as $option) { |
|
309 | if (is_array($option)) { |
||
310 | 1 | if (!strcasecmp($key, $option['name'])) { |
|
311 | 1 | return $getExpr ? $option['expr'] : $option['value']; |
|
312 | } |
||
313 | } elseif (!strcasecmp($key, $option)) { |
||
314 | return true; |
||
315 | } |
||
316 | 1 | } |
|
317 | 1 | return false; |
|
318 | 1 | } |
|
319 | |||
320 | 1 | /** |
|
321 | 1 | * Removes the option from the array. |
|
322 | * |
||
323 | * @param string $key The key to be removed. |
||
324 | * |
||
325 | * @return bool Whether the key was found and deleted or not. |
||
326 | */ |
||
327 | public function remove($key) |
||
328 | { |
||
329 | foreach ($this->options as $idx => $option) { |
||
330 | if (is_array($option)) { |
||
331 | if (!strcasecmp($key, $option['name'])) { |
||
332 | 5 | unset($this->options[$idx]); |
|
333 | return true; |
||
334 | 5 | } |
|
335 | 1 | } elseif (!strcasecmp($key, $option)) { |
|
336 | 5 | unset($this->options[$idx]); |
|
337 | 4 | return true; |
|
338 | 4 | } |
|
339 | 5 | } |
|
340 | return false; |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * Merges the specified options with these ones. Values with same ID will be |
||
345 | * replaced. |
||
346 | 5 | * |
|
347 | * @param array|OptionsArray $options The options to be merged. |
||
348 | 5 | * |
|
349 | * @return void |
||
350 | */ |
||
351 | public function merge($options) |
||
359 | |||
360 | /** |
||
361 | * Checks tf there are no options set. |
||
362 | * |
||
363 | * @return bool |
||
364 | */ |
||
365 | public function isEmpty() |
||
369 | } |
||
370 |