Passed
Push — master ( 90372d...e80252 )
by Nikolay
25:24
created

OptionsResolver::setDefault()   C

Complexity

Conditions 17
Paths 10

Size

Total Lines 64
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 64
c 0
b 0
f 0
rs 5.2166
cc 17
nc 10
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 function array_key_exists;
15
use Closure;
16
use function count;
17
use function func_get_arg;
18
use function func_num_args;
19
use function function_exists;
20
use function get_class;
21
use function gettype;
22
use function is_array;
23
use function is_object;
24
use function is_resource;
25
use function is_string;
26
use ReflectionFunction;
27
use function strlen;
28
use Symfony\Component\OptionsResolver\Exception\AccessException;
29
use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException;
30
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
31
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
32
use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException;
33
use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
34
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
35
36
/**
37
 * Validates options and merges them with default values.
38
 *
39
 * @author Bernhard Schussek <[email protected]>
40
 * @author Tobias Schultze <http://tobion.de>
41
 */
42
class OptionsResolver implements Options
43
{
44
    /**
45
     * The names of all defined options.
46
     */
47
    private $defined = [];
48
49
    /**
50
     * The default option values.
51
     */
52
    private $defaults = [];
53
54
    /**
55
     * A list of closure for nested options.
56
     *
57
     * @var \Closure[][]
58
     */
59
    private $nested = [];
60
61
    /**
62
     * The names of required options.
63
     */
64
    private $required = [];
65
66
    /**
67
     * The resolved option values.
68
     */
69
    private $resolved = [];
70
71
    /**
72
     * A list of normalizer closures.
73
     *
74
     * @var \Closure[]
75
     */
76
    private $normalizers = [];
77
78
    /**
79
     * A list of accepted values for each option.
80
     */
81
    private $allowedValues = [];
82
83
    /**
84
     * A list of accepted types for each option.
85
     */
86
    private $allowedTypes = [];
87
88
    /**
89
     * A list of closures for evaluating lazy options.
90
     */
91
    private $lazy = [];
92
93
    /**
94
     * A list of lazy options whose closure is currently being called.
95
     *
96
     * This list helps detecting circular dependencies between lazy options.
97
     */
98
    private $calling = [];
99
100
    /**
101
     * A list of deprecated options.
102
     */
103
    private $deprecated = [];
104
105
    /**
106
     * The list of options provided by the user.
107
     */
108
    private $given = [];
109
110
    /**
111
     * Whether the instance is locked for reading.
112
     *
113
     * Once locked, the options cannot be changed anymore. This is
114
     * necessary in order to avoid inconsistencies during the resolving
115
     * process. If any option is changed after being read, all evaluated
116
     * lazy options that depend on this option would become invalid.
117
     */
118
    private $locked = false;
119
120
    private static $typeAliases = [
121
        'boolean' => 'bool',
122
        'integer' => 'int',
123
        'double' => 'float',
124
    ];
125
126
    /**
127
     * Sets the default value of a given option.
128
     *
129
     * If the default value should be set based on other options, you can pass
130
     * a closure with the following signature:
131
     *
132
     *     function (Options $options) {
133
     *         // ...
134
     *     }
135
     *
136
     * The closure will be evaluated when {@link resolve()} is called. The
137
     * closure has access to the resolved values of other options through the
138
     * passed {@link Options} instance:
139
     *
140
     *     function (Options $options) {
141
     *         if (isset($options['port'])) {
142
     *             // ...
143
     *         }
144
     *     }
145
     *
146
     * If you want to access the previously set default value, add a second
147
     * argument to the closure's signature:
148
     *
149
     *     $options->setDefault('name', 'Default Name');
150
     *
151
     *     $options->setDefault('name', function (Options $options, $previousValue) {
152
     *         // 'Default Name' === $previousValue
153
     *     });
154
     *
155
     * This is mostly useful if the configuration of the {@link Options} object
156
     * is spread across different locations of your code, such as base and
157
     * sub-classes.
158
     *
159
     * If you want to define nested options, you can pass a closure with the
160
     * following signature:
161
     *
162
     *     $options->setDefault('database', function (OptionsResolver $resolver) {
163
     *         $resolver->setDefined(['dbname', 'host', 'port', 'user', 'pass']);
164
     *     }
165
     *
166
     * To get access to the parent options, add a second argument to the closure's
167
     * signature:
168
     *
169
     *     function (OptionsResolver $resolver, Options $parent) {
170
     *         // 'default' === $parent['connection']
171
     *     }
172
     *
173
     * @param string $option The name of the option
174
     * @param mixed  $value  The default value of the option
175
     *
176
     * @return $this
177
     *
178
     * @throws AccessException If called from a lazy option or normalizer
179
     */
180
    public function setDefault($option, $value)
181
    {
182
        // Setting is not possible once resolving starts, because then lazy
183
        // options could manipulate the state of the object, leading to
184
        // inconsistent results.
185
        if ($this->locked) {
186
            throw new AccessException('Default values cannot be set from a lazy option or normalizer.');
187
        }
188
189
        // If an option is a closure that should be evaluated lazily, store it
190
        // in the "lazy" property.
191
        if ($value instanceof Closure) {
192
            $reflClosure = new ReflectionFunction($value);
193
            $params = $reflClosure->getParameters();
194
195
            if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && Options::class === $class->name) {
196
                // Initialize the option if no previous value exists
197
                if (!isset($this->defaults[$option])) {
198
                    $this->defaults[$option] = null;
199
                }
200
201
                // Ignore previous lazy options if the closure has no second parameter
202
                if (!isset($this->lazy[$option]) || !isset($params[1])) {
203
                    $this->lazy[$option] = [];
204
                }
205
206
                // Store closure for later evaluation
207
                $this->lazy[$option][] = $value;
208
                $this->defined[$option] = true;
209
210
                // Make sure the option is processed and is not nested anymore
211
                unset($this->resolved[$option], $this->nested[$option]);
212
213
                return $this;
214
            }
215
216
            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))) {
217
                // Store closure for later evaluation
218
                $this->nested[$option][] = $value;
219
                $this->defaults[$option] = [];
220
                $this->defined[$option] = true;
221
222
                // Make sure the option is processed and is not lazy anymore
223
                unset($this->resolved[$option], $this->lazy[$option]);
224
225
                return $this;
226
            }
