Total Complexity | 147 |
Total Lines | 1120 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like OptionsResolver 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.
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 OptionsResolver, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
42 | class OptionsResolver implements Options |
||
43 | { |
||
44 | /** |
||
45 | * The names of all defined options. |
||
46 | */ |
||
47 | private $defined = []; |
||
48 | |||
49 | /** |
||
50 | * The default option values. |
||
51 | */ |
||
52 | private $defaults = []; |
||
53 | |||
54 | /** |
||
55 | * A list of closure for nested options. |
||
56 | * |
||
57 | * @var \Closure[][] |
||
58 | */ |
||
59 | private $nested = []; |
||
60 | |||
61 | /** |
||
62 | * The names of required options. |
||
63 | */ |
||
64 | private $required = []; |
||
65 | |||
66 | /** |
||
67 | * The resolved option values. |
||
68 | */ |
||
69 | private $resolved = []; |
||
70 | |||
71 | /** |
||
72 | * A list of normalizer closures. |
||
73 | * |
||
74 | * @var \Closure[] |
||
75 | */ |
||
76 | private $normalizers = []; |
||
77 | |||
78 | /** |
||
79 | * A list of accepted values for each option. |
||
80 | */ |
||
81 | private $allowedValues = []; |
||
82 | |||
83 | /** |
||
84 | * A list of accepted types for each option. |
||
85 | */ |
||
86 | private $allowedTypes = []; |
||
87 | |||
88 | /** |
||
89 | * A list of closures for evaluating lazy options. |
||
90 | */ |
||
91 | private $lazy = []; |
||
92 | |||
93 | /** |
||
94 | * A list of lazy options whose closure is currently being called. |
||
95 | * |
||
96 | * This list helps detecting circular dependencies between lazy options. |
||
97 | */ |
||
98 | private $calling = []; |
||
99 | |||
100 | /** |
||
101 | * A list of deprecated options. |
||
102 | */ |
||
103 | private $deprecated = []; |
||
104 | |||
105 | /** |
||
106 | * The list of options provided by the user. |
||
107 | */ |
||
108 | private $given = []; |
||
109 | |||
110 | /** |
||
111 | * Whether the instance is locked for reading. |
||
112 | * |
||
113 | * Once locked, the options cannot be changed anymore. This is |
||
114 | * necessary in order to avoid inconsistencies during the resolving |
||
115 | * process. If any option is changed after being read, all evaluated |
||
116 | * lazy options that depend on this option would become invalid. |
||
117 | */ |
||
118 | private $locked = false; |
||
119 | |||
120 | private static $typeAliases = [ |
||
121 | 'boolean' => 'bool', |
||
122 | 'integer' => 'int', |
||
123 | 'double' => 'float', |
||
124 | ]; |
||
125 | |||
126 | /** |
||
127 | * Sets the default value of a given option. |
||
128 | * |
||
129 | * If the default value should be set based on other options, you can pass |
||
130 | * a closure with the following signature: |
||
131 | * |
||
132 | * function (Options $options) { |
||
133 | * // ... |
||
134 | * } |
||
135 | * |
||
136 | * The closure will be evaluated when {@link resolve()} is called. The |
||
137 | * closure has access to the resolved values of other options through the |
||
138 | * passed {@link Options} instance: |
||
139 | * |
||
140 | * function (Options $options) { |
||
141 | * if (isset($options['port'])) { |
||
142 | * // ... |
||
143 | * } |
||
144 | * } |
||
145 | * |
||
146 | * If you want to access the previously set default value, add a second |
||
147 | * argument to the closure's signature: |
||
148 | * |
||
149 | * $options->setDefault('name', 'Default Name'); |
||
150 | * |
||
151 | * $options->setDefault('name', function (Options $options, $previousValue) { |
||
152 | * // 'Default Name' === $previousValue |
||
153 | * }); |
||
154 | * |
||
155 | * This is mostly useful if the configuration of the {@link Options} object |
||
156 | * is spread across different locations of your code, such as base and |
||
157 | * sub-classes. |
||
158 | * |
||
159 | * If you want to define nested options, you can pass a closure with the |
||
160 | * following signature: |
||
161 | * |
||
162 | * $options->setDefault('database', function (OptionsResolver $resolver) { |
||
163 | * $resolver->setDefined(['dbname', 'host', 'port', 'user', 'pass']); |
||
164 | * } |
||
165 | * |
||
166 | * To get access to the parent options, add a second argument to the closure's |
||
167 | * signature: |
||
168 | * |
||
169 | * function (OptionsResolver $resolver, Options $parent) { |
||
170 | * // 'default' === $parent['connection'] |
||
171 | * } |
||
172 | * |
||
173 | * @param string $option The name of the option |
||
174 | * @param mixed $value The default value of the option |
||
175 | * |
||
176 | * @return $this |
||
177 | * |
||
178 | * @throws AccessException If called from a lazy option or normalizer |
||
179 | */ |
||
180 | public function setDefault($option, $value) |
||
181 | { |
||
182 | // Setting is not possible once resolving starts, because then lazy |
||
183 | // options could manipulate the state of the object, leading to |
||
184 | // inconsistent results. |
||
185 | if ($this->locked) { |
||
186 | throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); |
||
187 | } |
||
188 | |||
189 | // If an option is a closure that should be evaluated lazily, store it |
||
190 | // in the "lazy" property. |
||
191 | if ($value instanceof Closure) { |
||
192 | $reflClosure = new ReflectionFunction($value); |
||
193 | $params = $reflClosure->getParameters(); |
||
194 | |||
195 | if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && Options::class === $class->name) { |
||
196 | // Initialize the option if no previous value exists |
||
197 | if (!isset($this->defaults[$option])) { |
||
198 | $this->defaults[$option] = null; |
||
199 | } |
||
200 | |||
201 | // Ignore previous lazy options if the closure has no second parameter |
||
202 | if (!isset($this->lazy[$option]) || !isset($params[1])) { |
||
203 | $this->lazy[$option] = []; |
||
204 | } |
||
205 | |||
206 | // Store closure for later evaluation |
||
207 | $this->lazy[$option][] = $value; |
||
208 | $this->defined[$option] = true; |
||
209 | |||
210 | // Make sure the option is processed and is not nested anymore |
||
211 | unset($this->resolved[$option], $this->nested[$option]); |
||
212 | |||
213 | return $this; |
||
214 | } |
||
215 | |||
216 | if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && self::class === $class->name && (!isset($params[1]) || (null !== ($class = $params[1]->getClass()) && Options::class === $class->name))) { |
||
217 | // Store closure for later evaluation |
||
218 | $this->nested[$option][] = $value; |
||
219 | $this->defaults[$option] = []; |
||
220 | $this->defined[$option] = true; |
||
221 | |||
222 | // Make sure the option is processed and is not lazy anymore |
||
223 | unset($this->resolved[$option], $this->lazy[$option]); |
||
224 | |||
225 | return $this; |
||
226 | } |
||
227 | } |
||
228 | |||
229 | // This option is not lazy nor nested anymore |
||
230 | unset($this->lazy[$option], $this->nested[$option]); |
||
231 | |||
232 | // Yet undefined options can be marked as resolved, because we only need |
||
233 | // to resolve options with lazy closures, normalizers or validation |
||
234 | // rules, none of which can exist for undefined options |
||
235 | // If the option was resolved before, update the resolved value |
||
236 | if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) { |
||
237 | $this->resolved[$option] = $value; |
||
238 | } |
||
239 | |||
240 | $this->defaults[$option] = $value; |
||
241 | $this->defined[$option] = true; |
||
242 | |||
243 | return $this; |
||
244 | } |
||
245 | |||
246 | /** |
||
247 | * Sets a list of default values. |
||
248 | * |
||
249 | * @param array $defaults The default values to set |
||
250 | * |
||
251 | * @return $this |
||
252 | * |
||
253 | * @throws AccessException If called from a lazy option or normalizer |
||
254 | */ |
||
255 | public function setDefaults(array $defaults) |
||
256 | { |
||
257 | foreach ($defaults as $option => $value) { |
||
258 | $this->setDefault($option, $value); |
||
259 | } |
||
260 | |||
261 | return $this; |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Returns whether a default value is set for an option. |
||
266 | * |
||
267 | * Returns true if {@link setDefault()} was called for this option. |
||
268 | * An option is also considered set if it was set to null. |
||
269 | * |
||
270 | * @param string $option The option name |
||
271 | * |
||
272 | * @return bool Whether a default value is set |
||
273 | */ |
||
274 | public function hasDefault($option) |
||
275 | { |
||
276 | return array_key_exists($option, $this->defaults); |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Marks one or more options as required. |
||
281 | * |
||
282 | * @param string|string[] $optionNames One or more option names |
||
283 | * |
||
284 | * @return $this |
||
285 | * |
||
286 | * @throws AccessException If called from a lazy option or normalizer |
||
287 | */ |
||
288 | public function setRequired($optionNames) |
||
289 | { |
||
290 | if ($this->locked) { |
||
291 | throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); |
||
292 | } |
||
293 | |||
294 | foreach ((array) $optionNames as $option) { |
||
295 | $this->defined[$option] = true; |
||
296 | $this->required[$option] = true; |
||
297 | } |
||
298 | |||
299 | return $this; |
||
300 | } |
||
301 | |||
302 | /** |
||
303 | * Returns whether an option is required. |
||
304 | * |
||
305 | * An option is required if it was passed to {@link setRequired()}. |
||
306 | * |
||
307 | * @param string $option The name of the option |
||
308 | * |
||
309 | * @return bool Whether the option is required |
||
310 | */ |
||
311 | public function isRequired($option) |
||
312 | { |
||
313 | return isset($this->required[$option]); |
||
314 | } |
||
315 | |||
316 | /** |
||
317 | * Returns the names of all required options. |
||
318 | * |
||
319 | * @return string[] The names of the required options |
||
320 | * |
||
321 | * @see isRequired() |
||
322 | */ |
||
323 | public function getRequiredOptions() |
||
324 | { |
||
325 | return array_keys($this->required); |
||
326 | } |
||
327 | |||
328 | /** |
||
329 | * Returns whether an option is missing a default value. |
||
330 | * |
||
331 | * An option is missing if it was passed to {@link setRequired()}, but not |
||
332 | * to {@link setDefault()}. This option must be passed explicitly to |
||
333 | * {@link resolve()}, otherwise an exception will be thrown. |
||
334 | * |
||
335 | * @param string $option The name of the option |
||
336 | * |
||
337 | * @return bool Whether the option is missing |
||
338 | */ |
||
339 | public function isMissing($option) |
||
340 | { |
||
341 | return isset($this->required[$option]) && ! array_key_exists($option, $this->defaults); |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * Returns the names of all options missing a default value. |
||
346 | * |
||
347 | * @return string[] The names of the missing options |
||
348 | * |
||
349 | * @see isMissing() |
||
350 | */ |
||
351 | public function getMissingOptions() |
||
352 | { |
||
353 | return array_keys(array_diff_key($this->required, $this->defaults)); |
||
354 | } |
||
355 | |||
356 | /** |
||
357 | * Defines a valid option name. |
||
358 | * |
||
359 | * Defines an option name without setting a default value. The option will |
||
360 | * be accepted when passed to {@link resolve()}. When not passed, the |
||
361 | * option will not be included in the resolved options. |
||
362 | * |
||
363 | * @param string|string[] $optionNames One or more option names |
||
364 | * |
||
365 | * @return $this |
||
366 | * |
||
367 | * @throws AccessException If called from a lazy option or normalizer |
||
368 | */ |
||
369 | public function setDefined($optionNames) |
||
370 | { |
||
371 | if ($this->locked) { |
||
372 | throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); |
||
373 | } |
||
374 | |||
375 | foreach ((array) $optionNames as $option) { |
||
376 | $this->defined[$option] = true; |
||
377 | } |
||
378 | |||
379 | return $this; |
||
380 | } |
||
381 | |||
382 | /** |
||
383 | * Returns whether an option is defined. |
||
384 | * |
||
385 | * Returns true for any option passed to {@link setDefault()}, |
||
386 | * {@link setRequired()} or {@link setDefined()}. |
||
387 | * |
||
388 | * @param string $option The option name |
||
389 | * |
||
390 | * @return bool Whether the option is defined |
||
391 | */ |
||
392 | public function isDefined($option) |
||
393 | { |
||
394 | return isset($this->defined[$option]); |
||
395 | } |
||
396 | |||
397 | /** |
||
398 | * Returns the names of all defined options. |
||
399 | * |
||
400 | * @return string[] The names of the defined options |
||
401 | * |
||
402 | * @see isDefined() |
||
403 | */ |
||
404 | public function getDefinedOptions() |
||
405 | { |
||
406 | return array_keys($this->defined); |
||
407 | } |
||
408 | |||
409 | public function isNested(string $option): bool |
||
410 | { |
||
411 | return isset($this->nested[$option]); |
||
412 | } |
||
413 | |||
414 | /** |
||
415 | * Deprecates an option, allowed types or values. |
||
416 | * |
||
417 | * Instead of passing the message, you may also pass a closure with the |
||
418 | * following signature: |
||
419 | * |
||
420 | * function (Options $options, $value): string { |
||
421 | * // ... |
||
422 | * } |
||
423 | * |
||
424 | * The closure receives the value as argument and should return a string. |
||
425 | * Return an empty string to ignore the option deprecation. |
||
426 | * |
||
427 | * The closure is invoked when {@link resolve()} is called. The parameter |
||
428 | * passed to the closure is the value of the option after validating it |
||
429 | * and before normalizing it. |
||
430 | * |
||
431 | * @param string|\Closure $deprecationMessage |
||
432 | */ |
||
433 | public function setDeprecated(string $option, $deprecationMessage = 'The option "%name%" is deprecated.'): self |
||
458 | } |
||
459 | |||
460 | public function isDeprecated(string $option): bool |
||
463 | } |
||
464 | |||
465 | /** |
||
466 | * Sets the normalizer for an option. |
||
467 | * |
||
468 | * The normalizer should be a closure with the following signature: |
||
469 | * |
||
470 | * function (Options $options, $value) { |
||
471 | * // ... |
||
472 | * } |
||
473 | * |
||
474 | * The closure is invoked when {@link resolve()} is called. The closure |
||
475 | * has access to the resolved values of other options through the passed |
||
476 | * {@link Options} instance. |
||
477 | * |
||
478 | * The second parameter passed to the closure is the value of |
||
479 | * the option. |
||
480 | * |
||
481 | * The resolved option value is set to the return value of the closure. |
||
482 | * |
||
483 | * @param string $option The option name |
||
484 | * @param \Closure $normalizer The normalizer |
||
485 | * |
||
486 | * @return $this |
||
487 | * |
||
488 | * @throws UndefinedOptionsException If the option is undefined |
||
489 | * @throws AccessException If called from a lazy option or normalizer |
||
490 | */ |
||
491 | public function setNormalizer($option, Closure $normalizer) |
||
492 | { |
||
493 | if ($this->locked) { |
||
494 | throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); |
||
495 | } |
||
496 | |||
497 | if (!isset($this->defined[$option])) { |
||
498 | throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); |
||
499 | } |
||
500 | |||
501 | $this->normalizers[$option] = $normalizer; |
||
502 | |||
503 | // Make sure the option is processed |
||
504 | unset($this->resolved[$option]); |
||
505 | |||
506 | return $this; |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * Sets allowed values for an option. |
||
511 | * |
||
512 | * Instead of passing values, you may also pass a closures with the |
||
513 | * following signature: |
||
514 | * |
||
515 | * function ($value) { |
||
516 | * // return true or false |
||
517 | * } |
||
518 | * |
||
519 | * The closure receives the value as argument and should return true to |
||
520 | * accept the value and false to reject the value. |
||
521 | * |
||
522 | * @param string $option The option name |
||
523 | * @param mixed $allowedValues One or more acceptable values/closures |
||
524 | * |
||
525 | * @return $this |
||
526 | * |
||
527 | * @throws UndefinedOptionsException If the option is undefined |
||
528 | * @throws AccessException If called from a lazy option or normalizer |
||
529 | */ |
||
530 | public function setAllowedValues($option, $allowedValues) |
||
531 | { |
||
532 | if ($this->locked) { |
||
533 | throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); |
||
534 | } |
||
535 | |||
536 | if (!isset($this->defined[$option])) { |
||
537 | throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); |
||
538 | } |
||
539 | |||
540 | $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : [$allowedValues]; |
||
541 | |||
542 | // Make sure the option is processed |
||
543 | unset($this->resolved[$option]); |
||
544 | |||
545 | return $this; |
||
546 | } |
||
547 | |||
548 | /** |
||
549 | * Adds allowed values for an option. |
||
550 | * |
||
551 | * The values are merged with the allowed values defined previously. |
||
552 | * |
||
553 | * Instead of passing values, you may also pass a closures with the |
||
554 | * following signature: |
||
555 | * |
||
556 | * function ($value) { |
||
557 | * // return true or false |
||
558 | * } |
||
559 | * |
||
560 | * The closure receives the value as argument and should return true to |
||
561 | * accept the value and false to reject the value. |
||
562 | * |
||
563 | * @param string $option The option name |
||
564 | * @param mixed $allowedValues One or more acceptable values/closures |
||
565 | * |
||
566 | * @return $this |
||
567 | * |
||
568 | * @throws UndefinedOptionsException If the option is undefined |
||
569 | * @throws AccessException If called from a lazy option or normalizer |
||
570 | */ |
||
571 | public function addAllowedValues($option, $allowedValues) |
||
572 | { |
||
573 | if ($this->locked) { |
||
574 | throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); |
||
575 | } |
||
576 | |||
577 | if (!isset($this->defined[$option])) { |
||
578 | throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); |
||
579 | } |
||
580 | |||
581 | if (! is_array($allowedValues)) { |
||
582 | $allowedValues = [$allowedValues]; |
||
583 | } |
||
584 | |||
585 | if (!isset($this->allowedValues[$option])) { |
||
586 | $this->allowedValues[$option] = $allowedValues; |
||
587 | } else { |
||
588 | $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); |
||
589 | } |
||
590 | |||
591 | // Make sure the option is processed |
||
592 | unset($this->resolved[$option]); |
||
593 | |||
594 | return $this; |
||
595 | } |
||
596 | |||
597 | /** |
||
598 | * Sets allowed types for an option. |
||
599 | * |
||
600 | * Any type for which a corresponding is_<type>() function exists is |
||
601 | * acceptable. Additionally, fully-qualified class or interface names may |
||
602 | * be passed. |
||
603 | * |
||
604 | * @param string $option The option name |
||
605 | * @param string|string[] $allowedTypes One or more accepted types |
||
606 | * |
||
607 | * @return $this |
||
608 | * |
||
609 | * @throws UndefinedOptionsException If the option is undefined |
||
610 | * @throws AccessException If called from a lazy option or normalizer |
||
611 | */ |
||
612 | public function setAllowedTypes($option, $allowedTypes) |
||
613 | { |
||
614 | if ($this->locked) { |
||
615 | throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); |
||
616 | } |
||
617 | |||
618 | if (!isset($this->defined[$option])) { |
||
619 | throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); |
||
620 | } |
||
621 | |||
622 | $this->allowedTypes[$option] = (array) $allowedTypes; |
||
623 | |||
624 | // Make sure the option is processed |
||
625 | unset($this->resolved[$option]); |
||
626 | |||
627 | return $this; |
||
628 | } |
||
629 | |||
630 | /** |
||
631 | * Adds allowed types for an option. |
||
632 | * |
||
633 | * The types are merged with the allowed types defined previously. |
||
634 | * |
||
635 | * Any type for which a corresponding is_<type>() function exists is |
||
636 | * acceptable. Additionally, fully-qualified class or interface names may |
||
637 | * be passed. |
||
638 | * |
||
639 | * @param string $option The option name |
||
640 | * @param string|string[] $allowedTypes One or more accepted types |
||
641 | * |
||
642 | * @return $this |
||
643 | * |
||
644 | * @throws UndefinedOptionsException If the option is undefined |
||
645 | * @throws AccessException If called from a lazy option or normalizer |
||
646 | */ |
||
647 | public function addAllowedTypes($option, $allowedTypes) |
||
648 | { |
||
649 | if ($this->locked) { |
||
650 | throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); |
||
651 | } |
||
652 | |||
653 | if (!isset($this->defined[$option])) { |
||
654 | throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); |
||
655 | } |
||
656 | |||
657 | if (!isset($this->allowedTypes[$option])) { |
||
658 | $this->allowedTypes[$option] = (array) $allowedTypes; |
||
659 | } else { |
||
660 | $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); |
||
661 | } |
||
662 | |||
663 | // Make sure the option is processed |
||
664 | unset($this->resolved[$option]); |
||
665 | |||
666 | return $this; |
||
667 | } |
||
668 | |||
669 | /** |
||
670 | * Removes the option with the given name. |
||
671 | * |
||
672 | * Undefined options are ignored. |
||
673 | * |
||
674 | * @param string|string[] $optionNames One or more option names |
||
675 | * |
||
676 | * @return $this |
||
677 | * |
||
678 | * @throws AccessException If called from a lazy option or normalizer |
||
679 | */ |
||
680 | public function remove($optionNames) |
||
681 | { |
||
682 | if ($this->locked) { |
||
683 | throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); |
||
684 | } |
||
685 | |||
686 | foreach ((array) $optionNames as $option) { |
||
687 | unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); |
||
688 | unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]); |
||
689 | } |
||
690 | |||
691 | return $this; |
||
692 | } |
||
693 | |||
694 | /** |
||
695 | * Removes all options. |
||
696 | * |
||
697 | * @return $this |
||
698 | * |
||
699 | * @throws AccessException If called from a lazy option or normalizer |
||
700 | */ |
||
701 | public function clear() |
||
702 | { |
||
703 | if ($this->locked) { |
||
704 | throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); |
||
705 | } |
||
706 | |||
707 | $this->defined = []; |
||
708 | $this->defaults = []; |
||
709 | $this->nested = []; |
||
710 | $this->required = []; |
||
711 | $this->resolved = []; |
||
712 | $this->lazy = []; |
||
713 | $this->normalizers = []; |
||
714 | $this->allowedTypes = []; |
||
715 | $this->allowedValues = []; |
||
716 | $this->deprecated = []; |
||
717 | |||
718 | return $this; |
||
719 | } |
||
720 | |||
721 | /** |
||
722 | * Merges options with the default values stored in the container and |
||
723 | * validates them. |
||
724 | * |
||
725 | * Exceptions are thrown if: |
||
726 | * |
||
727 | * - Undefined options are passed; |
||
728 | * - Required options are missing; |
||
729 | * - Options have invalid types; |
||
730 | * - Options have invalid values. |
||
731 | * |
||
732 | * @param array $options A map of option names to values |
||
733 | * |
||
734 | * @return array The merged and validated options |
||
735 | * |
||
736 | * @throws UndefinedOptionsException If an option name is undefined |
||
737 | * @throws InvalidOptionsException If an option doesn't fulfill the |
||
738 | * specified validation rules |
||
739 | * @throws MissingOptionsException If a required option is missing |
||
740 | * @throws OptionDefinitionException If there is a cyclic dependency between |
||
741 | * lazy options and/or normalizers |
||
742 | * @throws NoSuchOptionException If a lazy option reads an unavailable option |
||
743 | * @throws AccessException If called from a lazy option or normalizer |
||
744 | */ |
||
745 | public function resolve(array $options = []) |
||
746 | { |
||
747 | if ($this->locked) { |
||
748 | throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); |
||
749 | } |
||
750 | |||
751 | // Allow this method to be called multiple times |
||
752 | $clone = clone $this; |
||
753 | |||
754 | // Make sure that no unknown options are passed |
||
755 | $diff = array_diff_key($options, $clone->defined); |
||
756 | |||
757 | if (count($diff) > 0) { |
||
758 | ksort($clone->defined); |
||
759 | ksort($diff); |
||
760 | |||
761 | throw new UndefinedOptionsException(sprintf((count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', implode('", "', array_keys($diff)), implode('", "', array_keys($clone->defined)))); |
||
762 | } |
||
763 | |||
764 | // Override options set by the user |
||
765 | foreach ($options as $option => $value) { |
||
766 | $clone->given[$option] = true; |
||
767 | $clone->defaults[$option] = $value; |
||
768 | unset($clone->resolved[$option], $clone->lazy[$option]); |
||
769 | } |
||
770 | |||
771 | // Check whether any required option is missing |
||
772 | $diff = array_diff_key($clone->required, $clone->defaults); |
||
773 | |||
774 | if (count($diff) > 0) { |
||
775 | ksort($diff); |
||
776 | |||
777 | throw new MissingOptionsException(sprintf(count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', implode('", "', array_keys($diff)))); |
||
778 | } |
||
779 | |||
780 | // Lock the container |
||
781 | $clone->locked = true; |
||
782 | |||
783 | // Now process the individual options. Use offsetGet(), which resolves |
||
784 | // the option itself and any options that the option depends on |
||
785 | foreach ($clone->defaults as $option => $_) { |
||
786 | $clone->offsetGet($option); |
||
787 | } |
||
788 | |||
789 | return $clone->resolved; |
||
790 | } |
||
791 | |||
792 | /** |
||
793 | * Returns the resolved value of an option. |
||
794 | * |
||
795 | * @param string $option The option name |
||
796 | * @param bool $triggerDeprecation Whether to trigger the deprecation or not (true by default) |
||
797 | * |
||
798 | * @return mixed The option value |
||
799 | * |
||
800 | * @throws AccessException If accessing this method outside of |
||
801 | * {@link resolve()} |
||
802 | * @throws NoSuchOptionException If the option is not set |
||
803 | * @throws InvalidOptionsException If the option doesn't fulfill the |
||
804 | * specified validation rules |
||
805 | * @throws OptionDefinitionException If there is a cyclic dependency between |
||
806 | * lazy options and/or normalizers |
||
807 | */ |
||
808 | public function offsetGet($option/*, bool $triggerDeprecation = true*/) |
||
809 | { |
||
810 | if (!$this->locked) { |
||
811 | throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); |
||
812 | } |
||
813 | |||
814 | $triggerDeprecation = 1 === func_num_args() || func_get_arg(1); |
||
815 | |||
816 | // Shortcut for resolved options |
||
817 | if (isset($this->resolved[$option]) || array_key_exists($option, $this->resolved)) { |
||
818 | if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || $this->calling) && is_string($this->deprecated[$option])) { |
||
819 | @trigger_error(strtr($this->deprecated[$option], ['%name%' => $option]), E_USER_DEPRECATED); |
||
820 | } |
||
821 | |||
822 | return $this->resolved[$option]; |
||
823 | } |
||
824 | |||
825 | // Check whether the option is set at all |
||
826 | if (!isset($this->defaults[$option]) && ! array_key_exists($option, $this->defaults)) { |
||
827 | if (!isset($this->defined[$option])) { |
||
828 | throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); |
||
829 | } |
||
830 | |||
831 | throw new NoSuchOptionException(sprintf('The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', $option)); |
||
832 | } |
||
833 | |||
834 | $value = $this->defaults[$option]; |
||
835 | |||
836 | // Resolve the option if it is a nested definition |
||
837 | if (isset($this->nested[$option])) { |
||
838 | // If the closure is already being called, we have a cyclic dependency |
||
839 | if (isset($this->calling[$option])) { |
||
840 | throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling)))); |
||
841 | } |
||
842 | |||
843 | if (! is_array($value)) { |
||
844 | throw new InvalidOptionsException(sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $option, $this->formatValue($value), $this->formatTypeOf($value))); |
||
845 | } |
||
846 | |||
847 | // The following section must be protected from cyclic calls. |
||
848 | $this->calling[$option] = true; |
||
849 | try { |
||
850 | $resolver = new self(); |
||
851 | foreach ($this->nested[$option] as $closure) { |
||
852 | $closure($resolver, $this); |
||
853 | } |
||
854 | $value = $resolver->resolve($value); |
||
855 | } finally { |
||
856 | unset($this->calling[$option]); |
||
857 | } |
||
858 | } |
||
859 | |||
860 | // Resolve the option if the default value is lazily evaluated |
||
861 | if (isset($this->lazy[$option])) { |
||
862 | // If the closure is already being called, we have a cyclic |
||
863 | // dependency |
||
864 | if (isset($this->calling[$option])) { |
||
865 | throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling)))); |
||
866 | } |
||
867 | |||
868 | // The following section must be protected from cyclic |
||
869 | // calls. Set $calling for the current $option to detect a cyclic |
||
870 | // dependency |
||
871 | // BEGIN |
||
872 | $this->calling[$option] = true; |
||
873 | try { |
||
874 | foreach ($this->lazy[$option] as $closure) { |
||
875 | $value = $closure($this, $value); |
||
876 | } |
||
877 | } finally { |
||
878 | unset($this->calling[$option]); |
||
879 | } |
||
880 | // END |
||
881 | } |
||
882 | |||
883 | // Validate the type of the resolved option |
||
884 | if (isset($this->allowedTypes[$option])) { |
||
885 | $valid = false; |
||
886 | $invalidTypes = []; |
||
887 | |||
888 | foreach ($this->allowedTypes[$option] as $type) { |
||
889 | $type = self::$typeAliases[$type] ?? $type; |
||
890 | |||
891 | if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) { |
||
892 | break; |
||
893 | } |
||
894 | } |
||
895 | |||
896 | if (!$valid) { |
||
897 | $keys = array_keys($invalidTypes); |
||
898 | |||
899 | if (1 === count($keys) && '[]' === substr($keys[0], -2)) { |
||
900 | throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0])); |
||
901 | } |
||
902 | |||
903 | throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), implode('|', array_keys($invalidTypes)))); |
||
904 | } |
||
905 | } |
||
906 | |||
907 | // Validate the value of the resolved option |
||
908 | if (isset($this->allowedValues[$option])) { |
||
909 | $success = false; |
||
910 | $printableAllowedValues = []; |
||
911 | |||
912 | foreach ($this->allowedValues[$option] as $allowedValue) { |
||
913 | if ($allowedValue instanceof Closure) { |
||
914 | if ($allowedValue($value)) { |
||
915 | $success = true; |
||
916 | break; |
||
917 | } |
||
918 | |||
919 | // Don't include closures in the exception message |
||
920 | continue; |
||
921 | } |
||
922 | |||
923 | if ($value === $allowedValue) { |
||
924 | $success = true; |
||
925 | break; |
||
926 | } |
||
927 | |||
928 | $printableAllowedValues[] = $allowedValue; |
||
929 | } |
||
930 | |||
931 | if (!$success) { |
||
932 | $message = sprintf( |
||
933 | 'The option "%s" with value %s is invalid.', |
||
934 | $option, |
||
935 | $this->formatValue($value) |
||
936 | ); |
||
937 | |||
938 | if (count($printableAllowedValues) > 0) { |
||
939 | $message .= sprintf( |
||
940 | ' Accepted values are: %s.', |
||
941 | $this->formatValues($printableAllowedValues) |
||
942 | ); |
||
943 | } |
||
944 | |||
945 | throw new InvalidOptionsException($message); |
||
946 | } |
||
947 | } |
||
948 | |||
949 | // Check whether the option is deprecated |
||
950 | // and it is provided by the user or is being called from a lazy evaluation |
||
951 | if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && is_string($this->deprecated[$option])))) { |
||
952 | $deprecationMessage = $this->deprecated[$option]; |
||
953 | |||
954 | if ($deprecationMessage instanceof Closure) { |
||
955 | // If the closure is already being called, we have a cyclic dependency |
||
956 | if (isset($this->calling[$option])) { |
||
957 | throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling)))); |
||
958 | } |
||
959 | |||
960 | $this->calling[$option] = true; |
||
961 | try { |
||
962 | if (! is_string($deprecationMessage = $deprecationMessage($this, $value))) { |
||
963 | throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', gettype($deprecationMessage))); |
||
964 | } |
||
965 | } finally { |
||
966 | unset($this->calling[$option]); |
||
967 | } |
||
968 | } |
||
969 | |||
970 | if ('' !== $deprecationMessage) { |
||
971 | @trigger_error(strtr($deprecationMessage, ['%name%' => $option]), E_USER_DEPRECATED); |
||
972 | } |
||
973 | } |
||
974 | |||
975 | // Normalize the validated option |
||
976 | if (isset($this->normalizers[$option])) { |
||
977 | // If the closure is already being called, we have a cyclic |
||
978 | // dependency |
||
979 | if (isset($this->calling[$option])) { |
||
980 | throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling)))); |
||
981 | } |
||
982 | |||
983 | $normalizer = $this->normalizers[$option]; |
||
984 | |||
985 | // The following section must be protected from cyclic |
||
986 | // calls. Set $calling for the current $option to detect a cyclic |
||
987 | // dependency |
||
988 | // BEGIN |
||
989 | $this->calling[$option] = true; |
||
990 | try { |
||
991 | $value = $normalizer($this, $value); |
||
992 | } finally { |
||
993 | unset($this->calling[$option]); |
||
994 | } |
||
995 | // END |
||
996 | } |
||
997 | |||
998 | // Mark as resolved |
||
999 | $this->resolved[$option] = $value; |
||
1000 | |||
1001 | return $value; |
||
1002 | } |
||
1003 | |||
1004 | private function verifyTypes(string $type, $value, array &$invalidTypes, int $level = 0): bool |
||
1005 | { |
||
1006 | if (is_array($value) && '[]' === substr($type, -2)) { |
||
1007 | $type = substr($type, 0, -2); |
||
1008 | |||
1009 | foreach ($value as $val) { |
||
1010 | if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) { |
||
1011 | return false; |
||
1012 | } |
||
1013 | } |
||
1014 | |||
1015 | return true; |
||
1016 | } |
||
1017 | |||
1018 | if (('null' === $type && null === $value) || (function_exists($func = 'is_'.$type) && $func($value)) || $value instanceof $type) { |
||
1019 | return true; |
||
1020 | } |
||
1021 | |||
1022 | if (!$invalidTypes) { |
||
1023 | $suffix = ''; |
||
1024 | while (strlen($suffix) < $level * 2) { |
||
1025 | $suffix .= '[]'; |
||
1026 | } |
||
1027 | $invalidTypes[$this->formatTypeOf($value).$suffix] = true; |
||
1028 | } |
||
1029 | |||
1030 | return false; |
||
1031 | } |
||
1032 | |||
1033 | /** |
||
1034 | * Returns whether a resolved option with the given name exists. |
||
1035 | * |
||
1036 | * @param string $option The option name |
||
1037 | * |
||
1038 | * @return bool Whether the option is set |
||
1039 | * |
||
1040 | * @throws AccessException If accessing this method outside of {@link resolve()} |
||
1041 | * |
||
1042 | * @see \ArrayAccess::offsetExists() |
||
1043 | */ |
||
1044 | public function offsetExists($option) |
||
1045 | { |
||
1046 | if (!$this->locked) { |
||
1047 | throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); |
||
1048 | } |
||
1049 | |||
1050 | return array_key_exists($option, $this->defaults); |
||
1051 | } |
||
1052 | |||
1053 | /** |
||
1054 | * Not supported. |
||
1055 | * |
||
1056 | * @throws AccessException |
||
1057 | */ |
||
1058 | public function offsetSet($option, $value) |
||
1059 | { |
||
1060 | throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); |
||
1061 | } |
||
1062 | |||
1063 | /** |
||
1064 | * Not supported. |
||
1065 | * |
||
1066 | * @throws AccessException |
||
1067 | */ |
||
1068 | public function offsetUnset($option) |
||
1069 | { |
||
1070 | throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); |
||
1071 | } |
||
1072 | |||
1073 | /** |
||
1074 | * Returns the number of set options. |
||
1075 | * |
||
1076 | * This may be only a subset of the defined options. |
||
1077 | * |
||
1078 | * @return int Number of options |
||
1079 | * |
||
1080 | * @throws AccessException If accessing this method outside of {@link resolve()} |
||
1081 | * |
||
1082 | * @see \Countable::count() |
||
1083 | */ |
||
1084 | public function count() |
||
1085 | { |
||
1086 | if (!$this->locked) { |
||
1087 | throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); |
||
1088 | } |
||
1089 | |||
1090 | return count($this->defaults); |
||
1091 | } |
||
1092 | |||
1093 | /** |
||
1094 | * Returns a string representation of the type of the value. |
||
1095 | * |
||
1096 | * @param mixed $value The value to return the type of |
||
1097 | * |
||
1098 | * @return string The type of the value |
||
1099 | */ |
||
1100 | private function formatTypeOf($value): string |
||
1101 | { |
||
1102 | return is_object($value) ? get_class($value) : gettype($value); |
||
1103 | } |
||
1104 | |||
1105 | /** |
||
1106 | * Returns a string representation of the value. |
||
1107 | * |
||
1108 | * This method returns the equivalent PHP tokens for most scalar types |
||
1109 | * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped |
||
1110 | * in double quotes ("). |
||
1111 | * |
||
1112 | * @param mixed $value The value to format as string |
||
1113 | */ |
||
1114 | private function formatValue($value): string |
||
1145 | } |
||
1146 | |||
1147 | /** |
||
1148 | * Returns a string representation of a list of values. |
||
1149 | * |
||
1150 | * Each of the values is converted to a string using |
||
1151 | * {@link formatValue()}. The values are then concatenated with commas. |
||
1152 | * |
||
1153 | * @see formatValue() |
||
1154 | */ |
||
1155 | private function formatValues(array $values): string |
||
1156 | { |
||
1162 | } |
||
1163 | } |
||
1164 |