OptionsResolver.php 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347
  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
  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
  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[]
  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
  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[]
  310. */
  311. public function getMissingOptions()
  312. {
  313. return array_keys(array_diff_key($this->required, $this->defaults));
  314. }
  315. /**
  316. * Defines a valid option name.
  317. *
  318. * Defines an option name without setting a default value. The option will
  319. * be accepted when passed to {@link resolve()}. When not passed, the
  320. * option will not be included in the resolved options.
  321. *
  322. * @param string|string[] $optionNames One or more option names
  323. *
  324. * @return $this
  325. *
  326. * @throws AccessException If called from a lazy option or normalizer
  327. */
  328. public function setDefined($optionNames)
  329. {
  330. if ($this->locked) {
  331. throw new AccessException('Options cannot be defined from a lazy option or normalizer.');
  332. }
  333. foreach ((array) $optionNames as $option) {
  334. $this->defined[$option] = true;
  335. }
  336. return $this;
  337. }
  338. /**
  339. * Returns whether an option is defined.
  340. *
  341. * Returns true for any option passed to {@link setDefault()},
  342. * {@link setRequired()} or {@link setDefined()}.
  343. *
  344. * @return bool
  345. */
  346. public function isDefined(string $option)
  347. {
  348. return isset($this->defined[$option]);
  349. }
  350. /**
  351. * Returns the names of all defined options.
  352. *
  353. * @return string[]
  354. *
  355. * @see isDefined()
  356. */
  357. public function getDefinedOptions()
  358. {
  359. return array_keys($this->defined);
  360. }
  361. public function isNested(string $option): bool
  362. {
  363. return isset($this->nested[$option]);
  364. }
  365. /**
  366. * Deprecates an option, allowed types or values.
  367. *
  368. * Instead of passing the message, you may also pass a closure with the
  369. * following signature:
  370. *
  371. * function (Options $options, $value): string {
  372. * // ...
  373. * }
  374. *
  375. * The closure receives the value as argument and should return a string.
  376. * Return an empty string to ignore the option deprecation.
  377. *
  378. * The closure is invoked when {@link resolve()} is called. The parameter
  379. * passed to the closure is the value of the option after validating it
  380. * and before normalizing it.
  381. *
  382. * @param string $package The name of the composer package that is triggering the deprecation
  383. * @param string $version The version of the package that introduced the deprecation
  384. * @param string|\Closure $message The deprecation message to use
  385. *
  386. * @return $this
  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
  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
  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. #[\ReturnTypeWillChange]
  821. public function offsetGet($option, bool $triggerDeprecation = true)
  822. {
  823. if (!$this->locked) {
  824. throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
  825. }
  826. // Shortcut for resolved options
  827. if (isset($this->resolved[$option]) || \array_key_exists($option, $this->resolved)) {
  828. if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || $this->calling) && \is_string($this->deprecated[$option]['message'])) {
  829. trigger_deprecation($this->deprecated[$option]['package'], $this->deprecated[$option]['version'], strtr($this->deprecated[$option]['message'], ['%name%' => $option]));
  830. }
  831. return $this->resolved[$option];
  832. }
  833. // Check whether the option is set at all
  834. if (!isset($this->defaults[$option]) && !\array_key_exists($option, $this->defaults)) {
  835. if (!isset($this->defined[$option])) {
  836. throw new NoSuchOptionException(sprintf('The option "%s" does not exist. Defined options are: "%s".', $this->formatOptions([$option]), implode('", "', array_keys($this->defined))));
  837. }
  838. 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])));
  839. }
  840. $value = $this->defaults[$option];
  841. // Resolve the option if it is a nested definition
  842. if (isset($this->nested[$option])) {
  843. // If the closure is already being called, we have a cyclic dependency
  844. if (isset($this->calling[$option])) {
  845. throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
  846. }
  847. if (!\is_array($value)) {
  848. 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)));
  849. }
  850. // The following section must be protected from cyclic calls.
  851. $this->calling[$option] = true;
  852. try {
  853. $resolver = new self();
  854. $resolver->prototype = false;
  855. $resolver->parentsOptions = $this->parentsOptions;
  856. $resolver->parentsOptions[] = $option;
  857. foreach ($this->nested[$option] as $closure) {
  858. $closure($resolver, $this);
  859. }
  860. if ($resolver->prototype) {
  861. $values = [];
  862. foreach ($value as $index => $prototypeValue) {
  863. if (!\is_array($prototypeValue)) {
  864. 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)));
  865. }
  866. $resolver->prototypeIndex = $index;
  867. $values[$index] = $resolver->resolve($prototypeValue);
  868. }
  869. $value = $values;
  870. } else {
  871. $value = $resolver->resolve($value);
  872. }
  873. } finally {
  874. $resolver->prototypeIndex = null;
  875. unset($this->calling[$option]);
  876. }
  877. }
  878. // Resolve the option if the default value is lazily evaluated
  879. if (isset($this->lazy[$option])) {
  880. // If the closure is already being called, we have a cyclic
  881. // dependency
  882. if (isset($this->calling[$option])) {
  883. throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
  884. }
  885. // The following section must be protected from cyclic
  886. // calls. Set $calling for the current $option to detect a cyclic
  887. // dependency
  888. // BEGIN
  889. $this->calling[$option] = true;
  890. try {
  891. foreach ($this->lazy[$option] as $closure) {
  892. $value = $closure($this, $value);
  893. }
  894. } finally {
  895. unset($this->calling[$option]);
  896. }
  897. // END
  898. }
  899. // Validate the type of the resolved option
  900. if (isset($this->allowedTypes[$option])) {
  901. $valid = true;
  902. $invalidTypes = [];
  903. foreach ($this->allowedTypes[$option] as $type) {
  904. if ($valid = $this->verifyTypes($type, $value, $invalidTypes)) {
  905. break;
  906. }
  907. }
  908. if (!$valid) {
  909. $fmtActualValue = $this->formatValue($value);
  910. $fmtAllowedTypes = implode('" or "', $this->allowedTypes[$option]);
  911. $fmtProvidedTypes = implode('|', array_keys($invalidTypes));
  912. $allowedContainsArrayType = \count(array_filter($this->allowedTypes[$option], static function ($item) {
  913. return str_ends_with($item, '[]');
  914. })) > 0;
  915. if (\is_array($value) && $allowedContainsArrayType) {
  916. 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));
  917. }
  918. 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));
  919. }
  920. }
  921. // Validate the value of the resolved option
  922. if (isset($this->allowedValues[$option])) {
  923. $success = false;
  924. $printableAllowedValues = [];
  925. foreach ($this->allowedValues[$option] as $allowedValue) {
  926. if ($allowedValue instanceof \Closure) {
  927. if ($allowedValue($value)) {
  928. $success = true;
  929. break;
  930. }
  931. // Don't include closures in the exception message
  932. continue;
  933. }
  934. if ($value === $allowedValue) {
  935. $success = true;
  936. break;
  937. }
  938. $printableAllowedValues[] = $allowedValue;
  939. }
  940. if (!$success) {
  941. $message = sprintf(
  942. 'The option "%s" with value %s is invalid.',
  943. $option,
  944. $this->formatValue($value)
  945. );
  946. if (\count($printableAllowedValues) > 0) {
  947. $message .= sprintf(
  948. ' Accepted values are: %s.',
  949. $this->formatValues($printableAllowedValues)
  950. );
  951. }
  952. if (isset($this->info[$option])) {
  953. $message .= sprintf(' Info: %s.', $this->info[$option]);
  954. }
  955. throw new InvalidOptionsException($message);
  956. }
  957. }
  958. // Check whether the option is deprecated
  959. // and it is provided by the user or is being called from a lazy evaluation
  960. if ($triggerDeprecation && isset($this->deprecated[$option]) && (isset($this->given[$option]) || ($this->calling && \is_string($this->deprecated[$option]['message'])))) {
  961. $deprecation = $this->deprecated[$option];
  962. $message = $this->deprecated[$option]['message'];
  963. if ($message instanceof \Closure) {
  964. // If the closure is already being called, we have a cyclic dependency
  965. if (isset($this->calling[$option])) {
  966. throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
  967. }
  968. $this->calling[$option] = true;
  969. try {
  970. if (!\is_string($message = $message($this, $value))) {
  971. throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", return an empty string to ignore.', get_debug_type($message)));
  972. }
  973. } finally {
  974. unset($this->calling[$option]);
  975. }
  976. }
  977. if ('' !== $message) {
  978. trigger_deprecation($deprecation['package'], $deprecation['version'], strtr($message, ['%name%' => $option]));
  979. }
  980. }
  981. // Normalize the validated option
  982. if (isset($this->normalizers[$option])) {
  983. // If the closure is already being called, we have a cyclic
  984. // dependency
  985. if (isset($this->calling[$option])) {
  986. throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', $this->formatOptions(array_keys($this->calling))));
  987. }
  988. // The following section must be protected from cyclic
  989. // calls. Set $calling for the current $option to detect a cyclic
  990. // dependency
  991. // BEGIN
  992. $this->calling[$option] = true;
  993. try {
  994. foreach ($this->normalizers[$option] as $normalizer) {
  995. $value = $normalizer($this, $value);
  996. }
  997. } finally {
  998. unset($this->calling[$option]);
  999. }
  1000. // END
  1001. }
  1002. // Mark as resolved
  1003. $this->resolved[$option] = $value;
  1004. return $value;
  1005. }
  1006. private function verifyTypes(string $type, $value, array &$invalidTypes, int $level = 0): bool
  1007. {
  1008. if (\is_array($value) && '[]' === substr($type, -2)) {
  1009. $type = substr($type, 0, -2);
  1010. $valid = true;
  1011. foreach ($value as $val) {
  1012. if (!$this->verifyTypes($type, $val, $invalidTypes, $level + 1)) {
  1013. $valid = false;
  1014. }
  1015. }
  1016. return $valid;
  1017. }
  1018. if (('null' === $type && null === $value) || (isset(self::VALIDATION_FUNCTIONS[$type]) ? self::VALIDATION_FUNCTIONS[$type]($value) : $value instanceof $type)) {
  1019. return true;
  1020. }
  1021. if (!$invalidTypes || $level > 0) {
  1022. $invalidTypes[get_debug_type($value)] = true;
  1023. }
  1024. return false;
  1025. }
  1026. /**
  1027. * Returns whether a resolved option with the given name exists.
  1028. *
  1029. * @param string $option The option name
  1030. *
  1031. * @return bool
  1032. *
  1033. * @throws AccessException If accessing this method outside of {@link resolve()}
  1034. *
  1035. * @see \ArrayAccess::offsetExists()
  1036. */
  1037. #[\ReturnTypeWillChange]
  1038. public function offsetExists($option)
  1039. {
  1040. if (!$this->locked) {
  1041. throw new AccessException('Array access is only supported within closures of lazy options and normalizers.');
  1042. }
  1043. return \array_key_exists($option, $this->defaults);
  1044. }
  1045. /**
  1046. * Not supported.
  1047. *
  1048. * @return void
  1049. *
  1050. * @throws AccessException
  1051. */
  1052. #[\ReturnTypeWillChange]
  1053. public function offsetSet($option, $value)
  1054. {
  1055. throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.');
  1056. }
  1057. /**
  1058. * Not supported.
  1059. *
  1060. * @return void
  1061. *
  1062. * @throws AccessException
  1063. */
  1064. #[\ReturnTypeWillChange]
  1065. public function offsetUnset($option)
  1066. {
  1067. throw new AccessException('Removing options via array access is not supported. Use remove() instead.');
  1068. }
  1069. /**
  1070. * Returns the number of set options.
  1071. *
  1072. * This may be only a subset of the defined options.
  1073. *
  1074. * @return int
  1075. *
  1076. * @throws AccessException If accessing this method outside of {@link resolve()}
  1077. *
  1078. * @see \Countable::count()
  1079. */
  1080. #[\ReturnTypeWillChange]
  1081. public function count()
  1082. {
  1083. if (!$this->locked) {
  1084. throw new AccessException('Counting is only supported within closures of lazy options and normalizers.');
  1085. }
  1086. return \count($this->defaults);
  1087. }
  1088. /**
  1089. * Returns a string representation of the value.
  1090. *
  1091. * This method returns the equivalent PHP tokens for most scalar types
  1092. * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped
  1093. * in double quotes (").
  1094. *
  1095. * @param mixed $value The value to format as string
  1096. */
  1097. private function formatValue($value): string
  1098. {
  1099. if (\is_object($value)) {
  1100. return \get_class($value);
  1101. }
  1102. if (\is_array($value)) {
  1103. return 'array';
  1104. }
  1105. if (\is_string($value)) {
  1106. return '"'.$value.'"';
  1107. }
  1108. if (\is_resource($value)) {
  1109. return 'resource';
  1110. }
  1111. if (null === $value) {
  1112. return 'null';
  1113. }
  1114. if (false === $value) {
  1115. return 'false';
  1116. }
  1117. if (true === $value) {
  1118. return 'true';
  1119. }
  1120. return (string) $value;
  1121. }
  1122. /**
  1123. * Returns a string representation of a list of values.
  1124. *
  1125. * Each of the values is converted to a string using
  1126. * {@link formatValue()}. The values are then concatenated with commas.
  1127. *
  1128. * @see formatValue()
  1129. */
  1130. private function formatValues(array $values): string
  1131. {
  1132. foreach ($values as $key => $value) {
  1133. $values[$key] = $this->formatValue($value);
  1134. }
  1135. return implode(', ', $values);
  1136. }
  1137. private function formatOptions(array $options): string
  1138. {
  1139. if ($this->parentsOptions) {
  1140. $prefix = array_shift($this->parentsOptions);
  1141. if ($this->parentsOptions) {
  1142. $prefix .= sprintf('[%s]', implode('][', $this->parentsOptions));
  1143. }
  1144. if ($this->prototype && null !== $this->prototypeIndex) {
  1145. $prefix .= sprintf('[%s]', $this->prototypeIndex);
  1146. }
  1147. $options = array_map(static function (string $option) use ($prefix): string {
  1148. return sprintf('%s[%s]', $prefix, $option);
  1149. }, $options);
  1150. }
  1151. return implode('", "', $options);
  1152. }
  1153. private function getParameterClassName(\ReflectionParameter $parameter): ?string
  1154. {
  1155. if (!($type = $parameter->getType()) instanceof \ReflectionNamedType || $type->isBuiltin()) {
  1156. return null;
  1157. }
  1158. return $type->getName();
  1159. }
  1160. }