vendor/symfony/options-resolver/OptionsResolver.php line 896

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