OptionsResolver.php 40 KB

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