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

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