227
        }
228
229
        // This option is not lazy nor nested anymore
230
        unset($this->lazy[$option], $this->nested[$option]);
231
232
        // Yet undefined options can be marked as resolved, because we only need
233
        // to resolve options with lazy closures, normalizers or validation
234
        // rules, none of which can exist for undefined options
235
        // If the option was resolved before, update the resolved value
236
        if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) {
237
            $this->resolved[$option] = $value;
238
        }
239
240
        $this->defaults[$option] = $value;
241
        $this->defined[$option] = true;
242
243
        return $this;
244
    }
245
246
    /**
247
     * Sets a list of default values.
248
     *
249
     * @param array $defaults The default values to set
250
     *
251
     * @return $this
252
     *
253
     * @throws AccessException If called from a lazy option or normalizer
254
     */
255
    public function setDefaults(array $defaults)
256
    {
257
        foreach ($defaults as $option => $value) {
258
            $this->setDefault($option, $value);
259
        }
260
261
        return $this;
262
    }
263
264
    /**
265
     * Returns whether a default value is set for an option.
266
     *
267
     * Returns true if {@link setDefault()} was called for this option.
268
     * An option is also considered set if it was set to null.
269
     *
270
     * @param string $option The option name
271
     *
272
     * @return bool Whether a default value is set
273
     */
274
    public function hasDefault($option)
275
    {
276
        return array_key_exists($option, $this->defaults);
277
    }
278
279
    /**
280
     * Marks one or more options as required.
281
     *
282
     * @param string|string[] $optionNames One or more option names
283
     *
284
     * @return $this
285
     *
286
     * @throws AccessException If called from a lazy option or normalizer
287
     */
288
    public function setRequired($optionNames)
289
    {
290
        if ($this->locked) {
291
            throw new AccessException('Options cannot be made required from a lazy option or normalizer.');
292
        }
293
294
        foreach ((array) $optionNames as $option) {
295
            $this->defined[$option] = true;
296
            $this->required[$option] = true;
297
        }
298
299
        return $this;
300
    }
