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

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