OptionsResolver.php 33 KB

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