301
302
    /**
303
     * Returns whether an option is required.
304
     *
305
     * An option is required if it was passed to {@link setRequired()}.
306
     *
307
     * @param string $option The name of the option
308
     *
309
     * @return bool Whether the option is required
310
     */
311
    public function isRequired($option)
312
    {
313
        return isset($this->required[$option]);
314
    }
315
316
    /**
317
     * Returns the names of all required options.
318
     *
319
     * @return string[] The names of the required options
320
     *
321
     * @see isRequired()
322
     */
323
    public function getRequiredOptions()
324
    {
325
        return array_keys($this->required);
326
    }
327
328
    /**
329
     * Returns whether an option is missing a default value.
330
     *
331
     * An option is missing if it was passed to {@link setRequired()}, but not
332
     * to {@link setDefault()}. This option must be passed explicitly to
333
     * {@link resolve()}, otherwise an exception will be thrown.
334
     *
335
     * @param string $option The name of the option
336
     *
337
     * @return bool Whether the option is missing
338
     */
339
    public function isMissing($option)
340
    {
341
        return isset($this->required[$option]) && ! array_key_exists($option, $this->defaults);
342
    }
343
344
    /**
345
     * Returns the names of all options missing a default value.
346
     *
347
     * @return string[] The names of the missing options
348
     *
349
     * @see isMissing()
350
     */
351
    public function getMissingOptions()
352
    {
353
        return array_keys(array_diff_key($this->required, $this->defaults));
354
    }
355
356
    /**
357
     * Defines a valid option name.
358
     *
359
     * Defines an option name without setting a default value. The option will
360
     * be accepted when passed to {@link resolve()}. When not passed, the
361
     * option will not be included in the resolved options.
362
     *
363
     * @param string|string[] $optionNames One or more option names
364
     *
365
     * @return $this
366
     *
367
     * @throws AccessException If called from a lazy option or normalizer
368
     */
369
    public function setDefined($optionNames)
370
    {
371
        if ($this->locked) {
372
            throw new AccessException('Options cannot be defined from a lazy option or normalizer.');
373
        }
374
375
        foreach ((array) $optionNames as $option) {
376
            $this->defined[$option] = true;
377
        }
378
379
        return $this;
380
    }
381
382
    /**
383
     * Returns whether an option is defined.
384
     *
385
     * Returns true for any option passed to {@link setDefault()},
386
     * {@link setRequired()} or {@link setDefined()}.
387
     *
388
     * @param string $option The option name
389
     *
390
     * @return bool Whether the option is defined
391
     */
392
    public function isDefined($option)
393
    {
394
        return isset($this->defined[$option]);
395
    }
396
397
    /**
398
     * Returns the names of all defined options.
399
     *
400
     * @return string[] The names of the defined options
401
     *
402
     * @see isDefined()
403
     */
404
    public function getDefinedOptions()
405
    {
406
        return array_keys($this->defined);
407
    }
408
409
    public function isNested(string $option): bool
410
    {
411
        return isset($this->nested[$option]);
412
    }
413
414
    /**
415
     * Deprecates an option, allowed types or values.
416
     *
417
     * Instead of passing the message, you may also pass a closure with the
418
     * following signature:
419
     *
420
     *     function (Options $options, $value): string {
421
     *         // ...
422
     *     }
423
     *
424
     * The closure receives the value as argument and should return a string.
425
     * Return an empty string to ignore the option deprecation.
426
     *
427
     * The closure is invoked when {@link resolve()} is called. The parameter
428
     * passed to the closure is the value of the option after validating it
429
     * and before normalizing it.
430
     *
431
     * @param string|\Closure $deprecationMessage
432
     */
433
    public function setDeprecated(string $option, $deprecationMessage = 'The option "%name%" is deprecated.'): self
