Skip to content

Commit a56faa1

Browse files
committed
feature #11716 [OptionsResolver] Added a light-weight, low-level API for basic option resolving (webmozart)
This PR was merged into the 2.6-dev branch. Discussion ---------- [OptionsResolver] Added a light-weight, low-level API for basic option resolving | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #11705 | License | MIT | Doc PR | symfony/symfony-docs#4159 See [the updated documentation](https://github.com/webmozart/symfony-docs/blob/issue11705/components/options_resolver.rst) for details on the usage of the simple API. The most important motivation for this change is DX and speed. The basic features of the component should be easily usable in a wide variety of use cases without impacting performance. For DX reasons, I added the static methods to the `Options` class, which makes the code concise and easy to read and understand: ```php use Symfony\Component\OptionsResolver\Options; $options = Options::validateRequired($options, 'format'); $options = Options::validateTypes($options, array( 'format' => array('string', 'int'), 'calendar' => 'int', )); $options = Options::validateValues($options, array( 'calendar' => array( \IntlDateFormatter::GREGORIAN, \IntlDateFormatter::TRADITIONAL, ), )); $options = Options::resolve($options, array( 'format' => null, 'calendar' => \IntlDateFormatter::GREGORIAN, )); ``` If you need to distribute the option configuration, this PR also extracts the configuration part of the `OptionsResolver` class into a new class `OptionsConfig`, which can be passed around. When the configuration is complete, pass the config object to `Options::resolve()` as second argument: ```php $config = new OptionsConfig(); $config->setDefaults(array( 'format' => \IntlDateFormatter::MEDIUM, 'calendar' => \IntlDateFormatter::GREGORIAN, )); $options = Options::resolve($options, $config); ``` Consequently - since `OptionsResolver` extends `OptionsConfig` - the two following statements now become identical: ```php $options = $resolver->resolve($options); $options = Options::resolve($options, $resolver); ``` Commits ------- 9066025 [OptionsResolver] Added a light-weight, low-level API for basic option resolving
2 parents 9e9928a + 4a98533 commit a56faa1

File tree

7 files changed

+1062
-431
lines changed

7 files changed

+1062
-431
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\OptionsResolver\Exception;
13+
14+
/**
15+
* Thrown when an argument is invalid.
16+
*
17+
* @author Bernhard Schussek <[email protected]>
18+
*/
19+
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
20+
{
21+
}

Options.php

Lines changed: 252 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\Component\OptionsResolver;
1313

14+
use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException;
15+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
16+
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
1417
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
1518

1619
/**
@@ -56,6 +59,244 @@ class Options implements \ArrayAccess, \Iterator, \Countable
5659
*/
5760
private $reading = false;
5861

