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; |
13
|
|
|
|
14
|
|
|
use Symfony\Component\OptionsResolver\Exception\AccessException; |
15
|
|
|
use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException; |
16
|
|
|
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; |
17
|
|
|
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; |
18
|
|
|
use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; |
19
|
|
|
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; |
20
|
|
|
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Validates options and merges them with default values. |
24
|
|
|
* |
25
|
|
|
* @author Bernhard Schussek <[email protected]> |
26
|
|
|
* @author Tobias Schultze <http://tobion.de> |
27
|
|
|
*/ |
28
|
|
|
class OptionsResolver implements Options |
29
|
|
|
{ |
30
|
|
|
private const VALIDATION_FUNCTIONS = [ |
31
|
|
|
'bool' => 'is_bool', |
32
|
|
|
'boolean' => 'is_bool', |
33
|
|
|
'int' => 'is_int', |
34
|
|
|
'integer' => 'is_int', |
35
|
|
|
'long' => 'is_int', |
36
|
|
|
'float' => 'is_float', |
37
|
|
|
'double' => 'is_float', |
38
|
|
|
'real' => 'is_float', |
39
|
|
|
'numeric' => 'is_numeric', |
40
|
|
|
'string' => 'is_string', |
41
|
|
|
'scalar' => 'is_scalar', |
42
|
|
|
'array' => 'is_array', |
43
|
|
|
'iterable' => 'is_iterable', |
44
|
|
|
'countable' => 'is_countable', |
45
|
|
|
'callable' => 'is_callable', |
46
|
|
|
'object' => 'is_object', |
47
|
|
|
'resource' => 'is_resource', |
48
|
|
|
]; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* The names of all defined options. |
52
|
|
|
*/ |
53
|
|
|
private $defined = []; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The default option values. |
57
|
|
|
*/ |
58
|
|
|
private $defaults = []; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* A list of closure for nested options. |
62
|
|
|
* |
63
|
|
|
* @var \Closure[][] |
64
|
|
|
*/ |
65
|
|
|
private $nested = []; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* The names of required options. |
69
|
|
|
*/ |
70
|
|
|
private $required = []; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* The resolved option values. |
74
|
|
|
*/ |
75
|
|
|
private $resolved = []; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* A list of normalizer closures. |
79
|
|
|
* |
80
|
|
|
* @var \Closure[][] |
81
|
|
|
*/ |
82
|
|
|
private $normalizers = []; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* A list of accepted values for each option. |
86
|
|
|
*/ |
87
|
|
|
private $allowedValues = []; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* A list of accepted types for each option. |
91
|
|
|
*/ |
92
|
|
|
private $allowedTypes = []; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* A list of info messages for each option. |
96
|
|
|
*/ |
97
|
|
|
private $info = []; |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* A list of closures for evaluating lazy options. |
101
|
|
|
*/ |
102
|
|
|
private $lazy = []; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* A list of lazy options whose closure is currently being called. |
106
|
|
|
* |
107
|
|
|
* This list helps detecting circular dependencies between lazy options. |
108
|
|
|
*/ |
109
|
|
|
private $calling = []; |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* A list of deprecated options. |
113
|
|
|
*/ |
114
|
|
|
private $deprecated = []; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* The list of options provided by the user. |
118
|
|
|
*/ |
119
|
|
|
private $given = []; |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Whether the instance is locked for reading. |
123
|
|
|
* |
124
|
|
|
* Once locked, the options cannot be changed anymore. This is |
125
|
|
|
* necessary in order to avoid inconsistencies during the resolving |
126
|
|
|
* process. If any option is changed after being read, all evaluated |
127
|
|
|
* lazy options that depend on this option would become invalid. |
128
|
|
|
*/ |
129
|
|
|
private $locked = false; |
130
|
|
|
|
131
|
|
|
private $parentsOptions = []; |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Whether the whole options definition is marked as array prototype. |
135
|
|
|
*/ |
136
|
|
|
private $prototype; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* The prototype array's index that is being read. |
140
|
|
|
*/ |
141
|
|
|
private $prototypeIndex; |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Sets the default value of a given option. |
145
|
|
|
* |
146
|
|
|
* If the default value should be set based on other options, you can pass |
147
|
|
|
* a closure with the following signature: |
148
|
|
|
* |
149
|
|
|
* function (Options $options) { |
150
|
|
|
* // ... |
151
|
|
|
* } |
152
|
|
|
* |
153
|
|
|
* The closure will be evaluated when {@link resolve()} is called. The |
154
|
|
|
* closure has access to the resolved values of other options through the |
155
|
|
|
* passed {@link Options} instance: |
156
|
|
|
* |
157
|
|
|
* function (Options $options) { |
158
|
|
|
* if (isset($options['port'])) { |
159
|
|
|
* // ... |
160
|
|
|
* } |
161
|
|
|
* } |
162
|
|
|
* |
163
|
|
|
* If you want to access the previously set default value, add a second |
164
|
|
|
* argument to the closure's signature: |
165
|
|
|
* |
166
|
|
|
* $options->setDefault('name', 'Default Name'); |
167
|
|
|
* |
168
|
|
|
* $options->setDefault('name', function (Options $options, $previousValue) { |
169
|
|
|
* // 'Default Name' === $previousValue |
170
|
|
|
* }); |
171
|
|
|
* |
172
|
|
|
* This is mostly useful if the configuration of the {@link Options} object |
173
|
|
|
* is spread across different locations of your code, such as base and |
174
|
|
|
* sub-classes. |
175
|
|
|
* |
176
|
|
|
* If you want to define nested options, you can pass a closure with the |
177
|
|
|
* following signature: |
178
|
|
|
* |
179
|
|
|
* $options->setDefault('database', function (OptionsResolver $resolver) { |
180
|
|
|
* $resolver->setDefined(['dbname', 'host', 'port', 'user', 'pass']); |
181
|
|
|
* } |
182
|
|
|
* |
183
|
|
|
* To get access to the parent options, add a second argument to the closure's |
184
|
|
|
* signature: |
185
|
|
|
* |
186
|
|
|
* function (OptionsResolver $resolver, Options $parent) { |
187
|
|
|
* // 'default' === $parent['connection'] |
188
|
|
|
* } |
189
|
|
|
* |
190
|
|
|
* @param string $option The name of the option |
191
|
|
|
* @param mixed $value The default value of the option |
192
|
|
|
* |
193
|
|
|
* @return $this |
194
|
|
|
* |
195
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
196
|
|
|
*/ |
197
|
|
|
public function setDefault(string $option, $value) |
198
|
|
|
{ |
199
|
|
|
// Setting is not possible once resolving starts, because then lazy |
200
|
|
|
// options could manipulate the state of the object, leading to |
201
|
|
|
// inconsistent results. |
202
|
|
|
if ($this->locked) { |
203
|
|
|
throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// If an option is a closure that should be evaluated lazily, store it |
207
|
|
|
// in the "lazy" property. |
208
|
|
|
if ($value instanceof \Closure) { |
209
|
|
|
$reflClosure = new \ReflectionFunction($value); |
210
|
|
|
$params = $reflClosure->getParameters(); |
211
|
|
|
|
212
|
|
|
if (isset($params[0]) && Options::class === $this->getParameterClassName($params[0])) { |
213
|
|
|
// Initialize the option if no previous value exists |
214
|
|
|
if (!isset($this->defaults[$option])) { |
215
|
|
|
$this->defaults[$option] = null; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// Ignore previous lazy options if the closure has no second parameter |
219
|
|
|
if (!isset($this->lazy[$option]) || !isset($params[1])) { |
220
|
|
|
$this->lazy[$option] = []; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
// Store closure for later evaluation |
224
|
|
|
$this->lazy[$option][] = $value; |
225
|
|
|
$this->defined[$option] = true; |
226
|
|
|
|
227
|
|
|
// Make sure the option is processed and is not nested anymore |
228
|
|
|
unset($this->resolved[$option], $this->nested[$option]); |
229
|
|
|
|
230
|
|
|
return $this; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
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()))) { |
234
|
|
|
// Store closure for later evaluation |
235
|
|
|
$this->nested[$option][] = $value; |
236
|
|
|
$this->defaults[$option] = []; |
237
|
|
|
$this->defined[$option] = true; |
238
|
|
|
|
239
|
|
|
// Make sure the option is processed and is not lazy anymore |
240
|
|
|
unset($this->resolved[$option], $this->lazy[$option]); |
241
|
|
|
|
242
|
|
|
return $this; |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
// This option is not lazy nor nested anymore |
247
|
|
|
unset($this->lazy[$option], $this->nested[$option]); |
248
|
|
|
|
249
|
|
|
// Yet undefined options can be marked as resolved, because we only need |
250
|
|
|
// to resolve options with lazy closures, normalizers or validation |
251
|
|
|
// rules, none of which can exist for undefined options |
252
|
|
|
// If the option was resolved before, update the resolved value |
253
|
|
|
if (!isset($this->defined[$option]) || \array_key_exists($option, $this->resolved)) { |
254
|
|
|
$this->resolved[$option] = $value; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$this->defaults[$option] = $value; |
258
|
|
|
$this->defined[$option] = true; |
259
|
|
|
|
260
|
|
|
return $this; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Sets a list of default values. |
265
|
|
|
* |
266
|
|
|
* @param array $defaults The default values to set |
267
|
|
|
* |
268
|
|
|
* @return $this |
269
|
|
|
* |
270
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
271
|
|
|
*/ |
272
|
|
|
public function setDefaults(array $defaults) |
273
|
|
|
{ |
274
|
|
|
foreach ($defaults as $option => $value) { |
275
|
|
|
$this->setDefault($option, $value); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
return $this; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Returns whether a default value is set for an option. |
283
|
|
|
* |
284
|
|
|
* Returns true if {@link setDefault()} was called for this option. |
285
|
|
|
* An option is also considered set if it was set to null. |
286
|
|
|
* |
287
|
|
|
* @param string $option The option name |
288
|
|
|
* |
289
|
|
|
* @return bool Whether a default value is set |
290
|
|
|
*/ |
291
|
|
|
public function hasDefault(string $option) |
292
|
|
|
{ |
293
|
|
|
return \array_key_exists($option, $this->defaults); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Marks one or more options as required. |
298
|
|
|
* |
299
|
|
|
* @param string|string[] $optionNames One or more option names |
300
|
|
|
* |
301
|
|
|
* @return $this |
302
|
|
|
* |
303
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
304
|
|
|
*/ |
305
|
|
|
public function setRequired($optionNames) |
306
|
|
|
{ |
307
|
|
|
if ($this->locked) { |
308
|
|
|
throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
foreach ((array) $optionNames as $option) { |
312
|
|
|
$this->defined[$option] = true; |
313
|
|
|
$this->required[$option] = true; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
return $this; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Returns whether an option is required. |
321
|
|
|
* |
322
|
|
|
* An option is required if it was passed to {@link setRequired()}. |
323
|
|
|
* |
324
|
|
|
* @param string $option The name of the option |
325
|
|
|
* |
326
|
|
|
* @return bool Whether the option is required |
327
|
|
|
*/ |
328
|
|
|
public function isRequired(string $option) |
329
|
|
|
{ |
330
|
|
|
return isset($this->required[$option]); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Returns the names of all required options. |
335
|
|
|
* |
336
|
|
|
* @return string[] The names of the required options |
337
|
|
|
* |
338
|
|
|
* @see isRequired() |
339
|
|
|
*/ |
340
|
|
|
public function getRequiredOptions() |
341
|
|
|
{ |
342
|
|
|
return array_keys($this->required); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Returns whether an option is missing a default value. |
347
|
|
|
* |
348
|
|
|
* An option is missing if it was passed to {@link setRequired()}, but not |
349
|
|
|
* to {@link setDefault()}. This option must be passed explicitly to |
350
|
|
|
* {@link resolve()}, otherwise an exception will be thrown. |
351
|
|
|
* |
352
|
|
|
* @param string $option The name of the option |
353
|
|
|
* |
354
|
|
|
* @return bool Whether the option is missing |
355
|
|
|
*/ |
356
|
|
|
public function isMissing(string $option) |
357
|
|
|
{ |
358
|
|
|
return isset($this->required[$option]) && !\array_key_exists($option, $this->defaults); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Returns the names of all options missing a default value. |
363
|
|
|
* |
364
|
|
|
* @return string[] The names of the missing options |
365
|
|
|
* |
366
|
|
|
* @see isMissing() |
367
|
|
|
*/ |
368
|
|
|
public function getMissingOptions() |
369
|
|
|
{ |
370
|
|
|
return array_keys(array_diff_key($this->required, $this->defaults)); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Defines a valid option name. |
375
|
|
|
* |
376
|
|
|
* Defines an option name without setting a default value. The option will |
377
|
|
|
* be accepted when passed to {@link resolve()}. When not passed, the |
378
|
|
|
* option will not be included in the resolved options. |
379
|
|
|
* |
380
|
|
|
* @param string|string[] $optionNames One or more option names |
381
|
|
|
* |
382
|
|
|
* @return $this |
383
|
|
|
* |
384
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
385
|
|
|
*/ |
386
|
|
|
public function setDefined($optionNames) |
387
|
|
|
{ |
388
|
|
|
if ($this->locked) { |
389
|
|
|
throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
foreach ((array) $optionNames as $option) { |
393
|
|
|
$this->defined[$option] = true; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
return $this; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Returns whether an option is defined. |
401
|
|
|
* |
402
|
|
|
* Returns true for any option passed to {@link setDefault()}, |
403
|
|
|
* {@link setRequired()} or {@link setDefined()}. |
404
|
|
|
* |
405
|
|
|
* @param string $option The option name |
406
|
|
|
* |
407
|
|
|
* @return bool Whether the option is defined |
408
|
|
|
*/ |
409
|
|
|
public function isDefined(string $option) |
410
|
|
|
{ |
411
|
|
|
return isset($this->defined[$option]); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* Returns the names of all defined options. |
416
|
|
|
* |
417
|
|
|
* @return string[] The names of the defined options |
418
|
|
|
* |
419
|
|
|
* @see isDefined() |
420
|
|
|
*/ |
421
|
|
|
public function getDefinedOptions() |
422
|
|
|
{ |
423
|
|
|
return array_keys($this->defined); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
public function isNested(string $option): bool |
427
|
|
|
{ |
428
|
|
|
return isset($this->nested[$option]); |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Deprecates an option, allowed types or values. |
433
|
|
|
* |
434
|
|
|
* Instead of passing the message, you may also pass a closure with the |
435
|
|
|
* following signature: |
436
|
|
|
* |
437
|
|
|
* function (Options $options, $value): string { |
438
|
|
|
* // ... |
439
|
|
|
* } |
440
|
|
|
* |
441
|
|
|
* The closure receives the value as argument and should return a string. |
442
|
|
|
* Return an empty string to ignore the option deprecation. |
443
|
|
|
* |
444
|
|
|
* The closure is invoked when {@link resolve()} is called. The parameter |
445
|
|
|
* passed to the closure is the value of the option after validating it |
446
|
|
|
* and before normalizing it. |
447
|
|
|
* |
448
|
|
|
* @param string $package The name of the composer package that is triggering the deprecation |
449
|
|
|
* @param string $version The version of the package that introduced the deprecation |
450
|
|
|
* @param string|\Closure $message The deprecation message to use |
451
|
|
|
*/ |
452
|
|
|
public function setDeprecated(string $option/*, string $package, string $version, $message = 'The option "%name%" is deprecated.' */): self |
453
|
|
|
{ |
454
|
|
|
if ($this->locked) { |
455
|
|
|
throw new AccessException('Options cannot be deprecated from a lazy option or normalizer.'); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
if (!isset($this->defined[$option])) { |
459
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
$args = \func_get_args(); |
463
|
|
|
|
464
|
|
|
if (\func_num_args() < 3) { |
465
|
|
|
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__); |
466
|
|
|
|
467
|
|
|
$message = $args[1] ?? 'The option "%name%" is deprecated.'; |
468
|
|
|
$package = $version = ''; |
469
|
|
|
} else { |
470
|
|
|
$package = $args[1]; |
471
|
|
|
$version = $args[2]; |
472
|
|
|
$message = $args[3] ?? 'The option "%name%" is deprecated.'; |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
if (!\is_string($message) && !$message instanceof \Closure) { |
476
|
|
|
throw new InvalidArgumentException(sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', get_debug_type($message))); |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
// ignore if empty string |
480
|
|
|
if ('' === $message) { |
481
|
|
|
return $this; |
482
|
|
|
} |
483
|
|
|
|
484
|
|
|
$this->deprecated[$option] = [ |
485
|
|
|
'package' => $package, |
486
|
|
|
'version' => $version, |
487
|
|
|
'message' => $message, |
488
|
|
|
]; |
489
|
|
|
|
490
|
|
|
// Make sure the option is processed |
491
|
|
|
unset($this->resolved[$option]); |
492
|
|
|
|
493
|
|
|
return $this; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
public function isDeprecated(string $option): bool |
497
|
|
|
{ |
498
|
|
|
return isset($this->deprecated[$option]); |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* Sets the normalizer for an option. |
503
|
|
|
* |
504
|
|
|
* The normalizer should be a closure with the following signature: |
505
|
|
|
* |
506
|
|
|
* function (Options $options, $value) { |
507
|
|
|
* // ... |
508
|
|
|
* } |
509
|
|
|
* |
510
|
|
|
* The closure is invoked when {@link resolve()} is called. The closure |
511
|
|
|
* has access to the resolved values of other options through the passed |
512
|
|
|
* {@link Options} instance. |
513
|
|
|
* |
514
|
|
|
* The second parameter passed to the closure is the value of |
515
|
|
|
* the option. |
516
|
|
|
* |
517
|
|
|
* The resolved option value is set to the return value of the closure. |
518
|
|
|
* |
519
|
|
|
* @param string $option The option name |
520
|
|
|
* @param \Closure $normalizer The normalizer |
521
|
|
|
* |
522
|
|
|
* @return $this |
523
|
|
|
* |
524
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
525
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
526
|
|
|
*/ |
527
|
|
|
public function setNormalizer(string $option, \Closure $normalizer) |
528
|
|
|
{ |
529
|
|
|
if ($this->locked) { |
530
|
|
|
throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
if (!isset($this->defined[$option])) { |
534
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
$this->normalizers[$option] = [$normalizer]; |
538
|
|
|
|
539
|
|
|
// Make sure the option is processed |
540
|
|
|
unset($this->resolved[$option]); |
541
|
|
|
|
542
|
|
|
return $this; |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* Adds a normalizer for an option. |
547
|
|
|
* |
548
|
|
|
* The normalizer should be a closure with the following signature: |
549
|
|
|
* |
550
|
|
|
* function (Options $options, $value): mixed { |
551
|
|
|
* // ... |
552
|
|
|
* } |
553
|
|
|
* |
554
|
|
|
* The closure is invoked when {@link resolve()} is called. The closure |
555
|
|
|
* has access to the resolved values of other options through the passed |
556
|
|
|
* {@link Options} instance. |
557
|
|
|
* |
558
|
|
|
* The second parameter passed to the closure is the value of |
559
|
|
|
* the option. |
560
|
|
|
* |
561
|
|
|
* The resolved option value is set to the return value of the closure. |
562
|
|
|
* |
563
|
|
|
* @param string $option The option name |
564
|
|
|
* @param \Closure $normalizer The normalizer |
565
|
|
|
* @param bool $forcePrepend If set to true, prepend instead of appending |
566
|
|
|
* |
567
|
|
|
* @return $this |
568
|
|
|
* |
569
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
570
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
571
|
|
|
*/ |
572
|
|
|
public function addNormalizer(string $option, \Closure $normalizer, bool $forcePrepend = false): self |
573
|
|
|
{ |
574
|
|
|
if ($this->locked) { |
575
|
|
|
throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
if (!isset($this->defined[$option])) { |
579
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
if ($forcePrepend) { |
583
|
|
|
$this->normalizers[$option] = $this->normalizers[$option] ?? []; |
584
|
|
|
array_unshift($this->normalizers[$option], $normalizer); |
585
|
|
|
} else { |
586
|
|
|
$this->normalizers[$option][] = $normalizer; |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
// Make sure the option is processed |
590
|
|
|
unset($this->resolved[$option]); |
591
|
|
|
|
592
|
|
|
return $this; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* Sets allowed values for an option. |
597
|
|
|
* |
598
|
|
|
* Instead of passing values, you may also pass a closures with the |
599
|
|
|
* following signature: |
600
|
|
|
* |
601
|
|
|
* function ($value) { |
602
|
|
|
* // return true or false |
603
|
|
|
* } |
604
|
|
|
* |
605
|
|
|
* The closure receives the value as argument and should return true to |
606
|
|
|
* accept the value and false to reject the value. |
607
|
|
|
* |
608
|
|
|
* @param string $option The option name |
609
|
|
|
* @param mixed $allowedValues One or more acceptable values/closures |
610
|
|
|
* |
611
|
|
|
* @return $this |
612
|
|
|
* |
613
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
614
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
615
|
|
|
*/ |
616
|
|
|
public function setAllowedValues(string $option, $allowedValues) |
617
|
|
|
{ |
618
|
|
|
if ($this->locked) { |
619
|
|
|
throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
if (!isset($this->defined[$option])) { |
623
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
$this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : [$allowedValues]; |
627
|
|
|
|
628
|
|
|
// Make sure the option is processed |
629
|
|
|
unset($this->resolved[$option]); |
630
|
|
|
|
631
|
|
|
return $this; |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
/** |
635
|
|
|
* Adds allowed values for an option. |
636
|
|
|
* |
637
|
|
|
* The values are merged with the allowed values defined previously. |
638
|
|
|
* |
639
|
|
|
* Instead of passing values, you may also pass a closures with the |
640
|
|
|
* following signature: |
641
|
|
|
* |
642
|
|
|
* function ($value) { |
643
|
|
|
* // return true or false |
644
|
|
|
* } |
645
|
|
|
* |
646
|
|
|
* The closure receives the value as argument and should return true to |
647
|
|
|
* accept the value and false to reject the value. |
648
|
|
|
* |
649
|
|
|
* @param string $option The option name |
650
|
|
|
* @param mixed $allowedValues One or more acceptable values/closures |
651
|
|
|
* |
652
|
|
|
* @return $this |
653
|
|
|
* |
654
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
655
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
656
|
|
|
*/ |
657
|
|
|
public function addAllowedValues(string $option, $allowedValues) |
658
|
|
|
{ |
659
|
|
|
if ($this->locked) { |
660
|
|
|
throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
if (!isset($this->defined[$option])) { |
664
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
if (!\is_array($allowedValues)) { |
668
|
|
|
$allowedValues = [$allowedValues]; |
669
|
|
|
} |
670
|
|
|
|
671
|
|
|
if (!isset($this->allowedValues[$option])) { |
672
|
|
|
$this->allowedValues[$option] = $allowedValues; |
673
|
|
|
} else { |
674
|
|
|
$this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
// Make sure the option is processed |
678
|
|
|
unset($this->resolved[$option]); |
679
|
|
|
|
680
|
|
|
return $this; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
/** |
684
|
|
|
* Sets allowed types for an option. |
685
|
|
|
* |
686
|
|
|
* Any type for which a corresponding is_<type>() function exists is |
687
|
|
|
* acceptable. Additionally, fully-qualified class or interface names may |
688
|
|
|
* be passed. |
689
|
|
|
* |
690
|
|
|
* @param string $option The option name |
691
|
|
|
* @param string|string[] $allowedTypes One or more accepted types |
692
|
|
|
* |
693
|
|
|
* @return $this |
694
|
|
|
* |
695
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
696
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
697
|
|
|
*/ |
698
|
|
|
public function setAllowedTypes(string $option, $allowedTypes) |
699
|
|
|
{ |
700
|
|
|
if ($this->locked) { |
701
|
|
|
throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
if (!isset($this->defined[$option])) { |
705
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
$this->allowedTypes[$option] = (array) $allowedTypes; |
709
|
|
|
|
710
|
|
|
// Make sure the option is processed |
711
|
|
|
unset($this->resolved[$option]); |
712
|
|
|
|
713
|
|
|
return $this; |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
/** |
717
|
|
|
* Adds allowed types for an option. |
718
|
|
|
* |
719
|
|
|
* The types are merged with the allowed types defined previously. |
720
|
|
|
* |
721
|
|
|
* Any type for which a corresponding is_<type>() function exists is |
722
|
|
|
* acceptable. Additionally, fully-qualified class or interface names may |
723
|
|
|
* be passed. |
724
|
|
|
* |
725
|
|
|
* @param string $option The option name |
726
|
|
|
* @param string|string[] $allowedTypes One or more accepted types |
727
|
|
|
* |
728
|
|
|
* @return $this |
729
|
|
|
* |
730
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
731
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
732
|
|
|
*/ |
733
|
|
|
public function addAllowedTypes(string $option, $allowedTypes) |
734
|
|
|
{ |
735
|
|
|
if ($this->locked) { |
736
|
|
|
throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
if (!isset($this->defined[$option])) { |
740
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
741
|
|
|
} |
742
|
|
|
|
743
|
|
|
if (!isset($this->allowedTypes[$option])) { |
744
|
|
|
$this->allowedTypes[$option] = (array) $allowedTypes; |
745
|
|
|
} else { |
746
|
|
|
$this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); |
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
// Make sure the option is processed |
750
|
|
|
unset($this->resolved[$option]); |
751
|
|
|
|
752
|
|
|
return $this; |
753
|
|
|
} |
754
|
|
|
|
755
|
|
|
/** |
756
|
|
|
* Defines an option configurator with the given name. |
757
|
|
|
*/ |
758
|
|
|
public function define(string $option): OptionConfigurator |
759
|
|
|
{ |
760
|
|
|
if (isset($this->defined[$option])) { |
761
|
|
|
throw new OptionDefinitionException(sprintf('The option "%s" is already defined.', $option)); |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
return new OptionConfigurator($option, $this); |
765
|
|
|
} |
766
|
|
|
|
767
|
|
|
/** |
768
|
|
|
* Sets an info message for an option. |
769
|
|
|
* |
770
|
|
|
* @return $this |
771
|
|
|
* |
772
|
|
|
* @throws UndefinedOptionsException If the option is undefined |
773
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
774
|
|
|
*/ |
775
|
|
|
public function setInfo(string $option, string $info): self |
776
|
|
|
{ |
777
|
|
|
if ($this->locked) { |
778
|
|
|
throw new AccessException('The Info message cannot be set from a lazy option or normalizer.'); |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
if (!isset($this->defined[$option])) { |
782
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
783
|
|
|
} |
784
|
|
|
|
785
|
|
|
$this->info[$option] = $info; |
786
|
|
|
|
787
|
|
|
return $this; |
788
|
|
|
} |
789
|
|
|
|
790
|
|
|
/** |
791
|
|
|
* Gets the info message for an option. |
792
|
|
|
*/ |
793
|
|
|
public function getInfo(string $option): ?string |
794
|
|
|
{ |
795
|
|
|
if (!isset($this->defined[$option])) { |
796
|
|
|
throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
797
|
|
|
} |
798
|
|
|
|
799
|
|
|
return $this->info[$option] ?? null; |
800
|
|
|
} |
801
|
|
|
|
802
|
|
|
/** |
803
|
|
|
* Marks the whole options definition as array prototype. |
804
|
|
|
* |
805
|
|
|
* @return $this |
806
|
|
|
* |
807
|
|
|
* @throws AccessException If called from a lazy option, a normalizer or a root definition |
808
|
|
|
*/ |
809
|
|
|
public function setPrototype(bool $prototype): self |
810
|
|
|
{ |
811
|
|
|
if ($this->locked) { |
812
|
|
|
throw new AccessException('The prototype property cannot be set from a lazy option or normalizer.'); |
813
|
|
|
} |
814
|
|
|
|
815
|
|
|
if (null === $this->prototype && $prototype) { |
816
|
|
|
throw new AccessException('The prototype property cannot be set from a root definition.'); |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
$this->prototype = $prototype; |
820
|
|
|
|
821
|
|
|
return $this; |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
public function isPrototype(): bool |
825
|
|
|
{ |
826
|
|
|
return $this->prototype ?? false; |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
/** |
830
|
|
|
* Removes the option with the given name. |
831
|
|
|
* |
832
|
|
|
* Undefined options are ignored. |
833
|
|
|
* |
834
|
|
|
* @param string|string[] $optionNames One or more option names |
835
|
|
|
* |
836
|
|
|
* @return $this |
837
|
|
|
* |
838
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
839
|
|
|
*/ |
840
|
|
|
public function remove($optionNames) |
841
|
|
|
{ |
842
|
|
|
if ($this->locked) { |
843
|
|
|
throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); |
844
|
|
|
} |
845
|
|
|
|
846
|
|
|
foreach ((array) $optionNames as $option) { |
847
|
|
|
unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); |
848
|
|
|
unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option], $this->info[$option]); |
849
|
|
|
} |
850
|
|
|
|
851
|
|
|
return $this; |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
/** |
855
|
|
|
* Removes all options. |
856
|
|
|
* |
857
|
|
|
* @return $this |
858
|
|
|
* |
859
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
860
|
|
|
*/ |
861
|
|
|
public function clear() |
862
|
|
|
{ |
863
|
|
|
if ($this->locked) { |
864
|
|
|
throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); |
865
|
|
|
} |
866
|
|
|
|
867
|
|
|
$this->defined = []; |
868
|
|
|
$this->defaults = []; |
869
|
|
|
$this->nested = []; |
870
|
|
|
$this->required = []; |
871
|
|
|
$this->resolved = []; |
872
|
|
|
$this->lazy = []; |
873
|
|
|
$this->normalizers = []; |
874
|
|
|
$this->allowedTypes = []; |
875
|
|
|
$this->allowedValues = []; |
876
|
|
|
$this->deprecated = []; |
877
|
|
|
$this->info = []; |
878
|
|
|
|
879
|
|
|
return $this; |
880
|
|
|
} |
881
|
|
|
|
882
|
|
|
/** |
883
|
|
|
* Merges options with the default values stored in the container and |
884
|
|
|
* validates them. |
885
|
|
|
* |
886
|
|
|
* Exceptions are thrown if: |
887
|
|
|
* |
888
|
|
|
* - Undefined options are passed; |
889
|
|
|
* - Required options are missing; |
890
|
|
|
* - Options have invalid types; |
891
|
|
|
* - Options have invalid values. |
892
|
|
|
* |
893
|
|
|
* @param array $options A map of option names to values |
894
|
|
|
* |
895
|
|
|
* @return array The merged and validated options |
896
|
|
|
* |
897
|
|
|
* @throws UndefinedOptionsException If an option name is undefined |
898
|
|
|
* @throws InvalidOptionsException If an option doesn't fulfill the |
899
|
|
|
* specified validation rules |
900
|
|
|
* @throws MissingOptionsException If a required option is missing |
901
|
|
|
* @throws OptionDefinitionException If there is a cyclic dependency between |
902
|
|
|
* lazy options and/or normalizers |
903
|
|
|
* @throws NoSuchOptionException If a lazy option reads an unavailable option |
904
|
|
|
* @throws AccessException If called from a lazy option or normalizer |
905
|
|
|
*/ |
906
|
|
|
public function resolve(array $options = []) |
907
|
|
|
{ |
908
|
|
|
if ($this->locked) { |
909
|
|
|
throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
// Allow this method to be called multiple times |
913
|
|
|
$clone = clone $this; |
914
|
|
|
|
915
|
|
|
// Make sure that no unknown options are passed |
916
|
|
|
$diff = array_diff_key($options, $clone->defined); |
917
|
|
|
|
918
|
|
|
if (\count($diff) > 0) { |
919
|
|
|
ksort($clone->defined); |
920
|
|
|
ksort($diff); |
921
|
|
|
|
922
|
|
|
throw new UndefinedOptionsException(sprintf((\count($diff) > 1 ? '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)))); |
923
|
|
|
} |
924
|
|
|
|
925
|
|
|
// Override options set by the user |
926
|
|
|
foreach ($options as $option => $value) { |
927
|
|
|
$clone->given[$option] = true; |
928
|
|
|
$clone->defaults[$option] = $value; |
929
|
|
|
unset($clone->resolved[$option], $clone->lazy[$option]); |
930
|
|
|
} |
931
|
|
|
|
932
|
|
|
// Check whether any required option is missing |
933
|
|
|
$diff = array_diff_key($clone->required, $clone->defaults); |
934
|
|
|
|
935
|
|
|
if (\count($diff) > 0) { |
936
|
|
|
ksort($diff); |
937
|
|
|
|
938
|
|
|
throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', $this->formatOptions(array_keys($diff)))); |
939
|
|
|
} |
940
|
|
|
|
941
|
|
|
// Lock the container |
942
|
|
|
$clone->locked = true; |
943
|
|
|
|
944
|
|
|
// Now process the individual options. Use offsetGet(), which resolves |
945
|
|
|
// the option itself and any options that the option depends on |
946
|
|
|
foreach ($clone->defaults as $option => $_) { |
947
|
|
|
$clone->offsetGet($option); |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
return $clone->resolved; |
951
|
|
|
} |
952
|
|
|
|
953
|
|
|
/** |
954
|
|
|
* Returns the resolved value of an option. |
955
|
|
|
* |
956
|
|
|
* @param string $option The option name |
957
|
|
|
* @param bool $triggerDeprecation Whether to trigger the deprecation or not (true by default) |
958
|
|
|
* |
959
|
|
|
* @return mixed The option value |
960
|
|
|
* |
961
|
|
|
* @throws AccessException If accessing this method outside of |
962
|
|
|
* {@link resolve()} |
963
|
|
|
* @throws NoSuchOptionException If the option is not set |
964
|
|
|
* @throws InvalidOptionsException If the option doesn't fulfill the |
965
|
|
|
* specified validation rules |
966
|
|
|
* @throws OptionDefinitionException If there is a cyclic dependency between |
967
|
|
|
* lazy options and/or normalizers |
968
|
|
|
*/ |
969
|
|
|
public function offsetGet($option, bool $triggerDeprecation = true) |
970
|
|
|
{ |
971
|
|
|
if (!$this->locked) { |
972
|
|
|
throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); |
973
|
|
|
} |
974
|
|
|
|
975
|
|
|
// Shortcut for resolved options |
976
|
|
|
if (isset($this->resolved[$option]) || \array_key_exists($option, $this->resolved)) { |
977
|
|
|
if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || $this->calling) && \is_string($this->deprecated[$option]['message'])) { |
|
|
|
|
978
|
|
|
trigger_deprecation($this->deprecated[$option]['package'], $this->deprecated[$option]['version'], strtr($this->deprecated[$option]['message'], ['%name%' => $option])); |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
return $this->resolved[$option]; |
982
|
|
|
} |
983
|
|
|
|
984
|
|
|
// Check whether the option is set at all |
985
|
|
|
if (!isset($this->defaults[$option]) && !\array_key_exists($option, $this->defaults)) { |
986
|
|
|
if (!isset($this->defined[$option])) { |
987
|
|
|
throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined)))); |
988
|
|
|
} |
989
|
|
|
|
990
|
|
|
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]))); |
991
|
|
|
} |
992
|
|
|
|
993
|
|
|
$value = $this->defaults[$option]; |
994
|
|
|
|
995
|
|
|
// Resolve the option if it is a nested definition |
996
|
|
|
if (isset($this->nested[$option])) { |
997
|
|
|
// If the closure is already being called, we have a cyclic dependency |
998
|
|
|
if (isset($this->calling[$option])) { |
999
|
|
|
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
|
|
if (!\is_array($value)) { |
1003
|
|
|
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))); |
1004
|
|
|
} |
1005
|
|
|
|
1006
|
|
|
// The following section must be protected from cyclic calls. |
1007
|
|
|
$this->calling[$option] = true; |
1008
|
|
|
try { |
1009
|
|
|
$resolver = new self(); |
1010
|
|
|
$resolver->prototype = false; |
1011
|
|
|
$resolver->parentsOptions = $this->parentsOptions; |
1012
|
|
|
$resolver->parentsOptions[] = $option; |
1013
|
|
|
foreach ($this->nested[$option] as $closure) { |
1014
|
|
|
$closure($resolver, $this); |
1015
|
|
|
} |
1016
|
|
|
|
1017
|
|
|
if ($resolver->prototype) { |
1018
|
|
|
$values = []; |
1019
|
|
|
foreach ($value as $index => $prototypeValue) { |
1020
|
|
|
if (!\is_array($prototypeValue)) { |
1021
|
|
|
throw new InvalidOptionsException(sprintf('The value of the option "%s" is expected to be of type array of array, but is of type array of "%s".', $this->formatOptions([$option]), get_debug_type($prototypeValue))); |
1022
|
|
|
} |
1023
|
|
|
|
1024
|
|
|
$resolver->prototypeIndex = $index; |
1025
|
|
|
$values[$index] = $resolver->resolve($prototypeValue); |
1026
|
|
|
} |
1027
|
|
|
$value = $values; |
1028
|
|
|
} else { |
1029
|
|
|
$value = $resolver->resolve($value); |
1030
|
|
|
} |
1031
|
|
|
} finally { |
1032
|
|
|
$resolver->prototypeIndex = null; |
1033
|
|
|
unset($this->calling[$option]); |
1034
|
|
|
} |
1035
|
|
|
} |
1036
|
|
|
|
1037
|
|
|
// Resolve the option if the default value is lazily evaluated |
1038
|
|
|
if (isset($this->lazy[$option])) { |
1039
|
|
|
// If the closure is already being called, we have a cyclic |
1040
|
|
|
// dependency |
1041
|
|
|
if (isset($this->calling[$option])) { |
1042
|
|
|
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); |
1043
|
|
|
} |
1044
|
|
|
|
1045
|
|
|
// The following section must be protected from cyclic |
1046
|
|
|
// calls. Set $calling for the current $option to detect a cyclic |
1047
|
|
|
// dependency |
1048
|
|
|
// BEGIN |
1049
|
|
|
$this->calling[$option] = true; |
1050
|
|
|
try { |
1051
|
|
|
foreach ($this->lazy[$option] as $closure) { |
1052
|
|
|
$value = $closure($this, $value); |
1053
|
|
|
} |
1054
|
|
|
} finally { |
1055
|
|
|
unset($this->calling[$option]); |
1056
|
|
|
} |
1057
|
|
|
// END |
1058
|
|
|
} |
1059
|
|
|
|
1060
|
|
|
// Validate the type of the resolved option |
1061
|
|
|
if (isset($this->allowedTypes[$option])) { |
1062
|
|
|
$valid = true; |
1063
|
|
|
$invalidTypes = []; |
1064
|
|
|
|
1065
|
|
|
foreach ($this->allowedTypes[$option] as $type) { |
1066
|
|
|
if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) { |
1067
|
|
|
break; |
1068
|
|
|
} |
1069
|
|
|
} |
1070
|
|
|
|
1071
|
|
|
if (!$valid) { |
1072
|
|
|
$fmtActualValue = $this->formatValue($value); |
1073
|
|
|
$fmtAllowedTypes = implode('" or "', $this->allowedTypes[$option]); |
1074
|
|
|
$fmtProvidedTypes = implode('|', array_keys($invalidTypes)); |
1075
|
|
|
$allowedContainsArrayType = \count(array_filter($this->allowedTypes[$option], static function ($item) { |
1076
|
|
|
return '[]' === substr($item, -2); |
1077
|
|
|
})) > 0; |
1078
|
|
|
|
1079
|
|
|
if (\is_array($value) && $allowedContainsArrayType) { |
1080
|
|
|
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)); |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
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)); |
1084
|
|
|
} |
1085
|
|
|
} |
1086
|
|
|
|
1087
|
|
|
// Validate the value of the resolved option |
1088
|
|
|
if (isset($this->allowedValues[$option])) { |
1089
|
|
|
$success = false; |
1090
|
|
|
$printableAllowedValues = []; |
1091
|
|
|
|
1092
|
|
|
foreach ($this->allowedValues[$option] as $allowedValue) { |
1093
|
|
|
if ($allowedValue instanceof \Closure) { |
1094
|
|
|
if ($allowedValue($value)) { |
1095
|
|
|
$success = true; |
1096
|
|
|
break; |
1097
|
|
|
} |
1098
|
|
|
|
1099
|
|
|
// Don't include closures in the exception message |
1100
|
|
|
continue; |
1101
|
|
|
} |
1102
|
|
|
|
1103
|
|
|
if ($value === $allowedValue) { |
1104
|
|
|
$success = true; |
1105
|
|
|
break; |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
$printableAllowedValues[] = $allowedValue; |
1109
|
|
|
} |
1110
|
|
|
|
1111
|
|
|
if (!$success) { |
1112
|
|
|
$message = sprintf( |
1113
|
|
|
'The option "%s" with value %s is invalid.', |
1114
|
|
|
$option, |
1115
|
|
|
$this->formatValue($value) |
1116
|
|
|
); |
1117
|
|
|
|
1118
|
|
|
if (\count($printableAllowedValues) > 0) { |
1119
|
|
|
$message .= sprintf( |
1120
|
|
|
' Accepted values are: %s.', |
1121
|
|
|
$this->formatValues($printableAllowedValues) |
1122
|
|
|
); |
1123
|
|
|
} |
1124
|
|
|
|
1125
|
|
|
if (isset($this->info[$option])) { |
1126
|
|
|
$message .= sprintf(' Info: %s.', $this->info[$option]); |
1127
|
|
|
} |
1128
|
|
|
|
1129
|
|
|
throw new InvalidOptionsException($message); |
1130
|
|
|
} |
1131
|
|
|
} |
1132
|
|
|
|
1133
|
|
|
// Check whether the option is deprecated |
1134
|
|
|
// and it is provided by the user or is being called from a lazy evaluation |
1135
|
|
|
if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && \is_string($this->deprecated[$option]['message'])))) { |
|
|
|
|
1136
|
|
|
$deprecation = $this->deprecated[$option]; |
1137
|
|
|
$message = $this->deprecated[$option]['message']; |
1138
|
|
|
|
1139
|
|
|
if ($message instanceof \Closure) { |
1140
|
|
|
// If the closure is already being called, we have a cyclic dependency |
1141
|
|
|
if (isset($this->calling[$option])) { |
1142
|
|
|
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); |
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
$this->calling[$option] = true; |
1146
|
|
|
try { |
1147
|
|
|
if (!\is_string($message = $message($this, $value))) { |
1148
|
|
|
throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', get_debug_type($message))); |
1149
|
|
|
} |
1150
|
|
|
} finally { |
1151
|
|
|
unset($this->calling[$option]); |
1152
|
|
|
} |
1153
|
|
|
} |
1154
|
|
|
|
1155
|
|
|
if ('' !== $message) { |
1156
|
|
|
trigger_deprecation($deprecation['package'], $deprecation['version'], strtr($message, ['%name%' => $option])); |
1157
|
|
|
} |
1158
|
|
|
} |
1159
|
|
|
|
1160
|
|
|
// Normalize the validated option |
1161
|
|
|
if (isset($this->normalizers[$option])) { |
1162
|
|
|
// If the closure is already being called, we have a cyclic |
1163
|
|
|
// dependency |
1164
|
|
|
if (isset($this->calling[$option])) { |
1165
|
|
|
throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling)))); |
1166
|
|
|
} |
1167
|
|
|
|
1168
|
|
|
// The following section must be protected from cyclic |
1169
|
|
|
// calls. Set $calling for the current $option to detect a cyclic |
1170
|
|
|
// dependency |
1171
|
|
|
// BEGIN |
1172
|
|
|
$this->calling[$option] = true; |
1173
|
|
|
try { |
1174
|
|
|
foreach ($this->normalizers[$option] as $normalizer) { |
1175
|
|
|
$value = $normalizer($this, $value); |
1176
|
|
|
} |
1177
|
|
|
} finally { |
1178
|
|
|
unset($this->calling[$option]); |
1179
|
|
|
} |
1180
|
|
|
// END |
1181
|
|
|
} |
1182
|
|
|
|
1183
|
|
|
// Mark as resolved |
1184
|
|
|
$this->resolved[$option] = $value; |
1185
|
|
|
|
1186
|
|
|
return $value; |
1187
|
|
|
} |
1188
|
|
|
|
1189
|
|
|
private function verifyTypes(string $type, $value, array &$invalidTypes, int $level = 0): bool |
1190
|
|
|
{ |
1191
|
|
|
if (\is_array($value) && '[]' === substr($type, -2)) { |
1192
|
|
|
$type = substr($type, 0, -2); |
1193
|
|
|
$valid = true; |
1194
|
|
|
|
1195
|
|
|
foreach ($value as $val) { |
1196
|
|
|
if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) { |
1197
|
|
|
$valid = false; |
1198
|
|
|
} |
1199
|
|
|
} |
1200
|
|
|
|
1201
|
|
|
return $valid; |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
|
|
if (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) { |
1205
|
|
|
return true; |
1206
|
|
|
} |
1207
|
|
|
|
1208
|
|
|
if (!$invalidTypes || $level > 0) { |
|
|
|
|
1209
|
|
|
$invalidTypes[get_debug_type($value)] = true; |
1210
|
|
|
} |
1211
|
|
|
|
1212
|
|
|
return false; |
1213
|
|
|
} |
1214
|
|
|
|
1215
|
|
|
/** |
1216
|
|
|
* Returns whether a resolved option with the given name exists. |
1217
|
|
|
* |
1218
|
|
|
* @param string $option The option name |
1219
|
|
|
* |
1220
|
|
|
* @return bool Whether the option is set |
1221
|
|
|
* |
1222
|
|
|
* @throws AccessException If accessing this method outside of {@link resolve()} |
1223
|
|
|
* |
1224
|
|
|
* @see \ArrayAccess::offsetExists() |
1225
|
|
|
*/ |
1226
|
|
|
public function offsetExists($option) |
1227
|
|
|
{ |
1228
|
|
|
if (!$this->locked) { |
1229
|
|
|
throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); |
1230
|
|
|
} |
1231
|
|
|
|
1232
|
|
|
return \array_key_exists($option, $this->defaults); |
1233
|
|
|
} |
1234
|
|
|
|
1235
|
|
|
/** |
1236
|
|
|
* Not supported. |
1237
|
|
|
* |
1238
|
|
|
* @throws AccessException |
1239
|
|
|
*/ |
1240
|
|
|
public function offsetSet($option, $value) |
1241
|
|
|
{ |
1242
|
|
|
throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); |
1243
|
|
|
} |
1244
|
|
|
|
1245
|
|
|
/** |
1246
|
|
|
* Not supported. |
1247
|
|
|
* |
1248
|
|
|
* @throws AccessException |
1249
|
|
|
*/ |
1250
|
|
|
public function offsetUnset($option) |
1251
|
|
|
{ |
1252
|
|
|
throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); |
1253
|
|
|
} |
1254
|
|
|
|
1255
|
|
|
/** |
1256
|
|
|
* Returns the number of set options. |
1257
|
|
|
* |
1258
|
|
|
* This may be only a subset of the defined options. |
1259
|
|
|
* |
1260
|
|
|
* @return int Number of options |
1261
|
|
|
* |
1262
|
|
|
* @throws AccessException If accessing this method outside of {@link resolve()} |
1263
|
|
|
* |
1264
|
|
|
* @see \Countable::count() |
1265
|
|
|
*/ |
1266
|
|
|
public function count() |
1267
|
|
|
{ |
1268
|
|
|
if (!$this->locked) { |
1269
|
|
|
throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); |
1270
|
|
|
} |
1271
|
|
|
|
1272
|
|
|
return \count($this->defaults); |
1273
|
|
|
} |
1274
|
|
|
|
1275
|
|
|
/** |
1276
|
|
|
* Returns a string representation of the value. |
1277
|
|
|
* |
1278
|
|
|
* This method returns the equivalent PHP tokens for most scalar types |
1279
|
|
|
* (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped |
1280
|
|
|
* in double quotes ("). |
1281
|
|
|
* |
1282
|
|
|
* @param mixed $value The value to format as string |
1283
|
|
|
*/ |
1284
|
|
|
private function formatValue($value): string |
1285
|
|
|
{ |
1286
|
|
|
if (\is_object($value)) { |
1287
|
|
|
return \get_class($value); |
1288
|
|
|
} |
1289
|
|
|
|
1290
|
|
|
if (\is_array($value)) { |
1291
|
|
|
return 'array'; |
1292
|
|
|
} |
1293
|
|
|
|
1294
|
|
|
if (\is_string($value)) { |
1295
|
|
|
return '"'.$value.'"'; |
1296
|
|
|
} |
1297
|
|
|
|
1298
|
|
|
if (\is_resource($value)) { |
1299
|
|
|
return 'resource'; |
1300
|
|
|
} |
1301
|
|
|
|
1302
|
|
|
if (null === $value) { |
1303
|
|
|
return 'null'; |
1304
|
|
|
} |
1305
|
|
|
|
1306
|
|
|
if (false === $value) { |
1307
|
|
|
return 'false'; |
1308
|
|
|
} |
1309
|
|
|
|
1310
|
|
|
if (true === $value) { |
1311
|
|
|
return 'true'; |
1312
|
|
|
} |
1313
|
|
|
|
1314
|
|
|
return (string) $value; |
1315
|
|
|
} |
1316
|
|
|
|
1317
|
|
|
/** |
1318
|
|
|
* Returns a string representation of a list of values. |
1319
|
|
|
* |
1320
|
|
|
* Each of the values is converted to a string using |
1321
|
|
|
* {@link formatValue()}. The values are then concatenated with commas. |
1322
|
|
|
* |
1323
|
|
|
* @see formatValue() |
1324
|
|
|
*/ |
1325
|
|
|
private function formatValues(array $values): string |
1326
|
|
|
{ |
1327
|
|
|
foreach ($values as $key => $value) { |
1328
|
|
|
$values[$key] = $this->formatValue($value); |
1329
|
|
|
} |
1330
|
|
|
|
1331
|
|
|
return implode(', ', $values); |
1332
|
|
|
} |
1333
|
|
|
|
1334
|
|
|
private function formatOptions(array $options): string |
1335
|
|
|
{ |
1336
|
|
|
if ($this->parentsOptions) { |
|
|
|
|
1337
|
|
|
$prefix = array_shift($this->parentsOptions); |
1338
|
|
|
if ($this->parentsOptions) { |
1339
|
|
|
$prefix .= sprintf('[%s]', implode('][', $this->parentsOptions)); |
1340
|
|
|
} |
1341
|
|
|
|
1342
|
|
|
if ($this->prototype && null !== $this->prototypeIndex) { |
1343
|
|
|
$prefix .= sprintf('[%s]', $this->prototypeIndex); |
1344
|
|
|
} |
1345
|
|
|
|
1346
|
|
|
$options = array_map(static function (string $option) use ($prefix): string { |
1347
|
|
|
return sprintf('%s[%s]', $prefix, $option); |
1348
|
|
|
}, $options); |
1349
|
|
|
} |
1350
|
|
|
|
1351
|
|
|
return implode('", "', $options); |
1352
|
|
|
} |
1353
|
|
|
|
1354
|
|
|
private function getParameterClassName(\ReflectionParameter $parameter): ?string |
1355
|
|
|
{ |
1356
|
|
|
if (!($type = $parameter->getType()) instanceof \ReflectionNamedType || $type->isBuiltin()) { |
1357
|
|
|
return null; |
1358
|
|
|
} |
1359
|
|
|
|
1360
|
|
|
return $type->getName(); |
1361
|
|
|
} |
1362
|
|
|
} |
1363
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.