OptionsResolver.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  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\OptionsResolver;
  11. use Symfony\Component\OptionsResolver\Exception\AccessException;
  12. use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
  13. use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
  14. use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException;
  15. use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
  16. use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
  17. /**
  18. * Validates options and merges them with default values.
  19. *
  20. * @author Bernhard Schussek <bschussek@gmail.com>
  21. * @author Tobias Schultze <http://tobion.de>
  22. */
  23. class OptionsResolver implements Options, OptionsResolverInterface
  24. {
  25. /**
  26. * The fully qualified name of the {@link Options} interface.
  27. *
  28. * @internal
  29. */
  30. const OPTIONS_INTERFACE = 'Symfony\\Component\\OptionsResolver\\Options';
  31. /**
  32. * The names of all defined options.
  33. */
  34. private $defined = array();
  35. /**
  36. * The default option values.
  37. */
  38. private $defaults = array();
  39. /**
  40. * The names of required options.
  41. */
  42. private $required = array();
  43. /**
  44. * The resolved option values.
  45. */
  46. private $resolved = array();
  47. /**
  48. * A list of normalizer closures.
  49. *
  50. * @var \Closure[]
  51. */
  52. private $normalizers = array();
  53. /**
  54. * A list of accepted values for each option.
  55. */
  56. private $allowedValues = array();
  57. /**
  58. * A list of accepted types for each option.
  59. */
  60. private $allowedTypes = array();
  61. /**
  62. * A list of closures for evaluating lazy options.
  63. */
  64. private $lazy = array();
  65. /**
  66. * A list of lazy options whose closure is currently being called.
  67. *
  68. * This list helps detecting circular dependencies between lazy options.
  69. */
  70. private $calling = array();
  71. /**
  72. * Whether the instance is locked for reading.
  73. *
  74. * Once locked, the options cannot be changed anymore. This is
  75. * necessary in order to avoid inconsistencies during the resolving
  76. * process. If any option is changed after being read, all evaluated
  77. * lazy options that depend on this option would become invalid.
  78. */
  79. private $locked = false;
  80. private static $typeAliases = array(
  81. 'boolean' => 'bool',
  82. 'integer' => 'int',
  83. 'double' => 'float',
  84. );
  85. /**
  86. * Sets the default value of a given option.
  87. *
  88. * If the default value should be set based on other options, you can pass
  89. * a closure with the following signature:
  90. *
  91. * function (Options $options) {
  92. * // ...
  93. * }
  94. *
  95. * The closure will be evaluated when {@link resolve()} is called. The
  96. * closure has access to the resolved values of other options through the
  97. * passed {@link Options} instance:
  98. *
  99. * function (Options $options) {
  100. * if (isset($options['port'])) {
  101. * // ...
  102. * }
  103. * }
  104. *
  105. * If you want to access the previously set default value, add a second
  106. * argument to the closure's signature:
  107. *
  108. * $options->setDefault('name', 'Default Name');
  109. *
  110. * $options->setDefault('name', function (Options $options, $previousValue) {
  111. * // 'Default Name' === $previousValue
  112. * });
  113. *
  114. * This is mostly useful if the configuration of the {@link Options} object
  115. * is spread across different locations of your code, such as base and
  116. * sub-classes.
  117. *
  118. * @param string $option The name of the option
  119. * @param mixed $value The default value of the option
  120. *
  121. * @return $this
  122. *
  123. * @throws AccessException If called from a lazy option or normalizer
  124. */
  125. public function setDefault($option, $value)
  126. {
  127. // Setting is not possible once resolving starts, because then lazy
  128. // options could manipulate the state of the object, leading to
  129. // inconsistent results.
  130. if ($this->locked) {
  131. throw new AccessException('Default values cannot be set from a lazy option or normalizer.');
  132. }
  133. // If an option is a closure that should be evaluated lazily, store it
  134. // in the "lazy" property.
  135. if ($value instanceof \Closure) {
  136. $reflClosure = new \ReflectionFunction($value);
  137. $params = $reflClosure->getParameters();
  138. if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && self::OPTIONS_INTERFACE === $class->name) {
  139. // Initialize the option if no previous value exists
  140. if (!isset($this->defaults[$option])) {
  141. $this->defaults[$option] = null;
  142. }
  143. // Ignore previous lazy options if the closure has no second parameter
  144. if (!isset($this->lazy[$option]) || !isset($params[1])) {
  145. $this->lazy[$option] = array();
  146. }
  147. // Store closure for later evaluation
  148. $this->lazy[$option][] = $value;
  149. $this->defined[$option] = true;
  150. // Make sure the option is processed
  151. unset($this->resolved[$option]);
  152. return $this;
  153. }
  154. }
  155. // This option is not lazy anymore
  156. unset($this->lazy[$option]);
  157. // Yet undefined options can be marked as resolved, because we only need
  158. // to resolve options with lazy closures, normalizers or validation
  159. // rules, none of which can exist for undefined options
  160. // If the option was resolved before, update the resolved value
  161. if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) {
  162. $this->resolved[$option] = $value;
  163. }
  164. $this->defaults[$option] = $value;
  165. $this->defined[$option] = true;
  166. return $this;
  167. }
  168. /**
  169. * Sets a list of default values.
  170. *
  171. * @param array $defaults The default values to set
  172. *
  173. * @return $this
  174. *
  175. * @throws AccessException If called from a lazy option or normalizer
  176. */
  177. public function setDefaults(array $defaults)
  178. {
  179. foreach ($defaults as $option => $value) {
  180. $this->setDefault($option, $value);
  181. }
  182. return $this;
  183. }
  184. /**
  185. * Returns whether a default value is set for an option.
  186. *
  187. * Returns true if {@link setDefault()} was called for this option.
  188. * An option is also considered set if it was set to null.
  189. *
  190. * @param string $option The option name
  191. *
  192. * @return bool Whether a default value is set
  193. */
  194. public function hasDefault($option)
  195. {
  196. return array_key_exists($option, $this->defaults);
  197. }
  198. /**
  199. * Marks one or more options as required.
  200. *
  201. * @param string|string[] $optionNames One or more option names
  202. *
  203. * @return $this
  204. *
  205. * @throws AccessException If called from a lazy option or normalizer
  206. */
  207. public function setRequired($optionNames)
  208. {
  209. if ($this->locked) {
  210. throw new AccessException('Options cannot be made required from a lazy option or normalizer.');
  211. }
  212. foreach ((array) $optionNames as $option) {
  213. $this->defined[$option] = true;
  214. $this->required[$option] = true;
  215. }
  216. return $this;
  217. }
  218. /**
  219. * Returns whether an option is required.
  220. *
  221. * An option is required if it was passed to {@link setRequired()}.
  222. *
  223. * @param string $option The name of the option
  224. *
  225. * @return bool Whether the option is required
  226. */
  227. public function isRequired($option)
  228. {
  229. return isset($this->required[$option]);
  230. }
  231. /**
  232. * Returns the names of all required options.
  233. *
  234. * @return string[] The names of the required options
  235. *
  236. * @see isRequired()
  237. */
  238. public function getRequiredOptions()
  239. {
  240. return array_keys($this->required);
  241. }
  242. /**
  243. * Returns whether an option is missing a default value.
  244. *
  245. * An option is missing if it was passed to {@link setRequired()}, but not
  246. * to {@link setDefault()}. This option must be passed explicitly to
  247. * {@link resolve()}, otherwise an exception will be thrown.
  248. *
  249. * @param string $option The name of the option
  250. *
  251. * @return bool Whether the option is missing
  252. */
  253. public function isMissing($option)
  254. {
  255. return isset($this->required[$option]) && !array_key_exists($option, $this->defaults);
  256. }
  257. /**
  258. * Returns the names of all options missing a default value.
  259. *
  260. * @return string[] The names of the missing options
  261. *
  262. * @see isMissing()
  263. */
  264. public function getMissingOptions()
  265. {
  266. return array_keys(array_diff_key($this->required, $this->defaults));
  267. }
  268. /**
  269. * Defines a valid option name.
  270. *
  271. * Defines an option name without setting a default value. The option will
  272. * be accepted when passed to {@link resolve()}. When not passed, the
  273. * option will not be included in the resolved options.
  274. *
  275. * @param string|string[] $optionNames One or more option names
  276. *
  277. * @return $this
  278. *
  279. * @throws AccessException If called from a lazy option or normalizer
  280. */
  281. public function setDefined($optionNames)
  282. {
  283. if ($this->locked) {
  284. throw new AccessException('Options cannot be defined from a lazy option or normalizer.');
  285. }
  286. foreach ((array) $optionNames as $option) {
  287. $this->defined[$option] = true;
  288. }
  289. return $this;
  290. }
  291. /**
  292. * Returns whether an option is defined.
  293. *
  294. * Returns true for any option passed to {@link setDefault()},
  295. * {@link setRequired()} or {@link setDefined()}.
  296. *
  297. * @param string $option The option name
  298. *
  299. * @return bool Whether the option is defined
  300. */
  301. public function isDefined($option)
  302. {
  303. return isset($this->defined[$option]);
  304. }
  305. /**
  306. * Returns the names of all defined options.
  307. *
  308. * @return string[] The names of the defined options
  309. *
  310. * @see isDefined()
  311. */
  312. public function getDefinedOptions()
  313. {
  314. return array_keys($this->defined);
  315. }
  316. /**
  317. * Sets the normalizer for an option.
  318. *
  319. * The normalizer should be a closure with the following signature:
  320. *
  321. * ```php
  322. * function (Options $options, $value) {
  323. * // ...
  324. * }
  325. * ```
  326. *
  327. * The closure is invoked when {@link resolve()} is called. The closure
  328. * has access to the resolved values of other options through the passed
  329. * {@link Options} instance.
  330. *
  331. * The second parameter passed to the closure is the value of
  332. * the option.
  333. *
  334. * The resolved option value is set to the return value of the closure.
  335. *
  336. * @param string $option The option name
  337. * @param \Closure $normalizer The normalizer
  338. *
  339. * @return $this
  340. *
  341. * @throws UndefinedOptionsException If the option is undefined
  342. * @throws AccessException If called from a lazy option or normalizer
  343. */
  344. public function setNormalizer($option, \Closure $normalizer)
  345. {
  346. if ($this->locked) {
  347. throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.');
  348. }
  349. if (!isset($this->defined[$option])) {
  350. throw new UndefinedOptionsException(sprintf(
  351. 'The option "%s" does not exist. Defined options are: "%s".',
  352. $option,
  353. implode('", "', array_keys($this->defined))
  354. ));
  355. }
  356. $this->normalizers[$option] = $normalizer;
  357. // Make sure the option is processed
  358. unset($this->resolved[$option]);
  359. return $this;
  360. }
  361. /**
  362. * Sets the normalizers for an array of options.
  363. *
  364. * @param array $normalizers An array of closures
  365. *
  366. * @return $this
  367. *
  368. * @throws UndefinedOptionsException If the option is undefined
  369. * @throws AccessException If called from a lazy option or normalizer
  370. *
  371. * @see setNormalizer()
  372. * @deprecated since version 2.6, to be removed in 3.0.
  373. */
  374. public function setNormalizers(array $normalizers)
  375. {
  376. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use setNormalizer() instead.', E_USER_DEPRECATED);
  377. foreach ($normalizers as $option => $normalizer) {
  378. $this->setNormalizer($option, $normalizer);
  379. }
  380. return $this;
  381. }
  382. /**
  383. * Sets allowed values for an option.
  384. *
  385. * Instead of passing values, you may also pass a closures with the
  386. * following signature:
  387. *
  388. * function ($value) {
  389. * // return true or false
  390. * }
  391. *
  392. * The closure receives the value as argument and should return true to
  393. * accept the value and false to reject the value.
  394. *
  395. * @param string $option The option name
  396. * @param mixed $allowedValues One or more acceptable values/closures
  397. *
  398. * @return $this
  399. *
  400. * @throws UndefinedOptionsException If the option is undefined
  401. * @throws AccessException If called from a lazy option or normalizer
  402. */
  403. public function setAllowedValues($option, $allowedValues = null)
  404. {
  405. if ($this->locked) {
  406. throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.');
  407. }
  408. // BC
  409. if (is_array($option) && null === $allowedValues) {
  410. @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since Symfony 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED);
  411. foreach ($option as $optionName => $optionValues) {
  412. $this->setAllowedValues($optionName, $optionValues);
  413. }
  414. return $this;
  415. }
  416. if (!isset($this->defined[$option])) {
  417. throw new UndefinedOptionsException(sprintf(
  418. 'The option "%s" does not exist. Defined options are: "%s".',
  419. $option,
  420. implode('", "', array_keys($this->defined))
  421. ));
  422. }
  423. $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues);
  424. // Make sure the option is processed
  425. unset($this->resolved[$option]);
  426. return $this;
  427. }
  428. /**
  429. * Adds allowed values for an option.
  430. *
  431. * The values are merged with the allowed values defined previously.
  432. *
  433. * Instead of passing values, you may also pass a closures with the
  434. * following signature:
  435. *
  436. * function ($value) {
  437. * // return true or false
  438. * }
  439. *
  440. * The closure receives the value as argument and should return true to
  441. * accept the value and false to reject the value.
  442. *
  443. * @param string $option The option name
  444. * @param mixed $allowedValues One or more acceptable values/closures
  445. *
  446. * @return $this
  447. *
  448. * @throws UndefinedOptionsException If the option is undefined
  449. * @throws AccessException If called from a lazy option or normalizer
  450. */
  451. public function addAllowedValues($option, $allowedValues = null)
  452. {
  453. if ($this->locked) {
  454. throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.');
  455. }
  456. // BC
  457. if (is_array($option) && null === $allowedValues) {
  458. @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since Symfony 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED);
  459. foreach ($option as $optionName => $optionValues) {
  460. $this->addAllowedValues($optionName, $optionValues);
  461. }
  462. return $this;
  463. }
  464. if (!isset($this->defined[$option])) {
  465. throw new UndefinedOptionsException(sprintf(
  466. 'The option "%s" does not exist. Defined options are: "%s".',
  467. $option,
  468. implode('", "', array_keys($this->defined))
  469. ));
  470. }
  471. if (!is_array($allowedValues)) {
  472. $allowedValues = array($allowedValues);
  473. }
  474. if (!isset($this->allowedValues[$option])) {
  475. $this->allowedValues[$option] = $allowedValues;
  476. } else {
  477. $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues);
  478. }
  479. // Make sure the option is processed
  480. unset($this->resolved[$option]);
  481. return $this;
  482. }
  483. /**
  484. * Sets allowed types for an option.
  485. *
  486. * Any type for which a corresponding is_<type>() function exists is
  487. * acceptable. Additionally, fully-qualified class or interface names may
  488. * be passed.
  489. *
  490. * @param string $option The option name
  491. * @param string|string[] $allowedTypes One or more accepted types
  492. *
  493. * @return $this
  494. *
  495. * @throws UndefinedOptionsException If the option is undefined
  496. * @throws AccessException If called from a lazy option or normalizer
  497. */
  498. public function setAllowedTypes($option, $allowedTypes = null)
  499. {
  500. if ($this->locked) {
  501. throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.');
  502. }
  503. // BC
  504. if (is_array($option) && null === $allowedTypes) {
  505. @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since Symfony 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED);
  506. foreach ($option as $optionName => $optionTypes) {
  507. $this->setAllowedTypes($optionName, $optionTypes);
  508. }
  509. return $this;
  510. }
  511. if (!isset($this->defined[$option])) {
  512. throw new UndefinedOptionsException(sprintf(
  513. 'The option "%s" does not exist. Defined options are: "%s".',
  514. $option,
  515. implode('", "', array_keys($this->defined))
  516. ));
  517. }
  518. $this->allowedTypes[$option] = (array) $allowedTypes;
  519. // Make sure the option is processed
  520. unset($this->resolved[$option]);
  521. return $this;
  522. }
  523. /**
  524. * Adds allowed types for an option.
  525. *
  526. * The types are merged with the allowed types defined previously.
  527. *
  528. * Any type for which a corresponding is_<type>() function exists is
  529. * acceptable. Additionally, fully-qualified class or interface names may
  530. * be passed.
  531. *
  532. * @param string $option The option name
  533. * @param string|string[] $allowedTypes One or more accepted types
  534. *
  535. * @return $this
  536. *
  537. * @throws UndefinedOptionsException If the option is undefined
  538. * @throws AccessException If called from a lazy option or normalizer
  539. */
  540. public function addAllowedTypes($option, $allowedTypes = null)
  541. {
  542. if ($this->locked) {
  543. throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.');
  544. }
  545. // BC
  546. if (is_array($option) && null === $allowedTypes) {
  547. @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since Symfony 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED);
  548. foreach ($option as $optionName => $optionTypes) {
  549. $this->addAllowedTypes($optionName, $optionTypes);
  550. }
  551. return $this;
  552. }
  553. if (!isset($this->defined[$option])) {
  554. throw new UndefinedOptionsException(sprintf(
  555. 'The option "%s" does not exist. Defined options are: "%s".',
  556. $option,
  557. implode('", "', array_keys($this->defined))
  558. ));
  559. }
  560. if (!isset($this->allowedTypes[$option])) {
  561. $this->allowedTypes[$option] = (array) $allowedTypes;
  562. } else {
  563. $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes);
  564. }
  565. // Make sure the option is processed
  566. unset($this->resolved[$option]);
  567. return $this;
  568. }
  569. /**
  570. * Removes the option with the given name.
  571. *
  572. * Undefined options are ignored.
  573. *
  574. * @param string|string[] $optionNames One or more option names
  575. *
  576. * @return $this
  577. *
  578. * @throws AccessException If called from a lazy option or normalizer
  579. */
  580. public function remove($optionNames)
  581. {
  582. if ($this->locked) {
  583. throw new AccessException('Options cannot be removed from a lazy option or normalizer.');
  584. }
  585. foreach ((array) $optionNames as $option) {
  586. unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]);
  587. unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]);
  588. }
  589. return $this;
  590. }
  591. /**
  592. * Removes all options.
  593. *
  594. * @return $this
  595. *
  596. * @throws AccessException If called from a lazy option or normalizer
  597. */
  598. public function clear()
  599. {
  600. if ($this->locked) {
  601. throw new AccessException('Options cannot be cleared from a lazy option or normalizer.');
  602. }
  603. $this->defined = array();
  604. $this->defaults = array();
  605. $this->required = array();
  606. $this->resolved = array();
  607. $this->lazy = array();
  608. $this->normalizers = array();
  609. $this->allowedTypes = array();
  610. $this->allowedValues = array();
  611. return $this;
  612. }
  613. /**
  614. * Merges options with the default values stored in the container and
  615. * validates them.
  616. *
  617. * Exceptions are thrown if:
  618. *
  619. * - Undefined options are passed;
  620. * - Required options are missing;
  621. * - Options have invalid types;
  622. * - Options have invalid values.
  623. *
  624. * @param array $options A map of option names to values
  625. *
  626. * @return array The merged and validated options
  627. *
  628. * @throws UndefinedOptionsException If an option name is undefined
  629. * @throws InvalidOptionsException If an option doesn't fulfill the
  630. * specified validation rules
  631. * @throws MissingOptionsException If a required option is missing
  632. * @throws OptionDefinitionException If there is a cyclic dependency between
  633. * lazy options and/or normalizers
  634. * @throws NoSuchOptionException If a lazy option reads an unavailable option
  635. * @throws AccessException If called from a lazy option or normalizer
  636. */
  637. public function resolve(array $options = array())
  638. {
  639. if ($this->locked) {
  640. throw new AccessException('Options cannot be resolved from a lazy option or normalizer.');
  641. }
  642. // Allow this method to be called multiple times
  643. $clone = clone $this;
  644. // Make sure that no unknown options are passed
  645. $diff = array_diff_key($options, $clone->defined);
  646. if (count($diff) > 0) {
  647. ksort($clone->defined);
  648. ksort($diff);
  649. throw new UndefinedOptionsException(sprintf(
  650. (count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".',
  651. implode('", "', array_keys($diff)),
  652. implode('", "', array_keys($clone->defined))
  653. ));
  654. }
  655. // Override options set by the user
  656. foreach ($options as $option => $value) {
  657. $clone->defaults[$option] = $value;
  658. unset($clone->resolved[$option], $clone->lazy[$option]);
  659. }
  660. // Check whether any required option is missing
  661. $diff = array_diff_key($clone->required, $clone->defaults);
  662. if (count($diff) > 0) {
  663. ksort($diff);
  664. throw new MissingOptionsException(sprintf(
  665. count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.',
  666. implode('", "', array_keys($diff))
  667. ));
  668. }
  669. // Lock the container
  670. $clone->locked = true;
  671. // Now process the individual options. Use offsetGet(), which resolves
  672. // the option itself and any options that the option depends on
  673. foreach ($clone->defaults as $option => $_) {
  674. $clone->offsetGet($option);
  675. }
  676. return $clone->resolved;
  677. }
  678. /**
  679. * Returns the resolved value of an option.
  680. *
  681. * @param string $option The option name
  682. *
  683. * @return mixed The option value
  684. *
  685. * @throws AccessException If accessing this method outside of
  686. * {@link resolve()}
  687. * @throws NoSuchOptionException If the option is not set
  688. * @throws InvalidOptionsException If the option doesn't fulfill the
  689. * specified validation rules
  690. * @throws OptionDefinitionException If there is a cyclic dependency between
  691. * lazy options and/or normalizers
  692. */
  693. public function offsetGet($option)
  694. {
  695. if (!$this->locked) {
  696. throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
  697. }
  698. // Shortcut for resolved options
  699. if (array_key_exists($option, $this->resolved)) {
  700. return $this->resolved[$option];
  701. }
  702. // Check whether the option is set at all
  703. if (!array_key_exists($option, $this->defaults)) {
  704. if (!isset($this->defined[$option])) {
  705. throw new NoSuchOptionException(sprintf(
  706. 'The option "%s" does not exist. Defined options are: "%s".',
  707. $option,
  708. implode('", "', array_keys($this->defined))
  709. ));
  710. }
  711. throw new NoSuchOptionException(sprintf(
  712. 'The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.',
  713. $option
  714. ));
  715. }
  716. $value = $this->defaults[$option];
  717. // Resolve the option if the default value is lazily evaluated
  718. if (isset($this->lazy[$option])) {
  719. // If the closure is already being called, we have a cyclic
  720. // dependency
  721. if (isset($this->calling[$option])) {
  722. throw new OptionDefinitionException(sprintf(
  723. 'The options "%s" have a cyclic dependency.',
  724. implode('", "', array_keys($this->calling))
  725. ));
  726. }
  727. // The following section must be protected from cyclic
  728. // calls. Set $calling for the current $option to detect a cyclic
  729. // dependency
  730. // BEGIN
  731. $this->calling[$option] = true;
  732. try {
  733. foreach ($this->lazy[$option] as $closure) {
  734. $value = $closure($this, $value);
  735. }
  736. } catch (\Exception $e) {
  737. unset($this->calling[$option]);
  738. throw $e;
  739. } catch (\Throwable $e) {
  740. unset($this->calling[$option]);
  741. throw $e;
  742. }
  743. unset($this->calling[$option]);
  744. // END
  745. }
  746. // Validate the type of the resolved option
  747. if (isset($this->allowedTypes[$option])) {
  748. $valid = false;
  749. foreach ($this->allowedTypes[$option] as $type) {
  750. $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type;
  751. if (function_exists($isFunction = 'is_'.$type)) {
  752. if ($isFunction($value)) {
  753. $valid = true;
  754. break;
  755. }
  756. continue;
  757. }
  758. if ($value instanceof $type) {
  759. $valid = true;
  760. break;
  761. }
  762. }
  763. if (!$valid) {
  764. throw new InvalidOptionsException(sprintf(
  765. 'The option "%s" with value %s is expected to be of type '.
  766. '"%s", but is of type "%s".',
  767. $option,
  768. $this->formatValue($value),
  769. implode('" or "', $this->allowedTypes[$option]),
  770. $this->formatTypeOf($value)
  771. ));
  772. }
  773. }
  774. // Validate the value of the resolved option
  775. if (isset($this->allowedValues[$option])) {
  776. $success = false;
  777. $printableAllowedValues = array();
  778. foreach ($this->allowedValues[$option] as $allowedValue) {
  779. if ($allowedValue instanceof \Closure) {
  780. if ($allowedValue($value)) {
  781. $success = true;
  782. break;
  783. }
  784. // Don't include closures in the exception message
  785. continue;
  786. } elseif ($value === $allowedValue) {
  787. $success = true;
  788. break;
  789. }
  790. $printableAllowedValues[] = $allowedValue;
  791. }
  792. if (!$success) {
  793. $message = sprintf(
  794. 'The option "%s" with value %s is invalid.',
  795. $option,
  796. $this->formatValue($value)
  797. );
  798. if (count($printableAllowedValues) > 0) {
  799. $message .= sprintf(
  800. ' Accepted values are: %s.',
  801. $this->formatValues($printableAllowedValues)
  802. );
  803. }
  804. throw new InvalidOptionsException($message);
  805. }
  806. }
  807. // Normalize the validated option
  808. if (isset($this->normalizers[$option])) {
  809. // If the closure is already being called, we have a cyclic
  810. // dependency
  811. if (isset($this->calling[$option])) {
  812. throw new OptionDefinitionException(sprintf(
  813. 'The options "%s" have a cyclic dependency.',
  814. implode('", "', array_keys($this->calling))
  815. ));
  816. }
  817. $normalizer = $this->normalizers[$option];
  818. // The following section must be protected from cyclic
  819. // calls. Set $calling for the current $option to detect a cyclic
  820. // dependency
  821. // BEGIN
  822. $this->calling[$option] = true;
  823. try {
  824. $value = $normalizer($this, $value);
  825. } catch (\Exception $e) {
  826. unset($this->calling[$option]);
  827. throw $e;
  828. } catch (\Throwable $e) {
  829. unset($this->calling[$option]);
  830. throw $e;
  831. }
  832. unset($this->calling[$option]);
  833. // END
  834. }
  835. // Mark as resolved
  836. $this->resolved[$option] = $value;
  837. return $value;
  838. }
  839. /**
  840. * Returns whether a resolved option with the given name exists.
  841. *
  842. * @param string $option The option name
  843. *
  844. * @return bool Whether the option is set
  845. *
  846. * @throws AccessException If accessing this method outside of {@link resolve()}
  847. *
  848. * @see \ArrayAccess::offsetExists()
  849. */
  850. public function offsetExists($option)
  851. {
  852. if (!$this->locked) {
  853. throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
  854. }
  855. return array_key_exists($option, $this->defaults);
  856. }
  857. /**
  858. * Not supported.
  859. *
  860. * @throws AccessException
  861. */
  862. public function offsetSet($option, $value)
  863. {
  864. throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.');
  865. }
  866. /**
  867. * Not supported.
  868. *
  869. * @throws AccessException
  870. */
  871. public function offsetUnset($option)
  872. {
  873. throw new AccessException('Removing options via array access is not supported. Use remove() instead.');
  874. }
  875. /**
  876. * Returns the number of set options.
  877. *
  878. * This may be only a subset of the defined options.
  879. *
  880. * @return int Number of options
  881. *
  882. * @throws AccessException If accessing this method outside of {@link resolve()}
  883. *
  884. * @see \Countable::count()
  885. */
  886. public function count()
  887. {
  888. if (!$this->locked) {
  889. throw new AccessException('Counting is only supported within closures of lazy options and normalizers.');
  890. }
  891. return count($this->defaults);
  892. }
  893. /**
  894. * Alias of {@link setDefault()}.
  895. *
  896. * @deprecated since version 2.6, to be removed in 3.0.
  897. */
  898. public function set($option, $value)
  899. {
  900. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the setDefaults() method instead.', E_USER_DEPRECATED);
  901. return $this->setDefault($option, $value);
  902. }
  903. /**
  904. * Shortcut for {@link clear()} and {@link setDefaults()}.
  905. *
  906. * @deprecated since version 2.6, to be removed in 3.0.
  907. */
  908. public function replace(array $defaults)
  909. {
  910. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED);
  911. $this->clear();
  912. return $this->setDefaults($defaults);
  913. }
  914. /**
  915. * Alias of {@link setDefault()}.
  916. *
  917. * @deprecated since version 2.6, to be removed in 3.0.
  918. */
  919. public function overload($option, $value)
  920. {
  921. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the setDefault() method instead.', E_USER_DEPRECATED);
  922. return $this->setDefault($option, $value);
  923. }
  924. /**
  925. * Alias of {@link offsetGet()}.
  926. *
  927. * @deprecated since version 2.6, to be removed in 3.0.
  928. */
  929. public function get($option)
  930. {
  931. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED);
  932. return $this->offsetGet($option);
  933. }
  934. /**
  935. * Alias of {@link offsetExists()}.
  936. *
  937. * @deprecated since version 2.6, to be removed in 3.0.
  938. */
  939. public function has($option)
  940. {
  941. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED);
  942. return $this->offsetExists($option);
  943. }
  944. /**
  945. * Shortcut for {@link clear()} and {@link setDefaults()}.
  946. *
  947. * @deprecated since version 2.6, to be removed in 3.0.
  948. */
  949. public function replaceDefaults(array $defaultValues)
  950. {
  951. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED);
  952. $this->clear();
  953. return $this->setDefaults($defaultValues);
  954. }
  955. /**
  956. * Alias of {@link setDefined()}.
  957. *
  958. * @deprecated since version 2.6, to be removed in 3.0.
  959. */
  960. public function setOptional(array $optionNames)
  961. {
  962. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the setDefined() method instead.', E_USER_DEPRECATED);
  963. return $this->setDefined($optionNames);
  964. }
  965. /**
  966. * Alias of {@link isDefined()}.
  967. *
  968. * @deprecated since version 2.6, to be removed in 3.0.
  969. */
  970. public function isKnown($option)
  971. {
  972. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the isDefined() method instead.', E_USER_DEPRECATED);
  973. return $this->isDefined($option);
  974. }
  975. /**
  976. * Returns a string representation of the type of the value.
  977. *
  978. * This method should be used if you pass the type of a value as
  979. * message parameter to a constraint violation. Note that such
  980. * parameters should usually not be included in messages aimed at
  981. * non-technical people.
  982. *
  983. * @param mixed $value The value to return the type of
  984. *
  985. * @return string The type of the value
  986. */
  987. private function formatTypeOf($value)
  988. {
  989. return is_object($value) ? get_class($value) : gettype($value);
  990. }
  991. /**
  992. * Returns a string representation of the value.
  993. *
  994. * This method returns the equivalent PHP tokens for most scalar types
  995. * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped
  996. * in double quotes (").
  997. *
  998. * @param mixed $value The value to format as string
  999. *
  1000. * @return string The string representation of the passed value
  1001. */
  1002. private function formatValue($value)
  1003. {
  1004. if (is_object($value)) {
  1005. return get_class($value);
  1006. }
  1007. if (is_array($value)) {
  1008. return 'array';
  1009. }
  1010. if (is_string($value)) {
  1011. return '"'.$value.'"';
  1012. }
  1013. if (is_resource($value)) {
  1014. return 'resource';
  1015. }
  1016. if (null === $value) {
  1017. return 'null';
  1018. }
  1019. if (false === $value) {
  1020. return 'false';
  1021. }
  1022. if (true === $value) {
  1023. return 'true';
  1024. }
  1025. return (string) $value;
  1026. }
  1027. /**
  1028. * Returns a string representation of a list of values.
  1029. *
  1030. * Each of the values is converted to a string using
  1031. * {@link formatValue()}. The values are then concatenated with commas.
  1032. *
  1033. * @param array $values A list of values
  1034. *
  1035. * @return string The string representation of the value list
  1036. *
  1037. * @see formatValue()
  1038. */
  1039. private function formatValues(array $values)
  1040. {
  1041. foreach ($values as $key => $value) {
  1042. $values[$key] = $this->formatValue($value);
  1043. }
  1044. return implode(', ', $values);
  1045. }
  1046. }