@@ -76,13 +76,34 @@ class OptionsResolver implements Options
76
76
*/
77
77
private $ allowedValues = array ();
78
78
79
+ /**
80
+ * A list of accepted values for all options.
81
+ *
82
+ * @var array
83
+ */
84
+ private $ allowedValuesForAll = array ();
85
+
79
86
/**
80
87
* A list of accepted types for each option.
81
88
*
82
89
* @var array
83
90
*/
84
91
private $ allowedTypes = array ();
85
92
93
+ /**
94
+ * A list of accepted types for all options.
95
+ *
96
+ * @var array
97
+ */
98
+ private $ allowedTypesForAll = array ();
99
+
100
+ /**
101
+ * Whether to resolve undefined options.
102
+ *
103
+ * @var bool
104
+ */
105
+ private $ resolveUndefined = false ;
106
+
86
107
/**
87
108
* A list of closures for evaluating lazy options.
88
109
*
@@ -558,7 +579,7 @@ public function setAllowedValues($option, $allowedValues)
558
579
return $ this ;
559
580
}
560
581
561
- if (!isset ($ this ->defined [$ option ])) {
582
+ if (!isset ($ this ->defined [$ option ]) && ! $ this -> resolveUndefined && Options:: EXTRA !== $ option ) {
562
583
throw new UndefinedOptionsException (sprintf (
563
584
'The option "%s" does not exist. Defined options are: "%s". ' ,
564
585
$ option ,
@@ -609,7 +630,7 @@ public function addAllowedValues($option, $allowedValues)
609
630
return $ this ;
610
631
}
611
632
612
- if (!isset ($ this ->defined [$ option ])) {
633
+ if (!isset ($ this ->defined [$ option ]) && ! $ this -> resolveUndefined && Options:: EXTRA !== $ option ) {
613
634
throw new UndefinedOptionsException (sprintf (
614
635
'The option "%s" does not exist. Defined options are: "%s". ' ,
615
636
$ option ,
@@ -633,6 +654,98 @@ public function addAllowedValues($option, $allowedValues)
633
654
return $ this ;
634
655
}
635
656
657
+ /**
658
+ * Adds allowed values for one or more options.
659
+ *
660
+ * First argument may be a name or an array of option names.
661
+ *
662
+ * You can pass a constant of Options interface as first argument to target
663
+ * a group of options. Supported values are:
664
+ *
665
+ * - Options::ALL Defined and extra options
666
+ * - Options::DEFINED
667
+ * - Options::NESTED
668
+ * - Options::EXTRA
669
+ *
670
+ * If a nested option name is passed it will apply to all nested options.
671
+ * You can prevent it by passing Options::NONE as fourth argument.
672
+ *
673
+ * Instead of passing values, you may also pass a closures with the
674
+ * following signature:
675
+ *
676
+ * function ($value) {
677
+ * // return true or false
678
+ * }
679
+ *
680
+ * The closure receives the value as argument and should return true to
681
+ * accept the value and false to reject the value.
682
+ *
683
+ * @param string|string[]|int $optionNames One or more option names
684
+ * or an Options constant
685
+ * @param mixed $allowedValues One or more accepted values
686
+ * or closures
687
+ * @param bool $replace Whether to replace previous
688
+ * values
689
+ * @param int $nested This method is recursive for
690
+ * nested options which is the
691
+ * default. Pass Options::NONE
692
+ * to change it.
693
+ *
694
+ * @return OptionsResolver This instance
695
+ *
696
+ * @throws UndefinedOptionsException If an option is undefined
697
+ * @throws AccessException If called from a lazy option or normalizer
698
+ */
699
+ public function addAllowedValuesForAll ($ optionNames , $ allowedValues , $ replace = false , $ nested = Options::ALL )
700
+ {
701
+ if ($ this ->locked ) {
702
+ throw new AccessException ('Allowed types cannot be added from a lazy option or normalizer. ' );
703
+ }
704
+
705
+ switch ($ optionNames ) {
706
+ case Options::ALL :
707
+ // Use this values for all defined and extra options
708
+ // except nested which are always arrays.
709
+ $ this ->allowedValuesForAll = $ replace
710
+ ? $ allowedValues : array_merge ($ this ->allowedValuesForAll , $ allowedValues );
711
+
712
+ // If not recursive return
713
+ if (Options::NONE === $ nested ) {
714
+ return $ this ;
715
+ }
716
+
717
+ $ optionNames = $ this ->getNestedOptions ();
718
+ break ;
719
+ case Options::NESTED :
720
+ $ optionNames = $ this ->getNestedOptions ();
721
+ break ;
722
+ case Options::DEFINED :
723
+ $ optionNames = $ this ->getDefinedOptions ();
724
+ break ;
725
+ case Options::NONE :
726
+ // Not supported
727
+ return $ this ;
728
+ default :
729
+ // A custom array of option names or
730
+ // one option name or Options::EXTRA
731
+ $ optionNames = (array ) $ optionNames ;
732
+ }
733
+
734
+ foreach ($ optionNames as $ option ) {
735
+ if ($ this ->isNested ($ option ) && Options::NONE !== $ nested ) {
736
+ $ this ->nested [$ option ]->addAllowedValuesForAll ($ nested , $ allowedValues , $ replace );
737
+ } else {
738
+ if ($ replace ) {
739
+ $ this ->setAllowedValues ($ option , $ allowedValues );
740
+ } else {
741
+ $ this ->addAllowedValues ($ option , $ allowedValues );
742
+ }
743
+ }
744
+ }
745
+
746
+ return $ this ;
747
+ }
748
+
636
749
/**
637
750
* Sets allowed types for an option.
638
751
*
@@ -660,7 +773,7 @@ public function setAllowedTypes($option, $allowedTypes)
660
773
return $ this ;
661
774
}
662
775
663
- if (!isset ($ this ->defined [$ option ])) {
776
+ if (!isset ($ this ->defined [$ option ]) && ! $ this -> resolveUndefined && Options:: EXTRA !== $ option ) {
664
777
throw new UndefinedOptionsException (sprintf (
665
778
'The option "%s" does not exist. Defined options are: "%s". ' ,
666
779
$ option ,
@@ -705,7 +818,7 @@ public function addAllowedTypes($option, $allowedTypes)
705
818
return $ this ;
706
819
}
707
820
708
- if (!isset ($ this ->defined [$ option ])) {
821
+ if (!isset ($ this ->defined [$ option ]) && ! $ this -> resolveUndefined && Options:: EXTRA !== $ option ) {
709
822
throw new UndefinedOptionsException (sprintf (
710
823
'The option "%s" does not exist. Defined options are: "%s". ' ,
711
824
$ option ,
@@ -725,6 +838,147 @@ public function addAllowedTypes($option, $allowedTypes)
725
838
return $ this ;
726
839
}
727
840
841
+ /**
842
+ * Adds allowed types for one or more options.
843
+ *
844
+ * First argument may be a name or an array of option names.
845
+ *
846
+ * You can pass a constant of Options interface as first argument to target
847
+ * a group of options. Supported values are:
848
+ *
849
+ * - Options::ALL Defined and extra options
850
+ * - Options::DEFINED
851
+ * - Options::NESTED
852
+ *
853
+ * If a nested option name is passed it will apply to all nested options.
854
+ * You can prevent it by passing Options::NONE as fourth argument.
855
+ *
856
+ * Any type for which a corresponding is_<type>() function exists is
857
+ * acceptable. Additionally, fully-qualified class or interface names may
858
+ * be passed.
859
+ *
860
+ * @param string|string[]|int $optionNames One or more option names
861
+ * or Options::ALL
862
+ * @param string|string[] $allowedTypes One or more accepted types
863
+ * @param bool $replace Whether to replace previous
864
+ * value
865
+ * @param int $nested This method is recursive for
866
+ * nested options which is the
867
+ * default. Pass Options::NONE
868
+ * to change it.
869
+ *
870
+ * @return OptionsResolver This instance
871
+ *
872
+ * @throws UndefinedOptionsException If an option is undefined
873
+ * @throws AccessException If called from a lazy option or normalizer
874
+ */
875
+ public function addAllowedTypesForAll ($ optionNames , $ allowedTypes , $ replace = false , $ nested = Options::ALL )
876
+ {
877
+ if ($ this ->locked ) {
878
+ throw new AccessException ('Allowed types cannot be added from a lazy option or normalizer. ' );
879
+ }
880
+
881
+ switch ($ optionNames ) {
882
+ case Options::ALL :
883
+ // Use this values for all defined and extra options
884
+ // except nested which are always arrays.
885
+ $ this ->allowedTypesForAll = $ replace
886
+ ? array_merge ($ this ->allowedTypesForAll , $ allowedTypes ) : $ allowedTypes ;
887
+
888
+ // If not recursive return
889
+ if (Options::NONE === $ nested ) {
890
+ return $ this ;
891
+ }
892
+
893
+ $ optionNames = $ this ->getNestedOptions ();
894
+ break ;
895
+ case Options::NESTED :
896
+ $ optionNames = $ this ->getNestedOptions ();
897
+ break ;
898
+ case Options::DEFINED :
899
+ $ optionNames = $ this ->getDefinedOptions ();
900
+ break ;
901
+ case Options::NONE :
902
+ // Not supported
903
+ return $ this ;
904
+ default :
905
+ // A custom array of option names or
906
+ // one option name or Options::EXTRA
907
+ $ optionNames = (array ) $ optionNames ;
908
+ }
909
+
910
+ foreach ($ optionNames as $ option ) {
911
+ if ($ this ->isNested ($ option ) && Options::NONE !== $ nested ) {
912
+ $ this ->nested [$ option ]->addAllowedTypesForAll ($ nested , $ allowedTypes , $ replace );
913
+ } else {
914
+ if ($ replace ) {
915
+ $ this ->setAllowedTypes ($ option , $ allowedTypes );
916
+ } else {
917
+ $ this ->addAllowedTypes ($ option , $ allowedTypes );
918
+ }
919
+ }
920
+ }
921
+
922
+ return $ this ;
923
+ }
924
+
925
+ /**
926
+ * Add a prototype for extra or all options.
927
+ *
928
+ * If this instance was not accepting extra options before, this method
929
+ * allows it by default.
930
+ *
931
+ * First argument is an allowed type and second is one more values/closures.
932
+ * By default it only applies on undefined options, to validate previous defaults
933
+ * as well, pass Options::ALL as third argument.
934
+ *
935
+ * This method overrides previous allowed type and values for concerned options.
936
+ *
937
+ * $resolver->setNested('emails')
938
+ * ->setPrototype(array(
939
+ * 'string' => function ($email) use ($regex) {
940
+ * return preg_match($regex, $email);
941
+ * },
942
+ * );
943
+ *
944
+ * @param string $type A string defining an allowed type
945
+ * @param mixed $values One or more allowed values/closures
946
+ * @param int $options Options::EXTRA by default, but you can pass
947
+ * Options::ALL to apply the rule on previous defaults
948
+ *
949
+ * @return OptionsResolver This instance
950
+ *
951
+ * @throws UndefinedOptionsException If an option is undefined
952
+ * @throws AccessException If called from a lazy option or normalizer
953
+ */
954
+ public function setPrototype ($ type , $ values , $ options = Options::EXTRA )
955
+ {
956
+ if ($ this ->locked ) {
957
+ throw new AccessException ('Allowed types cannot be added from a lazy option or normalizer. ' );
958
+ }
959
+
960
+ $ this ->resolveUndefined = true ;
961
+
962
+ $ this ->addAllowedTypesForAll ($ options , $ type , true , Options::NONE );
963
+ $ this ->addAllowedValuesForAll ($ options , $ values , true , Options::NONE );
964
+
965
+ return $ this ;
966
+ }
967
+
968
+ /**
969
+ * Defines whether undefined options should be resolved.
970
+ *
971
+ * @param bool $allow Whether to resolve undefined options
972
+ *
973
+ * @return OptionsResolver This instance
974
+ */
975
+ public function allowExtraOptions ($ allow = true )
976
+ {
977
+ $ this ->resolveUndefined = $ allow ;
978
+
979
+ return $ this ;
980
+ }
981
+
728
982
/**
729
983
* Removes the option with the given name.
730
984
*
@@ -813,7 +1067,7 @@ public function resolve(array $options = array())
813
1067
// Make sure that no unknown options are passed
814
1068
$ diff = array_diff_key ($ options , $ clone ->defined );
815
1069
816
- if (count ($ diff ) > 0 ) {
1070
+ if (count ($ diff ) > 0 && false === $ this -> resolveUndefined ) {
817
1071
ksort ($ clone ->defined );
818
1072
ksort ($ diff );
819
1073
@@ -939,10 +1193,21 @@ public function offsetGet($option)
939
1193
}
940
1194
941
1195
// Validate the type of the resolved option
942
- if (isset ($ this ->allowedTypes [$ option ])) {
1196
+ if (isset ($ this ->allowedTypes [$ option ])
1197
+ || ($ this ->allowedTypesForAll && false === $ this ->isNested ($ option ))
1198
+ || $ extra = (isset ($ this ->allowedTypes [Options::EXTRA ]) && false === $ this ->isDefined ($ option ))
1199
+ ) {
943
1200
$ valid = false ;
944
1201
945
- foreach ($ this ->allowedTypes [$ option ] as $ type ) {
1202
+ if (isset ($ extra ) && $ extra && $ this ->resolveUndefined ) {
1203
+ $ allowedTypes = $ this ->allowedTypes [Options::EXTRA ];
1204
+ } else {
1205
+ $ allowedTypes = isset ($ this ->allowedTypes [$ option ]) ? $ this ->allowedTypes [$ option ] : array ();
1206
+ }
1207
+
1208
+ $ allowedTypes = array_unique (array_merge ($ allowedTypes , $ this ->allowedTypesForAll ));
1209
+
1210
+ foreach ($ allowedTypes as $ type ) {
946
1211
$ type = isset (self ::$ typeAliases [$ type ]) ? self ::$ typeAliases [$ type ] : $ type ;
947
1212
948
1213
if (function_exists ($ isFunction = 'is_ ' .$ type )) {
@@ -973,11 +1238,22 @@ public function offsetGet($option)
973
1238
}
974
1239
975
1240
// Validate the value of the resolved option
976
- if (isset ($ this ->allowedValues [$ option ])) {
1241
+ if (isset ($ this ->allowedValues [$ option ])
1242
+ || ($ this ->allowedValuesForAll && false === $ this ->isNested ($ option ))
1243
+ || $ extra = (isset ($ this ->allowedValues [Options::EXTRA ]) && false === $ this ->isDefined ($ option ))
1244
+ ) {
977
1245
$ success = false ;
978
1246
$ printableAllowedValues = array ();
979
1247
980
- foreach ($ this ->allowedValues [$ option ] as $ allowedValue ) {
1248
+ if (isset ($ extra ) && $ extra && $ this ->resolveUndefined ) {
1249
+ $ allowedValues = $ this ->allowedTypes [Options::EXTRA ];
1250
+ } else {
1251
+ $ allowedValues = isset ($ this ->allowedValues [$ option ]) ? $ this ->allowedValues [$ option ] : array ();
1252
+ }
1253
+
1254
+ $ allowedValues = array_merge ($ allowedValues , $ this ->allowedValuesForAll );
1255
+
1256
+ foreach ($ allowedValues as $ allowedValue ) {
981
1257
if ($ allowedValue instanceof \Closure) {
982
1258
if ($ allowedValue ($ value )) {
983
1259
$ success = true ;
0 commit comments