434
    {
435
        if ($this->locked) {
436
            throw new AccessException('Options cannot be deprecated from a lazy option or normalizer.');
437
        }
438
439
        if (!isset($this->defined[$option])) {
440
            throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
441
        }
442
443
        if (! is_string($deprecationMessage) && !$deprecationMessage instanceof Closure) {
0 ignored issues
show
introduced by
$deprecationMessage is always a sub-type of Closure.
Loading history...
444
            throw new InvalidArgumentException(sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', gettype($deprecationMessage)));
445
        }
446
447
        // ignore if empty string
448
        if ('' === $deprecationMessage) {
449
            return $this;
450
        }
451
452
        $this->deprecated[$option] = $deprecationMessage;
453
454
        // Make sure the option is processed
455
        unset($this->resolved[$option]);
456
457
        return $this;
458
    }
459
460
    public function isDeprecated(string $option): bool
461
    {
462
        return isset($this->deprecated[$option]);
463
    }
464
465
    /**
466
     * Sets the normalizer for an option.
467
     *
468
     * The normalizer should be a closure with the following signature:
469
     *
470
     *     function (Options $options, $value) {
471
     *         // ...
472
     *     }
473
     *
474
     * The closure is invoked when {@link resolve()} is called. The closure
475
     * has access to the resolved values of other options through the passed
476
     * {@link Options} instance.
477
     *
478
     * The second parameter passed to the closure is the value of
479
     * the option.
480
     *
481
     * The resolved option value is set to the return value of the closure.
482
     *
483
     * @param string   $option     The option name
484
     * @param \Closure $normalizer The normalizer
485
     *
486
     * @return $this
487
     *
488
     * @throws UndefinedOptionsException If the option is undefined
489
     * @throws AccessException           If called from a lazy option or normalizer
490
     */
491
    public function setNormalizer($option, Closure $normalizer)
492
    {
493
        if ($this->locked) {
494
            throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.');
495
        }
496
497
        if (!isset($this->defined[$option])) {
498
            throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
499
        }
500
501
        $this->normalizers[$option] = $normalizer;
502
503
        // Make sure the option is processed
504
        unset($this->resolved[$option]);
505
506
        return $this;
507
    }
508
509
    /**
510
     * Sets allowed values for an option.
511
     *
512
     * Instead of passing values, you may also pass a closures with the
513
     * following signature:
514
     *
515
     *     function ($value) {
516
     *         // return true or false
517
     *     }
518
     *
519
     * The closure receives the value as argument and should return true to
520
     * accept the value and false to reject the value.
521
     *
522
     * @param string $option        The option name
523
     * @param mixed  $allowedValues One or more acceptable values/closures
524
     *
525
     * @return $this
526
     *
527
     * @throws UndefinedOptionsException If the option is undefined
528
     * @throws AccessException           If called from a lazy option or normalizer
529
     */
530
    public function setAllowedValues($option, $allowedValues)
531
    {
532
        if ($this->locked) {
533
            throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.');
534
        }
535
536
        if (!isset($this->defined[$option])) {
537
            throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
538
        }
539
540
        $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : [$allowedValues];
541
542
        // Make sure the option is processed
543
        unset($this->resolved[$option]);
544
545
        return $this;
546
    }
547
548
    /**
549
     * Adds allowed values for an option.
550
     *
551
     * The values are merged with the allowed values defined previously.
552
     *
553
     * Instead of passing values, you may also pass a closures with the
554
     * following signature:
555
     *
556
     *     function ($value) {
557
     *         // return true or false
558
     *     }
559
     *
560
     * The closure receives the value as argument and should return true to
561
     * accept the value and false to reject the value.
562
     *
563
     * @param string $option        The option name
564
     * @param mixed  $allowedValues One or more acceptable values/closures
565
     *
566
     * @return $this
567
     *
568
     * @throws UndefinedOptionsException If the option is undefined
569
     * @throws AccessException           If called from a lazy option or normalizer
570
     */
571
    public function addAllowedValues($option, $allowedValues)
572
    {
573
        if ($this->locked) {
574
            throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.');
575
        }
576
577
        if (!isset($this->defined[$option])) {
578
            throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
579
        }
580
581
        if (! is_array($allowedValues)) {
582
            $allowedValues = [$allowedValues];
583
        }
584
585
        if (!isset($this->allowedValues[$option])) {
586
            $this->allowedValues[$option] = $allowedValues;
587
        } else {
588
            $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues);
589
        }
590
591
        // Make sure the option is processed
592
        unset($this->resolved[$option]);
593
594
        return $this;
595
    }
