OptionsResolver.php 45 KB

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