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

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