596
597
    /**
598
     * Sets allowed types for an option.
599
     *
600
     * Any type for which a corresponding is_<type>() function exists is
601
     * acceptable. Additionally, fully-qualified class or interface names may
602
     * be passed.
603
     *
604
     * @param string          $option       The option name
605
     * @param string|string[] $allowedTypes One or more accepted types
606
     *
607
     * @return $this
608
     *
609
     * @throws UndefinedOptionsException If the option is undefined
610
     * @throws AccessException           If called from a lazy option or normalizer
611
     */
612
    public function setAllowedTypes($option, $allowedTypes)
613
    {
614
        if ($this->locked) {
615
            throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.');
616
        }
617
618
        if (!isset($this->defined[$option])) {
619
            throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
620
        }
621
622
        $this->allowedTypes[$option] = (array) $allowedTypes;
623
624
        // Make sure the option is processed
625
        unset($this->resolved[$option]);
626
627
        return $this;
628
    }
629
630
    /**
631
     * Adds allowed types for an option.
632
     *
633
     * The types are merged with the allowed types defined previously.
634
     *
635
     * Any type for which a corresponding is_<type>() function exists is
636
     * acceptable. Additionally, fully-qualified class or interface names may
637
     * be passed.
638
     *
639
     * @param string          $option       The option name
640
     * @param string|string[] $allowedTypes One or more accepted types
641
     *
642
     * @return $this
643
     *
644
     * @throws UndefinedOptionsException If the option is undefined
645
     * @throws AccessException           If called from a lazy option or normalizer
646
     */
647
    public function addAllowedTypes($option, $allowedTypes)
648
    {
649
        if ($this->locked) {
650
            throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.');
651
        }
652
653
        if (!isset($this->defined[$option])) {
654
            throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
655
        }
656
657
        if (!isset($this->allowedTypes[$option])) {
658
            $this->allowedTypes[$option] = (array) $allowedTypes;
659
        } else {
660
            $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes);
661
        }
662
663
        // Make sure the option is processed
664
        unset($this->resolved[$option]);
665
666
        return $this;
667
    }
668
669
    /**
670
     * Removes the option with the given name.
671
     *
672
     * Undefined options are ignored.
673
     *
674
     * @param string|string[] $optionNames One or more option names
675
     *
676
     * @return $this
677
     *
678
     * @throws AccessException If called from a lazy option or normalizer
679
     */
680
    public function remove($optionNames)
681
    {
682
        if ($this->locked) {
683
            throw new AccessException('Options cannot be removed from a lazy option or normalizer.');
684
        }
685
686
        foreach ((array) $optionNames as $option) {
687
            unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]);
688
            unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]);
689
        }
690
691
        return $this;
692
    }
693
694
    /**
695
     * Removes all options.
696
     *
697
     * @return $this
698
     *
699
     * @throws AccessException If called from a lazy option or normalizer
700
     */
701
    public function clear()
702
    {
703
        if ($this->locked) {
704
            throw new AccessException('Options cannot be cleared from a lazy option or normalizer.');
705
        }
706
707
        $this->defined = [];
708
        $this->defaults = [];
709
        $this->nested = [];
710
        $this->required = [];
711
        $this->resolved = [];
712
        $this->lazy = [];
713
        $this->normalizers = [];
714
        $this->allowedTypes = [];
715
        $this->allowedValues = [];
716
        $this->deprecated = [];
717
718
        return $this;
719
    }
720
721
    /**
722
     * Merges options with the default values stored in the container and
723
     * validates them.
724
     *
725
     * Exceptions are thrown if:
726
     *
727
     *  - Undefined options are passed;
728
     *  - Required options are missing;
729
     *  - Options have invalid types;
730
     *  - Options have invalid values.
731
     *
732
     * @param array $options A map of option names to values
733
     *
734
     * @return array The merged and validated options
735
     *
736
     * @throws UndefinedOptionsException If an option name is undefined
737
     * @throws InvalidOptionsException   If an option doesn't fulfill the
738
     *                                   specified validation rules
739
     * @throws MissingOptionsException   If a required option is missing
740
     * @throws OptionDefinitionException If there is a cyclic dependency between
741
     *                                   lazy options and/or normalizers
742
     * @throws NoSuchOptionException     If a lazy option reads an unavailable option
743
     * @throws AccessException           If called from a lazy option or normalizer
744
     */
745
    public function resolve(array $options = [])
