OptionsResolver.php 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157
  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. * function (Options $options, $value) {
  322. * // ...
  323. * }
  324. *
  325. * The closure is invoked when {@link resolve()} is called. The closure
  326. * has access to the resolved values of other options through the passed
  327. * {@link Options} instance.
  328. *
  329. * The second parameter passed to the closure is the value of
  330. * the option.
  331. *
  332. * The resolved option value is set to the return value of the closure.
  333. *
  334. * @param string $option The option name
  335. * @param \Closure $normalizer The normalizer
  336. *
  337. * @return $this
  338. *
  339. * @throws UndefinedOptionsException If the option is undefined
  340. * @throws AccessException If called from a lazy option or normalizer
  341. */
  342. public function setNormalizer($option, \Closure $normalizer)
  343. {
  344. if ($this->locked) {
  345. throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.');
  346. }
  347. if (!isset($this->defined[$option])) {
  348. throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
  349. }
  350. $this->normalizers[$option] = $normalizer;
  351. // Make sure the option is processed
  352. unset($this->resolved[$option]);
  353. return $this;
  354. }
  355. /**
  356. * Sets the normalizers for an array of options.
  357. *
  358. * @param array $normalizers An array of closures
  359. *
  360. * @return $this
  361. *
  362. * @throws UndefinedOptionsException If the option is undefined
  363. * @throws AccessException If called from a lazy option or normalizer
  364. *
  365. * @see setNormalizer()
  366. * @deprecated since version 2.6, to be removed in 3.0.
  367. */
  368. public function setNormalizers(array $normalizers)
  369. {
  370. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use setNormalizer() instead.', E_USER_DEPRECATED);
  371. foreach ($normalizers as $option => $normalizer) {
  372. $this->setNormalizer($option, $normalizer);
  373. }
  374. return $this;
  375. }
  376. /**
  377. * Sets allowed values for an option.
  378. *
  379. * Instead of passing values, you may also pass a closures with the
  380. * following signature:
  381. *
  382. * function ($value) {
  383. * // return true or false
  384. * }
  385. *
  386. * The closure receives the value as argument and should return true to
  387. * accept the value and false to reject the value.
  388. *
  389. * @param string $option The option name
  390. * @param mixed $allowedValues One or more acceptable values/closures
  391. *
  392. * @return $this
  393. *
  394. * @throws UndefinedOptionsException If the option is undefined
  395. * @throws AccessException If called from a lazy option or normalizer
  396. */
  397. public function setAllowedValues($option, $allowedValues = null)
  398. {
  399. if ($this->locked) {
  400. throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.');
  401. }
  402. // BC
  403. if (\is_array($option) && null === $allowedValues) {
  404. @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);
  405. foreach ($option as $optionName => $optionValues) {
  406. $this->setAllowedValues($optionName, $optionValues);
  407. }
  408. return $this;
  409. }
  410. if (!isset($this->defined[$option])) {
  411. throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
  412. }
  413. $this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : array($allowedValues);
  414. // Make sure the option is processed
  415. unset($this->resolved[$option]);
  416. return $this;
  417. }
  418. /**
  419. * Adds allowed values for an option.
  420. *
  421. * The values are merged with the allowed values defined previously.
  422. *
  423. * Instead of passing values, you may also pass a closures with the
  424. * following signature:
  425. *
  426. * function ($value) {
  427. * // return true or false
  428. * }
  429. *
  430. * The closure receives the value as argument and should return true to
  431. * accept the value and false to reject the value.
  432. *
  433. * @param string $option The option name
  434. * @param mixed $allowedValues One or more acceptable values/closures
  435. *
  436. * @return $this
  437. *
  438. * @throws UndefinedOptionsException If the option is undefined
  439. * @throws AccessException If called from a lazy option or normalizer
  440. */
  441. public function addAllowedValues($option, $allowedValues = null)
  442. {
  443. if ($this->locked) {
  444. throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.');
  445. }
  446. // BC
  447. if (\is_array($option) && null === $allowedValues) {
  448. @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);
  449. foreach ($option as $optionName => $optionValues) {
  450. $this->addAllowedValues($optionName, $optionValues);
  451. }
  452. return $this;
  453. }
  454. if (!isset($this->defined[$option])) {
  455. throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
  456. }
  457. if (!\is_array($allowedValues)) {
  458. $allowedValues = array($allowedValues);
  459. }
  460. if (!isset($this->allowedValues[$option])) {
  461. $this->allowedValues[$option] = $allowedValues;
  462. } else {
  463. $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues);
  464. }
  465. // Make sure the option is processed
  466. unset($this->resolved[$option]);
  467. return $this;
  468. }
  469. /**
  470. * Sets allowed types for an option.
  471. *
  472. * Any type for which a corresponding is_<type>() function exists is
  473. * acceptable. Additionally, fully-qualified class or interface names may
  474. * be passed.
  475. *
  476. * @param string $option The option name
  477. * @param string|string[] $allowedTypes One or more accepted types
  478. *
  479. * @return $this
  480. *
  481. * @throws UndefinedOptionsException If the option is undefined
  482. * @throws AccessException If called from a lazy option or normalizer
  483. */
  484. public function setAllowedTypes($option, $allowedTypes = null)
  485. {
  486. if ($this->locked) {
  487. throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.');
  488. }
  489. // BC
  490. if (\is_array($option) && null === $allowedTypes) {
  491. @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);
  492. foreach ($option as $optionName => $optionTypes) {
  493. $this->setAllowedTypes($optionName, $optionTypes);
  494. }
  495. return $this;
  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. $this->allowedTypes[$option] = (array) $allowedTypes;
  501. // Make sure the option is processed
  502. unset($this->resolved[$option]);
  503. return $this;
  504. }
  505. /**
  506. * Adds allowed types for an option.
  507. *
  508. * The types are merged with the allowed types defined previously.
  509. *
  510. * Any type for which a corresponding is_<type>() function exists is
  511. * acceptable. Additionally, fully-qualified class or interface names may
  512. * be passed.
  513. *
  514. * @param string $option The option name
  515. * @param string|string[] $allowedTypes One or more accepted types
  516. *
  517. * @return $this
  518. *
  519. * @throws UndefinedOptionsException If the option is undefined
  520. * @throws AccessException If called from a lazy option or normalizer
  521. */
  522. public function addAllowedTypes($option, $allowedTypes = null)
  523. {
  524. if ($this->locked) {
  525. throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.');
  526. }
  527. // BC
  528. if (\is_array($option) && null === $allowedTypes) {
  529. @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);
  530. foreach ($option as $optionName => $optionTypes) {
  531. $this->addAllowedTypes($optionName, $optionTypes);
  532. }
  533. return $this;
  534. }
  535. if (!isset($this->defined[$option])) {
  536. throw new UndefinedOptionsException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
  537. }
  538. if (!isset($this->allowedTypes[$option])) {
  539. $this->allowedTypes[$option] = (array) $allowedTypes;
  540. } else {
  541. $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes);
  542. }
  543. // Make sure the option is processed
  544. unset($this->resolved[$option]);
  545. return $this;
  546. }
  547. /**
  548. * Removes the option with the given name.
  549. *
  550. * Undefined options are ignored.
  551. *
  552. * @param string|string[] $optionNames One or more option names
  553. *
  554. * @return $this
  555. *
  556. * @throws AccessException If called from a lazy option or normalizer
  557. */
  558. public function remove($optionNames)
  559. {
  560. if ($this->locked) {
  561. throw new AccessException('Options cannot be removed from a lazy option or normalizer.');
  562. }
  563. foreach ((array) $optionNames as $option) {
  564. unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]);
  565. unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]);
  566. }
  567. return $this;
  568. }
  569. /**
  570. * Removes all options.
  571. *
  572. * @return $this
  573. *
  574. * @throws AccessException If called from a lazy option or normalizer
  575. */
  576. public function clear()
  577. {
  578. if ($this->locked) {
  579. throw new AccessException('Options cannot be cleared from a lazy option or normalizer.');
  580. }
  581. $this->defined = array();
  582. $this->defaults = array();
  583. $this->required = array();
  584. $this->resolved = array();
  585. $this->lazy = array();
  586. $this->normalizers = array();
  587. $this->allowedTypes = array();
  588. $this->allowedValues = array();
  589. return $this;
  590. }
  591. /**
  592. * Merges options with the default values stored in the container and
  593. * validates them.
  594. *
  595. * Exceptions are thrown if:
  596. *
  597. * - Undefined options are passed;
  598. * - Required options are missing;
  599. * - Options have invalid types;
  600. * - Options have invalid values.
  601. *
  602. * @param array $options A map of option names to values
  603. *
  604. * @return array The merged and validated options
  605. *
  606. * @throws UndefinedOptionsException If an option name is undefined
  607. * @throws InvalidOptionsException If an option doesn't fulfill the
  608. * specified validation rules
  609. * @throws MissingOptionsException If a required option is missing
  610. * @throws OptionDefinitionException If there is a cyclic dependency between
  611. * lazy options and/or normalizers
  612. * @throws NoSuchOptionException If a lazy option reads an unavailable option
  613. * @throws AccessException If called from a lazy option or normalizer
  614. */
  615. public function resolve(array $options = array())
  616. {
  617. if ($this->locked) {
  618. throw new AccessException('Options cannot be resolved from a lazy option or normalizer.');
  619. }
  620. // Allow this method to be called multiple times
  621. $clone = clone $this;
  622. // Make sure that no unknown options are passed
  623. $diff = array_diff_key($options, $clone->defined);
  624. if (\count($diff) > 0) {
  625. ksort($clone->defined);
  626. ksort($diff);
  627. 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))));
  628. }
  629. // Override options set by the user
  630. foreach ($options as $option => $value) {
  631. $clone->defaults[$option] = $value;
  632. unset($clone->resolved[$option], $clone->lazy[$option]);
  633. }
  634. // Check whether any required option is missing
  635. $diff = array_diff_key($clone->required, $clone->defaults);
  636. if (\count($diff) > 0) {
  637. ksort($diff);
  638. throw new MissingOptionsException(sprintf(\count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', implode('", "', array_keys($diff))));
  639. }
  640. // Lock the container
  641. $clone->locked = true;
  642. // Now process the individual options. Use offsetGet(), which resolves
  643. // the option itself and any options that the option depends on
  644. foreach ($clone->defaults as $option => $_) {
  645. $clone->offsetGet($option);
  646. }
  647. return $clone->resolved;
  648. }
  649. /**
  650. * Returns the resolved value of an option.
  651. *
  652. * @param string $option The option name
  653. *
  654. * @return mixed The option value
  655. *
  656. * @throws AccessException If accessing this method outside of
  657. * {@link resolve()}
  658. * @throws NoSuchOptionException If the option is not set
  659. * @throws InvalidOptionsException If the option doesn't fulfill the
  660. * specified validation rules
  661. * @throws OptionDefinitionException If there is a cyclic dependency between
  662. * lazy options and/or normalizers
  663. */
  664. public function offsetGet($option)
  665. {
  666. if (!$this->locked) {
  667. throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
  668. }
  669. // Shortcut for resolved options
  670. if (array_key_exists($option, $this->resolved)) {
  671. return $this->resolved[$option];
  672. }
  673. // Check whether the option is set at all
  674. if (!array_key_exists($option, $this->defaults)) {
  675. if (!isset($this->defined[$option])) {
  676. throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $option, implode('", "', array_keys($this->defined))));
  677. }
  678. 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));
  679. }
  680. $value = $this->defaults[$option];
  681. // Resolve the option if the default value is lazily evaluated
  682. if (isset($this->lazy[$option])) {
  683. // If the closure is already being called, we have a cyclic
  684. // dependency
  685. if (isset($this->calling[$option])) {
  686. throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
  687. }
  688. // The following section must be protected from cyclic
  689. // calls. Set $calling for the current $option to detect a cyclic
  690. // dependency
  691. // BEGIN
  692. $this->calling[$option] = true;
  693. try {
  694. foreach ($this->lazy[$option] as $closure) {
  695. $value = $closure($this, $value);
  696. }
  697. } catch (\Exception $e) {
  698. unset($this->calling[$option]);
  699. throw $e;
  700. } catch (\Throwable $e) {
  701. unset($this->calling[$option]);
  702. throw $e;
  703. }
  704. unset($this->calling[$option]);
  705. // END
  706. }
  707. // Validate the type of the resolved option
  708. if (isset($this->allowedTypes[$option])) {
  709. $valid = false;
  710. foreach ($this->allowedTypes[$option] as $type) {
  711. $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type;
  712. if (\function_exists($isFunction = 'is_'.$type)) {
  713. if ($isFunction($value)) {
  714. $valid = true;
  715. break;
  716. }
  717. continue;
  718. }
  719. if ($value instanceof $type) {
  720. $valid = true;
  721. break;
  722. }
  723. }
  724. if (!$valid) {
  725. 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]), $this->formatTypeOf($value)));
  726. }
  727. }
  728. // Validate the value of the resolved option
  729. if (isset($this->allowedValues[$option])) {
  730. $success = false;
  731. $printableAllowedValues = array();
  732. foreach ($this->allowedValues[$option] as $allowedValue) {
  733. if ($allowedValue instanceof \Closure) {
  734. if ($allowedValue($value)) {
  735. $success = true;
  736. break;
  737. }
  738. // Don't include closures in the exception message
  739. continue;
  740. } elseif ($value === $allowedValue) {
  741. $success = true;
  742. break;
  743. }
  744. $printableAllowedValues[] = $allowedValue;
  745. }
  746. if (!$success) {
  747. $message = sprintf(
  748. 'The option "%s" with value %s is invalid.',
  749. $option,
  750. $this->formatValue($value)
  751. );
  752. if (\count($printableAllowedValues) > 0) {
  753. $message .= sprintf(
  754. ' Accepted values are: %s.',
  755. $this->formatValues($printableAllowedValues)
  756. );
  757. }
  758. throw new InvalidOptionsException($message);
  759. }
  760. }
  761. // Normalize the validated option
  762. if (isset($this->normalizers[$option])) {
  763. // If the closure is already being called, we have a cyclic
  764. // dependency
  765. if (isset($this->calling[$option])) {
  766. throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', array_keys($this->calling))));
  767. }
  768. $normalizer = $this->normalizers[$option];
  769. // The following section must be protected from cyclic
  770. // calls. Set $calling for the current $option to detect a cyclic
  771. // dependency
  772. // BEGIN
  773. $this->calling[$option] = true;
  774. try {
  775. $value = $normalizer($this, $value);
  776. } catch (\Exception $e) {
  777. unset($this->calling[$option]);
  778. throw $e;
  779. } catch (\Throwable $e) {
  780. unset($this->calling[$option]);
  781. throw $e;
  782. }
  783. unset($this->calling[$option]);
  784. // END
  785. }
  786. // Mark as resolved
  787. $this->resolved[$option] = $value;
  788. return $value;
  789. }
  790. /**
  791. * Returns whether a resolved option with the given name exists.
  792. *
  793. * @param string $option The option name
  794. *
  795. * @return bool Whether the option is set
  796. *
  797. * @throws AccessException If accessing this method outside of {@link resolve()}
  798. *
  799. * @see \ArrayAccess::offsetExists()
  800. */
  801. public function offsetExists($option)
  802. {
  803. if (!$this->locked) {
  804. throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
  805. }
  806. return array_key_exists($option, $this->defaults);
  807. }
  808. /**
  809. * Not supported.
  810. *
  811. * @throws AccessException
  812. */
  813. public function offsetSet($option, $value)
  814. {
  815. throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.');
  816. }
  817. /**
  818. * Not supported.
  819. *
  820. * @throws AccessException
  821. */
  822. public function offsetUnset($option)
  823. {
  824. throw new AccessException('Removing options via array access is not supported. Use remove() instead.');
  825. }
  826. /**
  827. * Returns the number of set options.
  828. *
  829. * This may be only a subset of the defined options.
  830. *
  831. * @return int Number of options
  832. *
  833. * @throws AccessException If accessing this method outside of {@link resolve()}
  834. *
  835. * @see \Countable::count()
  836. */
  837. public function count()
  838. {
  839. if (!$this->locked) {
  840. throw new AccessException('Counting is only supported within closures of lazy options and normalizers.');
  841. }
  842. return \count($this->defaults);
  843. }
  844. /**
  845. * Alias of {@link setDefault()}.
  846. *
  847. * @deprecated since version 2.6, to be removed in 3.0.
  848. */
  849. public function set($option, $value)
  850. {
  851. @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);
  852. return $this->setDefault($option, $value);
  853. }
  854. /**
  855. * Shortcut for {@link clear()} and {@link setDefaults()}.
  856. *
  857. * @deprecated since version 2.6, to be removed in 3.0.
  858. */
  859. public function replace(array $defaults)
  860. {
  861. @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);
  862. $this->clear();
  863. return $this->setDefaults($defaults);
  864. }
  865. /**
  866. * Alias of {@link setDefault()}.
  867. *
  868. * @deprecated since version 2.6, to be removed in 3.0.
  869. */
  870. public function overload($option, $value)
  871. {
  872. @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);
  873. return $this->setDefault($option, $value);
  874. }
  875. /**
  876. * Alias of {@link offsetGet()}.
  877. *
  878. * @deprecated since version 2.6, to be removed in 3.0.
  879. */
  880. public function get($option)
  881. {
  882. @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);
  883. return $this->offsetGet($option);
  884. }
  885. /**
  886. * Alias of {@link offsetExists()}.
  887. *
  888. * @deprecated since version 2.6, to be removed in 3.0.
  889. */
  890. public function has($option)
  891. {
  892. @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);
  893. return $this->offsetExists($option);
  894. }
  895. /**
  896. * Shortcut for {@link clear()} and {@link setDefaults()}.
  897. *
  898. * @deprecated since version 2.6, to be removed in 3.0.
  899. */
  900. public function replaceDefaults(array $defaultValues)
  901. {
  902. @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);
  903. $this->clear();
  904. return $this->setDefaults($defaultValues);
  905. }
  906. /**
  907. * Alias of {@link setDefined()}.
  908. *
  909. * @deprecated since version 2.6, to be removed in 3.0.
  910. */
  911. public function setOptional(array $optionNames)
  912. {
  913. @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);
  914. return $this->setDefined($optionNames);
  915. }
  916. /**
  917. * Alias of {@link isDefined()}.
  918. *
  919. * @deprecated since version 2.6, to be removed in 3.0.
  920. */
  921. public function isKnown($option)
  922. {
  923. @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);
  924. return $this->isDefined($option);
  925. }
  926. /**
  927. * Returns a string representation of the type of the value.
  928. *
  929. * This method should be used if you pass the type of a value as
  930. * message parameter to a constraint violation. Note that such
  931. * parameters should usually not be included in messages aimed at
  932. * non-technical people.
  933. *
  934. * @param mixed $value The value to return the type of
  935. *
  936. * @return string The type of the value
  937. */
  938. private function formatTypeOf($value)
  939. {
  940. return \is_object($value) ? \get_class($value) : \gettype($value);
  941. }
  942. /**
  943. * Returns a string representation of the value.
  944. *
  945. * This method returns the equivalent PHP tokens for most scalar types
  946. * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped
  947. * in double quotes (").
  948. *
  949. * @param mixed $value The value to format as string
  950. *
  951. * @return string The string representation of the passed value
  952. */
  953. private function formatValue($value)
  954. {
  955. if (\is_object($value)) {
  956. return \get_class($value);
  957. }
  958. if (\is_array($value)) {
  959. return 'array';
  960. }
  961. if (\is_string($value)) {
  962. return '"'.$value.'"';
  963. }
  964. if (\is_resource($value)) {
  965. return 'resource';
  966. }
  967. if (null === $value) {
  968. return 'null';
  969. }
  970. if (false === $value) {
  971. return 'false';
  972. }
  973. if (true === $value) {
  974. return 'true';
  975. }
  976. return (string) $value;
  977. }
  978. /**
  979. * Returns a string representation of a list of values.
  980. *
  981. * Each of the values is converted to a string using
  982. * {@link formatValue()}. The values are then concatenated with commas.
  983. *
  984. * @param array $values A list of values
  985. *
  986. * @return string The string representation of the value list
  987. *
  988. * @see formatValue()
  989. */
  990. private function formatValues(array $values)
  991. {
  992. foreach ($values as $key => $value) {
  993. $values[$key] = $this->formatValue($value);
  994. }
  995. return implode(', ', $values);
  996. }
  997. }