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

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