746
    {
747
        if ($this->locked) {
748
            throw new AccessException('Options cannot be resolved from a lazy option or normalizer.');
749
        }
750
751
        // Allow this method to be called multiple times
752
        $clone = clone $this;
753
754
        // Make sure that no unknown options are passed
755
        $diff = array_diff_key($options, $clone->defined);
756
757
        if (count($diff) > 0) {
758
            ksort($clone->defined);
759
            ksort($diff);
760
761
            throw new UndefinedOptionsException(sprintf((count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', implode('", "', array_keys($diff)), implode('", "', array_keys($clone->defined))));
762
        }
763
764
        // Override options set by the user
765
        foreach ($options as $option => $value) {
766
            $clone->given[$option] = true;
767
            $clone->defaults[$option] = $value;
768
            unset($clone->resolved[$option], $clone->lazy[$option]);
769
        }
770
771
        // Check whether any required option is missing
772
        $diff = array_diff_key($clone->required, $clone->defaults);
773
774
        if (count($diff) > 0) {
775
            ksort($diff);
776
777
            throw new MissingOptionsException(sprintf(count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', implode('", "', array_keys($diff))));
778
        }
779
780
        // Lock the container
781
        $clone->locked = true;
782
783
        // Now process the individual options. Use offsetGet(), which resolves
784
        // the option itself and any options that the option depends on
785
        foreach ($clone->defaults as $option => $_) {
786
            $clone->offsetGet($option);
787
        }
788
789
        return $clone->resolved;
790
    }
791
792
    /**
793
     * Returns the resolved value of an option.
794
     *
795
     * @param string $option             The option name
796
     * @param bool   $triggerDeprecation Whether to trigger the deprecation or not (true by default)
797
     *
798
     * @return mixed The option value
799
     *
800
     * @throws AccessException           If accessing this method outside of
801
     *                                   {@link resolve()}
802
     * @throws NoSuchOptionException     If the option is not set
803
     * @throws InvalidOptionsException   If the option doesn't fulfill the
804
     *                                   specified validation rules
805
     * @throws OptionDefinitionException If there is a cyclic dependency between
806
     *                                   lazy options and/or normalizers
807
     */
808
    public function offsetGet($option/*, bool $triggerDeprecation = true*/)
809
    {
810
        if (!$this->locked) {
811
            throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
812
        }
813
814
        $triggerDeprecation = 1 === func_num_args() || func_get_arg(1);
815
816
        // Shortcut for resolved options
817
        if (isset($this->resolved[$option]) || array_key_exists($option, $this->resolved)) {
818
            if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || $this->calling) && is_string($this->deprecated[$option])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->calling of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
819
                @trigger_error(strtr($this->deprecated[$option], ['%name%' => $option]), E_USER_DEPRECATED);
820
            }
821
822
            return $this->resolved[$option];
823
        }
824
825
        // Check whether the option is set at all
826
        if (!isset($this->defaults[$option]) && ! array_key_exists($option, $this->defaults)) {
827
            if (!isset($this->defined[$option])) {
828
                throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
829
            }
830
831
            throw new NoSuchOptionException(sprintf('The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', $option));
832
        }
833
834
        $value = $this->defaults[$option];
835
836
        // Resolve the option if it is a nested definition
837
        if (isset($this->nested[$option])) {
838
            // If the closure is already being called, we have a cyclic dependency
839
            if (isset($this->calling[$option])) {
840
                throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
841
            }
842
843
            if (! is_array($value)) {
844
                throw new InvalidOptionsException(sprintf('The nested option "%s" with value %s is expected to be of type array, but is of type "%s".', $option, $this->formatValue($value), $this->formatTypeOf($value)));
845
            }
846
847
            // The following section must be protected from cyclic calls.
848
            $this->calling[$option] = true;
849
            try {
850
                $resolver = new self();
851
                foreach ($this->nested[$option] as $closure) {
852
                    $closure($resolver, $this);
853
                }
854
                $value = $resolver->resolve($value);
855
            } finally {
856
                unset($this->calling[$option]);
857
            }
858
        }
859
860
        // Resolve the option if the default value is lazily evaluated
861
        if (isset($this->lazy[$option])) {
862
            // If the closure is already being called, we have a cyclic
863
            // dependency
864
            if (isset($this->calling[$option])) {
865
                throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
866
            }
867
868
            // The following section must be protected from cyclic
869
            // calls. Set $calling for the current $option to detect a cyclic
870
            // dependency
871
            // BEGIN
872
            $this->calling[$option] = true;
873
            try {
874
                foreach ($this->lazy[$option] as $closure) {
875
                    $value = $closure($this, $value);
876
                }
877
            } finally {
878
                unset($this->calling[$option]);
879
            }
880
            // END
881
        }
882
883
        // Validate the type of the resolved option
884
        if (isset($this->allowedTypes[$option])) {
885
            $valid = false;
886
            $invalidTypes = [];
887
888
            foreach ($this->allowedTypes[$option] as $type) {
889
                $type = self::$typeAliases[$type] ?? $type;
890
891
                if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) {
892
                    break;
893
                }
894
            }
895
896
            if (!$valid) {
897
                $keys = array_keys($invalidTypes);
898
899
                if (1 === count($keys) && '[]' === substr($keys[0], -2)) {
900
                    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".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0]));
901
                }
902
903
                throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), implode('|', array_keys($invalidTypes))));
904
            }
905
        }