62+
/**
63+
* Merges options with an array of default values and throws an exception if
64+
* any of the options does not exist.
65+
*
66+
* @param array $options A list of option names and
67+
* values
68+
* @param array|Options|OptionsConfig $defaults The accepted options and
69+
* their default values
70+
*
71+
* @return array The merged and validated options
72+
*
73+
* @throws InvalidOptionsException If any of the options is not present in
74+
* the defaults array
75+
* @throws InvalidArgumentException If the defaults are invalid
76+
*
77+
* @since 2.6
78+
*/
79+
public static function resolve(array $options, $defaults)
80+
{
81+
if (is_array($defaults)) {
82+
static::validateNames($options, $defaults, true);
83+
84+
return array_replace($defaults, $options);
85+
}
86+
87+
if ($defaults instanceof self) {
88+
static::validateNames($options, $defaults->options, true);
89+
90+
// Make sure this method can be called multiple times
91+
$combinedOptions = clone $defaults;
92+
93+
// Override options set by the user
94+
foreach ($options as $option => $value) {
95+
$combinedOptions->set($option, $value);
96+
}
97+
98+
// Resolve options
99+
return $combinedOptions->all();
100+
}
101+
102+
if ($defaults instanceof OptionsConfig) {
103+
static::validateNames($options, $defaults->knownOptions, true);
104+
static::validateRequired($options, $defaults->requiredOptions, true);
105+
106+
// Make sure this method can be called multiple times
107+
$combinedOptions = clone $defaults->defaultOptions;
108+
109+
// Override options set by the user
110+
foreach ($options as $option => $value) {
111+
$combinedOptions->set($option, $value);
112+
}
113+
114+
// Resolve options
115+
$resolvedOptions = $combinedOptions->all();
116+
117+
static::validateTypes($resolvedOptions, $defaults->allowedTypes);
118+
static::validateValues($resolvedOptions, $defaults->allowedValues);
119+
120+
return $resolvedOptions;
121+
}
122+
123+
throw new InvalidArgumentException('The second argument is expected to be given as array, Options instance or OptionsConfig instance.');
124+
}
125+
126+
/**
127+
* Validates that the given option names exist and throws an exception
128+
* otherwise.
129+
*
130+
* @param array $options A list of option names and values
131+
* @param string|array $acceptedOptions The accepted option(s), either passed
132+
* as single string or in the values of
133+
* the given array
134+
* @param bool $namesAsKeys If set to true, the option names
135+
* should be passed in the keys of the
136+
* accepted options array
137+
*
138+
* @throws InvalidOptionsException If any of the options is not present in
139+
* the accepted options
140+
*
141+
* @since 2.6
142+
*/
143+
public static function validateNames(array $options, $acceptedOptions, $namesAsKeys = false)
144+
{
145+
$acceptedOptions = (array) $acceptedOptions;
146+
147+
if (!$namesAsKeys) {
148+
$acceptedOptions = array_flip($acceptedOptions);
149+
}
150+
151+
$diff = array_diff_key($options, $acceptedOptions);
152+
153+
if (count($diff) > 0) {
154+
ksort($acceptedOptions);
155+
ksort($diff);
156+
157+
throw new InvalidOptionsException(sprintf(
158+
(count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Known options are: "%s"',
159+
implode('", "', array_keys($diff)),
160+
implode('", "', array_keys($acceptedOptions))
161+
));
162+
}
163+
}
164+
165+
/**
166+
* Validates that the required options are given and throws an exception
167+
* otherwise.
168+
*
169+
* The option names may be any strings that don't consist exclusively of
170+
* digits. For example, "case1" is a valid option name, "1" is not.
171+
*
172+
* @param array $options A list of option names and values
173+
* @param string|array $requiredOptions The required option(s), either
174+
* passed as single string or in the
175+
* values of the given array
176+
* @param bool $namesAsKeys If set to true, the option names
177+
* should be passed in the keys of the
178+
* required options array
179+
*
180+
* @throws MissingOptionsException If a required option is missing
181+
*
182+
* @since 2.6
183+
*/
184+
public static function validateRequired(array $options, $requiredOptions, $namesAsKeys = false)
185+
{
186+
$requiredOptions = (array) $requiredOptions;
187+
188+
if (!$namesAsKeys) {
189+
$requiredOptions = array_flip($requiredOptions);
190+
}
191+
192+
$diff = array_diff_key($requiredOptions, $options);
193+
194+
if (count($diff) > 0) {
195+
ksort($diff);
196+
197+
throw new MissingOptionsException(sprintf(
198+
count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.',
199+
implode('", "', array_keys($diff))
200+
));
201+
}
202+
}
203+
204+
/**
205+
* Validates that the given options match the accepted types and
206+
* throws an exception otherwise.
207+
*
208+
* Accepted type names are any types for which a native "is_*()" function
209+
* exists. For example, "int" is an acceptable type name and will be checked
210+
* with the "is_int()" function.
211+
*
212+
* Types may also be passed as closures which return true or false.
213+
*
214+
* @param array $options A list of option names and values
215+
* @param array $acceptedTypes A mapping of option names to accepted option
216+
* types. The types may be given as
217+
* string/closure or as array of strings/closures
218+
*
219+
* @throws InvalidOptionsException If any of the types does not match the
220+
* accepted types of the option
221+
*
222+
* @since 2.6
223+
*/
224+
public static function validateTypes(array $options, array $acceptedTypes)
225+
{
226+
foreach ($acceptedTypes as $option => $optionTypes) {
227+
if (!array_key_exists($option, $options)) {
228+
continue;
229+
}
230+
231+
$value = $options[$option];
232+
$optionTypes = (array) $optionTypes;
233+
234+
foreach ($optionTypes as $type) {
235+
$isFunction = 'is_'.$type;
236+
237+
if (function_exists($isFunction) && $isFunction($value)) {
238+
continue 2;
239+
} elseif ($value instanceof $type) {
240+
continue 2;
241+
}
242+
}
243+
244+
$printableValue = is_object($value)
245+
? get_class($value)
246+
: (is_array($value)
247+
? 'Array'
248+
: (string) $value);
249+
250+
throw new InvalidOptionsException(sprintf(
251+
'The option "%s" with value "%s" is expected to be of type "%s"',
252+
$option,
253+
$printableValue,
254+
implode('", "', $optionTypes)
255+
));
256+
}
257+
}
258+
259+
/**
260+
* Validates that the given option values match the accepted values and
261+
* throws an exception otherwise.
262+
*
263+
* @param array $options A list of option names and values
264+
* @param array $acceptedValues A mapping of option names to accepted option
265+
* values. The option values must be given as
266+
* arrays
267+
*
268+
* @throws InvalidOptionsException If any of the values does not match the
269+
* accepted values of the option
270+
*
271+
* @since 2.6
272+
*/
273+
public static function validateValues(array $options, array $acceptedValues)
274+
{
275+
foreach ($acceptedValues as $option => $optionValues) {
276+
if (array_key_exists($option, $options)) {
277+
if (is_array($optionValues) && !in_array($options[$option], $optionValues, true)) {
278+
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $optionValues)));
279+
}
280+
281+
if (is_callable($optionValues) && !call_user_func($optionValues, $options[$option])) {
282+
throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", which it is not valid', $option, $options[$option]));
283+
}
284+
}
285+
}
286+
}
287+
288+
/**
289+
* Constructs a new object with a set of default options.
290+
*
291+
* @param array $options A list of option names and values
292+
*/
293+
public function __construct(array $options = array())
294+
{
295+
foreach ($options as $option => $value) {
296+
$this->set($option, $value);
297+
}
298+
}
299+
59300
/**
60301
* Sets the value of a given option.
61302
*
@@ -179,8 +420,10 @@ public function overload($option, $value)
179420

180421
// If an option is a closure that should be evaluated lazily, store it
181422
// in the "lazy" property.
182-
if ($value instanceof \Closure) {
183-
$reflClosure = new \ReflectionFunction($value);
423+
if (is_callable($value)) {
424+
$reflClosure = is_array($value)
425+
? new \ReflectionMethod($value[0], $value[1])
426+
: new \ReflectionFunction($value);
184427
$params = $reflClosure->getParameters();
185428

186429
if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && __CLASS__ === $class->name) {
@@ -229,11 +472,11 @@ public function get($option)
229472
}
230473

231474
if (isset($this->lazy[$option])) {
232-
$this->resolve($option);
475+
$this->resolveOption($option);
233476
}
234477

235478
if (isset($this->normalizers[$option])) {
236-
$this->normalize($option);
479+
$this->normalizeOption($option);
237480
}
238481

239482
return $this->options[$option];
@@ -306,13 +549,13 @@ public function all()
306549
// Double check, in case the option has already been resolved
307550
// by cascade in the previous cycles
308551
if (isset($this->lazy[$option])) {
309-
$this->resolve($option);
552+
$this->resolveOption($option);
310553
}
311554
}
312555

313556
foreach ($this->normalizers as $option => $normalizer) {
314557
if (isset($this->normalizers[$option])) {
315-
$this->normalize($option);
558+
$this->normalizeOption($option);
316559
}
317560
}
318561

@@ -444,7 +687,7 @@ public function count()
444687
* @throws OptionDefinitionException If the option has a cyclic dependency
445688
* on another option.
446689
*/
447-
private function resolve($option)
690+
private function resolveOption($option)
448691
{
449692
// The code duplication with normalize() exists for performance
450693
// reasons, in order to save a method call.
@@ -464,7 +707,7 @@ private function resolve($option)
464707

465708
$this->lock[$option] = true;
466709
foreach ($this->lazy[$option] as $closure) {
467-
$this->options[$option] = $closure($this, $this->options[$option]);
710+
$this->options[$option] = call_user_func($closure, $this, $this->options[$option]);
468711
}
469712
unset($this->lock[$option]);
470713

@@ -482,7 +725,7 @@ private function resolve($option)
482725
* @throws OptionDefinitionException If the option has a cyclic dependency
483726
* on another option.
484727
*/
485-
private function normalize($option)
728+
private function normalizeOption($option)
486729
{
487730
// The code duplication with resolve() exists for performance
488731
// reasons, in order to save a method call.

0 commit comments

Comments
 (0)