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