PageRenderTime 27ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Symfony/Component/Form/DefaultOptions.php

https://github.com/Exercise/symfony
PHP | 320 lines | 58 code | 20 blank | 242 comment | 4 complexity | 365a5095ec353209e61371c8800db4cd MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Form;
  11. use Symfony\Component\Form\Exception\OptionDefinitionException;
  12. use Symfony\Component\Form\Exception\InvalidOptionException;
  13. /**
  14. * Helper for specifying and resolving inter-dependent options.
  15. *
  16. * Options are a common pattern for initializing classes in PHP. Avoiding the
  17. * problems related to this approach is however a non-trivial task. Usually,
  18. * both classes and subclasses should be able to set default option values.
  19. * These default options should be overridden by the options passed to the
  20. * constructor. Last but not least, the (default) values of some options may
  21. * depend on the values of other options, which themselves may depend on other
  22. * options and so on.
  23. *
  24. * DefaultOptions resolves these problems. It allows you to:
  25. *
  26. * - Define default option values
  27. * - Define options in layers that correspond to your class hierarchy. Each
  28. * layer may depend on the default value set in the higher layers.
  29. * - Define default values for options that depend on the <em>concrete</em>
  30. * values of other options.
  31. * - Resolve the concrete option values by passing the options set by the
  32. * user.
  33. *
  34. * You can use it in your classes by implementing the following pattern:
  35. *
  36. * <code>
  37. * class Car
  38. * {
  39. * protected $options;
  40. *
  41. * public function __construct(array $options)
  42. * {
  43. * $defaultOptions = new DefaultOptions();
  44. * $this->addDefaultOptions($defaultOptions);
  45. *
  46. * $this->options = $defaultOptions->resolve($options);
  47. * }
  48. *
  49. * protected function addDefaultOptions(DefaultOptions $options)
  50. * {
  51. * $options->add(array(
  52. * 'make' => 'VW',
  53. * 'year' => '1999',
  54. * ));
  55. * }
  56. * }
  57. *
  58. * $car = new Car(array(
  59. * 'make' => 'Mercedes',
  60. * 'year' => 2005,
  61. * ));
  62. * </code>
  63. *
  64. * By calling add(), new default options are added to the container. The method
  65. * resolve() accepts an array of options passed by the user that are matched
  66. * against the defined options. If any option is not recognized, an exception
  67. * is thrown. Finally, resolve() returns the merged default and user options.
  68. *
  69. * You can now easily add or override options in subclasses:
  70. *
  71. * <code>
  72. * class Renault extends Car
  73. * {
  74. * protected function addDefaultOptions(DefaultOptions $options)
  75. * {
  76. * parent::addDefaultOptions($options);
  77. *
  78. * $options->add(array(
  79. * 'make' => 'Renault',
  80. * 'gear' => 'auto',
  81. * ));
  82. * }
  83. * }
  84. *
  85. * $renault = new Renault(array(
  86. * 'year' => 1997,
  87. * 'gear' => 'manual'
  88. * ));
  89. * </code>
  90. *
  91. * IMPORTANT: parent::addDefaultOptions() must always be called before adding
  92. * new default options!
  93. *
  94. * In the previous example, it makes sense to restrict the option "gear" to
  95. * a set of allowed values:
  96. *
  97. * <code>
  98. * class Renault extends Car
  99. * {
  100. * protected function addDefaultOptions(DefaultOptions $options)
  101. * {
  102. * // ... like above ...
  103. *
  104. * $options->addAllowedValues(array(
  105. * 'gear' => array('auto', 'manual'),
  106. * ));
  107. * }
  108. * }
  109. *
  110. * // Fails!
  111. * $renault = new Renault(array(
  112. * 'gear' => 'v6',
  113. * ));
  114. * </code>
  115. *
  116. * Now it is impossible to pass a value in the "gear" option that is not
  117. * expected.
  118. *
  119. * Last but not least, you can define options that depend on other options.
  120. * For example, depending on the "make" you could preset the country that the
  121. * car is registered in.
  122. *
  123. * <code>
  124. * class Car
  125. * {
  126. * protected function addDefaultOptions(DefaultOptions $options)
  127. * {
  128. * $options->add(array(
  129. * 'make' => 'VW',
  130. * 'year' => '1999',
  131. * 'country' => function (Options $options) {
  132. * if ('VW' === $options['make']) {
  133. * return 'DE';
  134. * }
  135. *
  136. * return null;
  137. * },
  138. * ));
  139. * }
  140. * }
  141. *
  142. * $car = new Car(array(
  143. * 'make' => 'VW', // => "country" is "DE"
  144. * ));
  145. * </code>
  146. *
  147. * The closure receives as its first parameter a container of class Options
  148. * that contains the <em>concrete</em> options determined upon resolving. The
  149. * closure is executed once resolve() is called.
  150. *
  151. * The closure also receives a second parameter $previousValue that contains the
  152. * value defined by the parent layer of the hierarchy. If the option has not
  153. * been defined in any parent layer, the second parameter is NULL.
  154. *
  155. * <code>
  156. * class Renault extends Car
  157. * {
  158. * protected function addDefaultOptions(DefaultOptions $options)
  159. * {
  160. * $options->add(array(
  161. * 'country' => function (Options $options, $previousValue) {
  162. * if ('Renault' === $options['make']) {
  163. * return 'FR';
  164. * }
  165. *
  166. * // return default value defined in Car
  167. * return $previousValue;
  168. * },
  169. * ));
  170. * }
  171. * }
  172. *
  173. * $renault = new Renault(array(
  174. * 'make' => 'VW', // => "country" is still "DE"
  175. * ));
  176. * </code>
  177. *
  178. * @author Bernhard Schussek <bschussek@gmail.com>
  179. */
  180. class DefaultOptions
  181. {
  182. /**
  183. * The container resolving the options.
  184. * @var Options
  185. */
  186. private $options;
  187. /**
  188. * A list of accepted values for each option.
  189. * @var array
  190. */
  191. private $allowedValues = array();
  192. /**
  193. * Creates a new instance.
  194. */
  195. public function __construct()
  196. {
  197. $this->options = new Options();
  198. }
  199. /**
  200. * Adds default options.
  201. *
  202. * @param array $options A list of option names as keys and option values
  203. * as values. The option values may be closures
  204. * of the following signatures:
  205. *
  206. * - function (Options $options)
  207. * - function (Options $options, $previousValue)
  208. */
  209. public function add(array $options)
  210. {
  211. foreach ($options as $option => $value) {
  212. $this->options[$option] = $value;
  213. }
  214. }
  215. /**
  216. * Adds allowed values for a list of options.
  217. *
  218. * @param array $allowedValues A list of option names as keys and arrays
  219. * with values acceptable for that option as
  220. * values.
  221. *
  222. * @throws InvalidOptionException If an option has not been defined for
  223. * which an allowed value is set.
  224. */
  225. public function addAllowedValues(array $allowedValues)
  226. {
  227. $this->validateOptionNames(array_keys($allowedValues));
  228. $this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues);
  229. }
  230. /**
  231. * Resolves the final option values by merging default options with user
  232. * options.
  233. *
  234. * @param array $userOptions The options passed by the user.
  235. *
  236. * @return array A list of options and their final values.
  237. *
  238. * @throws InvalidOptionException If any of the passed options has not
  239. * been defined or does not contain an
  240. * allowed value.
  241. * @throws OptionDefinitionException If a cyclic dependency is detected
  242. * between option closures.
  243. */
  244. public function resolve(array $userOptions)
  245. {
  246. // Make sure this method can be called multiple times
  247. $options = clone $this->options;
  248. $this->validateOptionNames(array_keys($userOptions));
  249. // Override options set by the user
  250. foreach ($userOptions as $option => $value) {
  251. $options[$option] = $value;
  252. }
  253. // Resolve options
  254. $options = iterator_to_array($options);
  255. // Validate against allowed values
  256. $this->validateOptionValues($options);
  257. return $options;
  258. }
  259. /**
  260. * Validates that the given option names exist and throws an exception
  261. * otherwise.
  262. *
  263. * @param array $optionNames A list of option names.
  264. *
  265. * @throws InvalidOptionException If any of the options has not been
  266. * defined.
  267. */
  268. private function validateOptionNames(array $optionNames)
  269. {
  270. $knownOptions = $this->options->getNames();
  271. $diff = array_diff($optionNames, $knownOptions);
  272. if (count($diff) > 0) {
  273. sort($knownOptions);
  274. sort($diff);
  275. }
  276. if (count($diff) > 1) {
  277. throw new InvalidOptionException(sprintf('The options "%s" do not exist. Known options are: "%s"', implode('", "', $diff), implode('", "', $knownOptions)));
  278. }
  279. if (count($diff) > 0) {
  280. throw new InvalidOptionException(sprintf('The option "%s" does not exist. Known options are: "%s"', current($diff), implode('", "', $knownOptions)));
  281. }
  282. }
  283. /**
  284. * Validates that the given option values match the allowed values and
  285. * throws an exception otherwise.
  286. *
  287. * @param array $options A list of option values.
  288. *
  289. * @throws InvalidOptionException If any of the values does not match the
  290. * allowed values of the option.
  291. */
  292. private function validateOptionValues(array $options)
  293. {
  294. foreach ($this->allowedValues as $option => $allowedValues) {
  295. if (!in_array($options[$option], $allowedValues, true)) {
  296. throw new InvalidOptionException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
  297. }
  298. }
  299. }
  300. }