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

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