906
907
        // Validate the value of the resolved option
908
        if (isset($this->allowedValues[$option])) {
909
            $success = false;
910
            $printableAllowedValues = [];
911
912
            foreach ($this->allowedValues[$option] as $allowedValue) {
913
                if ($allowedValue instanceof Closure) {
914
                    if ($allowedValue($value)) {
915
                        $success = true;
916
                        break;
917
                    }
918
919
                    // Don't include closures in the exception message
920
                    continue;
921
                }
922
923
                if ($value === $allowedValue) {
924
                    $success = true;
925
                    break;
926
                }
927
928
                $printableAllowedValues[] = $allowedValue;
929
            }
930
931
            if (!$success) {
932
                $message = sprintf(
933
                    'The option "%s" with value %s is invalid.',
934
                    $option,
935
                    $this->formatValue($value)
936
                );
937
938
                if (count($printableAllowedValues) > 0) {
939
                    $message .= sprintf(
940
                        ' Accepted values are: %s.',
941
                        $this->formatValues($printableAllowedValues)
942
                    );
943
                }
944
945
                throw new InvalidOptionsException($message);
946
            }
947
        }
948
949
        // Check whether the option is deprecated
950
        // and it is provided by the user or is being called from a lazy evaluation
951
        if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && is_string($this->deprecated[$option])))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->calling of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
952
            $deprecationMessage = $this->deprecated[$option];
953
954
            if ($deprecationMessage instanceof Closure) {
955
                // If the closure is already being called, we have a cyclic dependency
956
                if (isset($this->calling[$option])) {
957
                    throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
958
                }
959
960
                $this->calling[$option] = true;
961
                try {
962
                    if (! is_string($deprecationMessage = $deprecationMessage($this, $value))) {
963
                        throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', gettype($deprecationMessage)));
964
                    }
965
                } finally {
966
                    unset($this->calling[$option]);
967
                }
968
            }
969
970
            if ('' !== $deprecationMessage) {
971
                @trigger_error(strtr($deprecationMessage, ['%name%' => $option]), E_USER_DEPRECATED);
972
            }
973
        }
974
975
        // Normalize the validated option
976
        if (isset($this->normalizers[$option])) {
977
            // If the closure is already being called, we have a cyclic
978
            // dependency
979
            if (isset($this->calling[$option])) {
980
                throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
981
            }
982
983
            $normalizer = $this->normalizers[$option];
984
985
            // The following section must be protected from cyclic
986
            // calls. Set $calling for the current $option to detect a cyclic
987
            // dependency
988
            // BEGIN
989
            $this->calling[$option] = true;
990
            try {
991
                $value = $normalizer($this, $value);
992
            } finally {
993
                unset($this->calling[$option]);
994
            }
995
            // END
996
        }
997
998
        // Mark as resolved
999
        $this->resolved[$option] = $value;
1000
1001
        return $value;
1002
    }
1003
1004
    private function verifyTypes(string $type, $value, array &$invalidTypes, int $level = 0): bool
