OptionsResolver.php 44 KB

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