1005
    {
1006
        if (is_array($value) && '[]' === substr($type, -2)) {
1007
            $type = substr($type, 0, -2);
1008
1009
            foreach ($value as $val) {
1010
                if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) {
1011
                    return false;
1012
                }
1013
            }
1014
1015
            return true;
1016
        }
1017
1018
        if (('null' === $type && null === $value) || (function_exists($func = 'is_'.$type) && $func($value)) || $value instanceof $type) {
1019
            return true;
1020
        }
1021
1022
        if (!$invalidTypes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $invalidTypes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
1023
            $suffix = '';
1024
            while (strlen($suffix) < $level * 2) {
1025
                $suffix .= '[]';
1026
            }
1027
            $invalidTypes[$this->formatTypeOf($value).$suffix] = true;
1028
        }
1029
1030
        return false;
1031
    }
1032
1033
    /**
1034
     * Returns whether a resolved option with the given name exists.
1035
     *
1036
     * @param string $option The option name
1037
     *
1038
     * @return bool Whether the option is set
1039
     *
1040
     * @throws AccessException If accessing this method outside of {@link resolve()}
1041
     *
1042
     * @see \ArrayAccess::offsetExists()
1043
     */
1044
    public function offsetExists($option)
1045
    {
1046
        if (!$this->locked) {
1047
            throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
1048
        }
1049
1050
        return array_key_exists($option, $this->defaults);
1051
    }
1052
1053
    /**
1054
     * Not supported.
1055
     *
1056
     * @throws AccessException
1057
     */
1058
    public function offsetSet($option, $value)
1059
    {
1060
        throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.');
1061
    }
1062
1063
    /**
1064
     * Not supported.
1065
     *
1066
     * @throws AccessException
1067
     */
1068
    public function offsetUnset($option)
1069
    {
1070
        throw new AccessException('Removing options via array access is not supported. Use remove() instead.');
1071
    }
1072
1073
    /**
1074
     * Returns the number of set options.
1075
     *
1076
     * This may be only a subset of the defined options.
1077
     *
1078
     * @return int Number of options
1079
     *
1080
     * @throws AccessException If accessing this method outside of {@link resolve()}
1081
     *
1082
     * @see \Countable::count()
1083
     */
1084
    public function count()
1085
    {
1086
        if (!$this->locked) {
1087
            throw new AccessException('Counting is only supported within closures of lazy options and normalizers.');
1088
        }
1089
1090
        return count($this->defaults);
1091
    }
1092
1093
    /**
1094
     * Returns a string representation of the type of the value.
1095
     *
1096
     * @param mixed $value The value to return the type of
1097
     *
1098
     * @return string The type of the value
1099
     */
1100
    private function formatTypeOf($value): string
1101
    {
1102
        return is_object($value) ? get_class($value) : gettype($value);
1103
    }
1104
1105
    /**
1106
     * Returns a string representation of the value.
1107
     *
1108
     * This method returns the equivalent PHP tokens for most scalar types
1109
     * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped
1110
     * in double quotes (").
1111
     *
1112
     * @param mixed $value The value to format as string
1113
     */
1114
    private function formatValue($value): string
1115
    {
1116
        if (is_object($value)) {
1117
            return get_class($value);
1118
        }
1119
1120
        if (is_array($value)) {
1121
            return 'array';
1122
        }
1123
1124
        if (is_string($value)) {
1125
            return '"'.$value.'"';
1126
        }
1127
1128
        if (is_resource($value)) {
1129
            return 'resource';
1130
        }
1131
1132
        if (null === $value) {
1133
            return 'null';
1134
        }
1135
1136
        if (false === $value) {
1137
            return 'false';
1138
        }
1139
1140
        if (true === $value) {
1141
            return 'true';
1142
        }
1143
1144
        return (string) $value;
1145
    }
1146
1147
    /**
1148
     * Returns a string representation of a list of values.
1149
     *
1150
     * Each of the values is converted to a string using
1151
     * {@link formatValue()}. The values are then concatenated with commas.
1152
     *
1153
     * @see formatValue()
1154
     */
1155
    private function formatValues(array $values): string
1156
    {
1157
        foreach ($values as $key => $value) {
1158
            $values[$key] = $this->formatValue($value);
1159
        }
1160
1161
        return implode(', ', $values);
1162
    }
1163
}
1164