vendor/twig/twig/src/Extension/CoreExtension.php line 1802

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Twig.
  4. *
  5. * (c) Fabien Potencier
  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 Twig\Extension;
  11. use Twig\Environment;
  12. use Twig\Error\LoaderError;
  13. use Twig\Error\RuntimeError;
  14. use Twig\Error\SyntaxError;
  15. use Twig\ExpressionParser;
  16. use Twig\Markup;
  17. use Twig\Node\Expression\AbstractExpression;
  18. use Twig\Node\Expression\Binary\AddBinary;
  19. use Twig\Node\Expression\Binary\AndBinary;
  20. use Twig\Node\Expression\Binary\BitwiseAndBinary;
  21. use Twig\Node\Expression\Binary\BitwiseOrBinary;
  22. use Twig\Node\Expression\Binary\BitwiseXorBinary;
  23. use Twig\Node\Expression\Binary\ConcatBinary;
  24. use Twig\Node\Expression\Binary\DivBinary;
  25. use Twig\Node\Expression\Binary\EndsWithBinary;
  26. use Twig\Node\Expression\Binary\EqualBinary;
  27. use Twig\Node\Expression\Binary\FloorDivBinary;
  28. use Twig\Node\Expression\Binary\GreaterBinary;
  29. use Twig\Node\Expression\Binary\GreaterEqualBinary;
  30. use Twig\Node\Expression\Binary\HasEveryBinary;
  31. use Twig\Node\Expression\Binary\HasSomeBinary;
  32. use Twig\Node\Expression\Binary\InBinary;
  33. use Twig\Node\Expression\Binary\LessBinary;
  34. use Twig\Node\Expression\Binary\LessEqualBinary;
  35. use Twig\Node\Expression\Binary\MatchesBinary;
  36. use Twig\Node\Expression\Binary\ModBinary;
  37. use Twig\Node\Expression\Binary\MulBinary;
  38. use Twig\Node\Expression\Binary\NotEqualBinary;
  39. use Twig\Node\Expression\Binary\NotInBinary;
  40. use Twig\Node\Expression\Binary\OrBinary;
  41. use Twig\Node\Expression\Binary\PowerBinary;
  42. use Twig\Node\Expression\Binary\RangeBinary;
  43. use Twig\Node\Expression\Binary\SpaceshipBinary;
  44. use Twig\Node\Expression\Binary\StartsWithBinary;
  45. use Twig\Node\Expression\Binary\SubBinary;
  46. use Twig\Node\Expression\BlockReferenceExpression;
  47. use Twig\Node\Expression\Filter\DefaultFilter;
  48. use Twig\Node\Expression\FunctionNode\EnumCasesFunction;
  49. use Twig\Node\Expression\GetAttrExpression;
  50. use Twig\Node\Expression\NullCoalesceExpression;
  51. use Twig\Node\Expression\ParentExpression;
  52. use Twig\Node\Expression\Test\ConstantTest;
  53. use Twig\Node\Expression\Test\DefinedTest;
  54. use Twig\Node\Expression\Test\DivisiblebyTest;
  55. use Twig\Node\Expression\Test\EvenTest;
  56. use Twig\Node\Expression\Test\NullTest;
  57. use Twig\Node\Expression\Test\OddTest;
  58. use Twig\Node\Expression\Test\SameasTest;
  59. use Twig\Node\Expression\Unary\NegUnary;
  60. use Twig\Node\Expression\Unary\NotUnary;
  61. use Twig\Node\Expression\Unary\PosUnary;
  62. use Twig\Node\Node;
  63. use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
  64. use Twig\Parser;
  65. use Twig\Sandbox\SecurityNotAllowedMethodError;
  66. use Twig\Sandbox\SecurityNotAllowedPropertyError;
  67. use Twig\Source;
  68. use Twig\Template;
  69. use Twig\TemplateWrapper;
  70. use Twig\TokenParser\ApplyTokenParser;
  71. use Twig\TokenParser\BlockTokenParser;
  72. use Twig\TokenParser\DeprecatedTokenParser;
  73. use Twig\TokenParser\DoTokenParser;
  74. use Twig\TokenParser\EmbedTokenParser;
  75. use Twig\TokenParser\ExtendsTokenParser;
  76. use Twig\TokenParser\FlushTokenParser;
  77. use Twig\TokenParser\ForTokenParser;
  78. use Twig\TokenParser\FromTokenParser;
  79. use Twig\TokenParser\IfTokenParser;
  80. use Twig\TokenParser\ImportTokenParser;
  81. use Twig\TokenParser\IncludeTokenParser;
  82. use Twig\TokenParser\MacroTokenParser;
  83. use Twig\TokenParser\SetTokenParser;
  84. use Twig\TokenParser\TypesTokenParser;
  85. use Twig\TokenParser\UseTokenParser;
  86. use Twig\TokenParser\WithTokenParser;
  87. use Twig\TwigFilter;
  88. use Twig\TwigFunction;
  89. use Twig\TwigTest;
  90. use Twig\Util\CallableArgumentsExtractor;
  91. final class CoreExtension extends AbstractExtension
  92. {
  93. public const ARRAY_LIKE_CLASSES = [
  94. 'ArrayIterator',
  95. 'ArrayObject',
  96. 'CachingIterator',
  97. 'RecursiveArrayIterator',
  98. 'RecursiveCachingIterator',
  99. 'SplDoublyLinkedList',
  100. 'SplFixedArray',
  101. 'SplObjectStorage',
  102. 'SplQueue',
  103. 'SplStack',
  104. 'WeakMap',
  105. ];
  106. private $dateFormats = ['F j, Y H:i', '%d days'];
  107. private $numberFormat = [0, '.', ','];
  108. private $timezone = null;
  109. /**
  110. * Sets the default format to be used by the date filter.
  111. *
  112. * @param string|null $format The default date format string
  113. * @param string|null $dateIntervalFormat The default date interval format string
  114. */
  115. public function setDateFormat($format = null, $dateIntervalFormat = null)
  116. {
  117. if (null !== $format) {
  118. $this->dateFormats[0] = $format;
  119. }
  120. if (null !== $dateIntervalFormat) {
  121. $this->dateFormats[1] = $dateIntervalFormat;
  122. }
  123. }
  124. /**
  125. * Gets the default format to be used by the date filter.
  126. *
  127. * @return array The default date format string and the default date interval format string
  128. */
  129. public function getDateFormat()
  130. {
  131. return $this->dateFormats;
  132. }
  133. /**
  134. * Sets the default timezone to be used by the date filter.
  135. *
  136. * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object
  137. */
  138. public function setTimezone($timezone)
  139. {
  140. $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone);
  141. }
  142. /**
  143. * Gets the default timezone to be used by the date filter.
  144. *
  145. * @return \DateTimeZone The default timezone currently in use
  146. */
  147. public function getTimezone()
  148. {
  149. if (null === $this->timezone) {
  150. $this->timezone = new \DateTimeZone(date_default_timezone_get());
  151. }
  152. return $this->timezone;
  153. }
  154. /**
  155. * Sets the default format to be used by the number_format filter.
  156. *
  157. * @param int $decimal the number of decimal places to use
  158. * @param string $decimalPoint the character(s) to use for the decimal point
  159. * @param string $thousandSep the character(s) to use for the thousands separator
  160. */
  161. public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
  162. {
  163. $this->numberFormat = [$decimal, $decimalPoint, $thousandSep];
  164. }
  165. /**
  166. * Get the default format used by the number_format filter.
  167. *
  168. * @return array The arguments for number_format()
  169. */
  170. public function getNumberFormat()
  171. {
  172. return $this->numberFormat;
  173. }
  174. public function getTokenParsers(): array
  175. {
  176. return [
  177. new ApplyTokenParser(),
  178. new ForTokenParser(),
  179. new IfTokenParser(),
  180. new ExtendsTokenParser(),
  181. new IncludeTokenParser(),
  182. new BlockTokenParser(),
  183. new UseTokenParser(),
  184. new MacroTokenParser(),
  185. new ImportTokenParser(),
  186. new FromTokenParser(),
  187. new SetTokenParser(),
  188. new TypesTokenParser(),
  189. new FlushTokenParser(),
  190. new DoTokenParser(),
  191. new EmbedTokenParser(),
  192. new WithTokenParser(),
  193. new DeprecatedTokenParser(),
  194. ];
  195. }
  196. public function getFilters(): array
  197. {
  198. return [
  199. // formatting filters
  200. new TwigFilter('date', [$this, 'formatDate']),
  201. new TwigFilter('date_modify', [$this, 'modifyDate']),
  202. new TwigFilter('format', [self::class, 'sprintf']),
  203. new TwigFilter('replace', [self::class, 'replace']),
  204. new TwigFilter('number_format', [$this, 'formatNumber']),
  205. new TwigFilter('abs', 'abs'),
  206. new TwigFilter('round', [self::class, 'round']),
  207. // encoding
  208. new TwigFilter('url_encode', [self::class, 'urlencode']),
  209. new TwigFilter('json_encode', 'json_encode'),
  210. new TwigFilter('convert_encoding', [self::class, 'convertEncoding']),
  211. // string filters
  212. new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]),
  213. new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]),
  214. new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]),
  215. new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]),
  216. new TwigFilter('striptags', [self::class, 'striptags']),
  217. new TwigFilter('trim', [self::class, 'trim']),
  218. new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
  219. new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig']),
  220. // array helpers
  221. new TwigFilter('join', [self::class, 'join']),
  222. new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]),
  223. new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]),
  224. new TwigFilter('merge', [self::class, 'merge']),
  225. new TwigFilter('batch', [self::class, 'batch']),
  226. new TwigFilter('column', [self::class, 'column']),
  227. new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]),
  228. new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]),
  229. new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]),
  230. new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true]),
  231. // string/array filters
  232. new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]),
  233. new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]),
  234. new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]),
  235. new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]),
  236. new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]),
  237. new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]),
  238. // iteration and runtime
  239. new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]),
  240. new TwigFilter('keys', [self::class, 'keys']),
  241. ];
  242. }
  243. public function getFunctions(): array
  244. {
  245. return [
  246. new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]),
  247. new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]),
  248. new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]),
  249. new TwigFunction('max', 'max'),
  250. new TwigFunction('min', 'min'),
  251. new TwigFunction('range', 'range'),
  252. new TwigFunction('constant', [self::class, 'constant']),
  253. new TwigFunction('cycle', [self::class, 'cycle']),
  254. new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]),
  255. new TwigFunction('date', [$this, 'convertDate']),
  256. new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
  257. new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]),
  258. new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]),
  259. ];
  260. }
  261. public function getTests(): array
  262. {
  263. return [
  264. new TwigTest('even', null, ['node_class' => EvenTest::class]),
  265. new TwigTest('odd', null, ['node_class' => OddTest::class]),
  266. new TwigTest('defined', null, ['node_class' => DefinedTest::class]),
  267. new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true]),
  268. new TwigTest('none', null, ['node_class' => NullTest::class]),
  269. new TwigTest('null', null, ['node_class' => NullTest::class]),
  270. new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
  271. new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
  272. new TwigTest('empty', [self::class, 'testEmpty']),
  273. new TwigTest('iterable', 'is_iterable'),
  274. new TwigTest('sequence', [self::class, 'testSequence']),
  275. new TwigTest('mapping', [self::class, 'testMapping']),
  276. ];
  277. }
  278. public function getNodeVisitors(): array
  279. {
  280. return [new MacroAutoImportNodeVisitor()];
  281. }
  282. public function getOperators(): array
  283. {
  284. return [
  285. [
  286. 'not' => ['precedence' => 50, 'class' => NotUnary::class],
  287. '-' => ['precedence' => 500, 'class' => NegUnary::class],
  288. '+' => ['precedence' => 500, 'class' => PosUnary::class],
  289. ],
  290. [
  291. 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  292. 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  293. 'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  294. 'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  295. 'b-and' => ['precedence' => 18, 'class' => BitwiseAndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  296. '==' => ['precedence' => 20, 'class' => EqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  297. '!=' => ['precedence' => 20, 'class' => NotEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  298. '<=>' => ['precedence' => 20, 'class' => SpaceshipBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  299. '<' => ['precedence' => 20, 'class' => LessBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  300. '>' => ['precedence' => 20, 'class' => GreaterBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  301. '>=' => ['precedence' => 20, 'class' => GreaterEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  302. '<=' => ['precedence' => 20, 'class' => LessEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  303. 'not in' => ['precedence' => 20, 'class' => NotInBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  304. 'in' => ['precedence' => 20, 'class' => InBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  305. 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  306. 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  307. 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  308. 'has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  309. 'has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  310. '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  311. '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  312. '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  313. '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  314. '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  315. '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  316. '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  317. '%' => ['precedence' => 60, 'class' => ModBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  318. 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  319. 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  320. '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
  321. '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
  322. ],
  323. ];
  324. }
  325. /**
  326. * Cycles over a sequence.
  327. *
  328. * @param array|\ArrayAccess $values A non-empty sequence of values
  329. * @param positive-int $position The position of the value to return in the cycle
  330. *
  331. * @return mixed The value at the given position in the sequence, wrapping around as needed
  332. *
  333. * @internal
  334. */
  335. public static function cycle($values, $position): mixed
  336. {
  337. if (!\is_array($values)) {
  338. if (!$values instanceof \ArrayAccess) {
  339. throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.');
  340. }
  341. if (!is_countable($values)) {
  342. // To be uncommented in 4.0
  343. // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.');
  344. trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__);
  345. return $values;
  346. }
  347. $values = self::toArray($values, false);
  348. }
  349. if (!$count = \count($values)) {
  350. throw new RuntimeError('The "cycle" function does not work on empty sequences.');
  351. }
  352. return $values[$position % $count];
  353. }
  354. /**
  355. * Returns a random value depending on the supplied parameter type:
  356. * - a random item from a \Traversable or array
  357. * - a random character from a string
  358. * - a random integer between 0 and the integer parameter.
  359. *
  360. * @param \Traversable|array|int|float|string $values The values to pick a random item from
  361. * @param int|null $max Maximum value used when $values is an int
  362. *
  363. * @return mixed A random value from the given sequence
  364. *
  365. * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
  366. *
  367. * @internal
  368. */
  369. public static function random(string $charset, $values = null, $max = null)
  370. {
  371. if (null === $values) {
  372. return null === $max ? mt_rand() : mt_rand(0, (int) $max);
  373. }
  374. if (\is_int($values) || \is_float($values)) {
  375. if (null === $max) {
  376. if ($values < 0) {
  377. $max = 0;
  378. $min = $values;
  379. } else {
  380. $max = $values;
  381. $min = 0;
  382. }
  383. } else {
  384. $min = $values;
  385. }
  386. return mt_rand((int) $min, (int) $max);
  387. }
  388. if (\is_string($values)) {
  389. if ('' === $values) {
  390. return '';
  391. }
  392. if ('UTF-8' !== $charset) {
  393. $values = self::convertEncoding($values, 'UTF-8', $charset);
  394. }
  395. // unicode version of str_split()
  396. // split at all positions, but not after the start and not before the end
  397. $values = preg_split('/(?<!^)(?!$)/u', $values);
  398. if ('UTF-8' !== $charset) {
  399. foreach ($values as $i => $value) {
  400. $values[$i] = self::convertEncoding($value, $charset, 'UTF-8');
  401. }
  402. }
  403. }
  404. if (!is_iterable($values)) {
  405. return $values;
  406. }
  407. $values = self::toArray($values);
  408. if (0 === \count($values)) {
  409. throw new RuntimeError('The random function cannot pick from an empty sequence/mapping.');
  410. }
  411. return $values[array_rand($values, 1)];
  412. }
  413. /**
  414. * Formats a date.
  415. *
  416. * {{ post.published_at|date("m/d/Y") }}
  417. *
  418. * @param \DateTimeInterface|\DateInterval|string $date A date
  419. * @param string|null $format The target format, null to use the default
  420. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  421. */
  422. public function formatDate($date, $format = null, $timezone = null): string
  423. {
  424. if (null === $format) {
  425. $formats = $this->getDateFormat();
  426. $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
  427. }
  428. if ($date instanceof \DateInterval) {
  429. return $date->format($format);
  430. }
  431. return $this->convertDate($date, $timezone)->format($format);
  432. }
  433. /**
  434. * Returns a new date object modified.
  435. *
  436. * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
  437. *
  438. * @param \DateTimeInterface|string $date A date
  439. * @param string $modifier A modifier string
  440. *
  441. * @return \DateTime|\DateTimeImmutable
  442. *
  443. * @internal
  444. */
  445. public function modifyDate($date, $modifier)
  446. {
  447. return $this->convertDate($date, false)->modify($modifier);
  448. }
  449. /**
  450. * Returns a formatted string.
  451. *
  452. * @param string|null $format
  453. * @param ...$values
  454. *
  455. * @internal
  456. */
  457. public static function sprintf($format, ...$values): string
  458. {
  459. return \sprintf($format ?? '', ...$values);
  460. }
  461. /**
  462. * @internal
  463. */
  464. public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string
  465. {
  466. return $env->getExtension(self::class)->formatDate($date, $format, $timezone);
  467. }
  468. /**
  469. * Converts an input to a \DateTime instance.
  470. *
  471. * {% if date(user.created_at) < date('+2days') %}
  472. * {# do something #}
  473. * {% endif %}
  474. *
  475. * @param \DateTimeInterface|string|null $date A date or null to use the current time
  476. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  477. *
  478. * @return \DateTime|\DateTimeImmutable
  479. */
  480. public function convertDate($date = null, $timezone = null)
  481. {
  482. // determine the timezone
  483. if (false !== $timezone) {
  484. if (null === $timezone) {
  485. $timezone = $this->getTimezone();
  486. } elseif (!$timezone instanceof \DateTimeZone) {
  487. $timezone = new \DateTimeZone($timezone);
  488. }
  489. }
  490. // immutable dates
  491. if ($date instanceof \DateTimeImmutable) {
  492. return false !== $timezone ? $date->setTimezone($timezone) : $date;
  493. }
  494. if ($date instanceof \DateTime) {
  495. $date = clone $date;
  496. if (false !== $timezone) {
  497. $date->setTimezone($timezone);
  498. }
  499. return $date;
  500. }
  501. if (null === $date || 'now' === $date) {
  502. if (null === $date) {
  503. $date = 'now';
  504. }
  505. return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone());
  506. }
  507. $asString = (string) $date;
  508. if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
  509. $date = new \DateTime('@'.$date);
  510. } else {
  511. $date = new \DateTime($date, $this->getTimezone());
  512. }
  513. if (false !== $timezone) {
  514. $date->setTimezone($timezone);
  515. }
  516. return $date;
  517. }
  518. /**
  519. * Replaces strings within a string.
  520. *
  521. * @param string|null $str String to replace in
  522. * @param array|\Traversable $from Replace values
  523. *
  524. * @internal
  525. */
  526. public static function replace($str, $from): string
  527. {
  528. if (!is_iterable($from)) {
  529. throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
  530. }
  531. return strtr($str ?? '', self::toArray($from));
  532. }
  533. /**
  534. * Rounds a number.
  535. *
  536. * @param int|float|string|null $value The value to round
  537. * @param int|float $precision The rounding precision
  538. * @param string $method The method to use for rounding
  539. *
  540. * @return int|float The rounded number
  541. *
  542. * @internal
  543. */
  544. public static function round($value, $precision = 0, $method = 'common')
  545. {
  546. $value = (float) $value;
  547. if ('common' === $method) {
  548. return round($value, $precision);
  549. }
  550. if ('ceil' !== $method && 'floor' !== $method) {
  551. throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
  552. }
  553. return $method($value * 10 ** $precision) / 10 ** $precision;
  554. }
  555. /**
  556. * Formats a number.
  557. *
  558. * All of the formatting options can be left null, in that case the defaults will
  559. * be used. Supplying any of the parameters will override the defaults set in the
  560. * environment object.
  561. *
  562. * @param mixed $number A float/int/string of the number to format
  563. * @param int|null $decimal the number of decimal points to display
  564. * @param string|null $decimalPoint the character(s) to use for the decimal point
  565. * @param string|null $thousandSep the character(s) to use for the thousands separator
  566. */
  567. public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string
  568. {
  569. $defaults = $this->getNumberFormat();
  570. if (null === $decimal) {
  571. $decimal = $defaults[0];
  572. }
  573. if (null === $decimalPoint) {
  574. $decimalPoint = $defaults[1];
  575. }
  576. if (null === $thousandSep) {
  577. $thousandSep = $defaults[2];
  578. }
  579. return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
  580. }
  581. /**
  582. * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
  583. *
  584. * @param string|array|null $url A URL or an array of query parameters
  585. *
  586. * @internal
  587. */
  588. public static function urlencode($url): string
  589. {
  590. if (\is_array($url)) {
  591. return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
  592. }
  593. return rawurlencode($url ?? '');
  594. }
  595. /**
  596. * Merges any number of arrays or Traversable objects.
  597. *
  598. * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
  599. *
  600. * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %}
  601. *
  602. * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #}
  603. *
  604. * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge
  605. *
  606. * @internal
  607. */
  608. public static function merge(...$arrays): array
  609. {
  610. $result = [];
  611. foreach ($arrays as $argNumber => $array) {
  612. if (!is_iterable($array)) {
  613. throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1));
  614. }
  615. $result = array_merge($result, self::toArray($array));
  616. }
  617. return $result;
  618. }
  619. /**
  620. * Slices a variable.
  621. *
  622. * @param mixed $item A variable
  623. * @param int $start Start of the slice
  624. * @param int $length Size of the slice
  625. * @param bool $preserveKeys Whether to preserve key or not (when the input is an array)
  626. *
  627. * @return mixed The sliced variable
  628. *
  629. * @internal
  630. */
  631. public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false)
  632. {
  633. if ($item instanceof \Traversable) {
  634. while ($item instanceof \IteratorAggregate) {
  635. $item = $item->getIterator();
  636. }
  637. if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
  638. try {
  639. return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys);
  640. } catch (\OutOfBoundsException $e) {
  641. return [];
  642. }
  643. }
  644. $item = iterator_to_array($item, $preserveKeys);
  645. }
  646. if (\is_array($item)) {
  647. return \array_slice($item, $start, $length, $preserveKeys);
  648. }
  649. return mb_substr((string) $item, $start, $length, $charset);
  650. }
  651. /**
  652. * Returns the first element of the item.
  653. *
  654. * @param mixed $item A variable
  655. *
  656. * @return mixed The first element of the item
  657. *
  658. * @internal
  659. */
  660. public static function first(string $charset, $item)
  661. {
  662. $elements = self::slice($charset, $item, 0, 1, false);
  663. return \is_string($elements) ? $elements : current($elements);
  664. }
  665. /**
  666. * Returns the last element of the item.
  667. *
  668. * @param mixed $item A variable
  669. *
  670. * @return mixed The last element of the item
  671. *
  672. * @internal
  673. */
  674. public static function last(string $charset, $item)
  675. {
  676. $elements = self::slice($charset, $item, -1, 1, false);
  677. return \is_string($elements) ? $elements : current($elements);
  678. }
  679. /**
  680. * Joins the values to a string.
  681. *
  682. * The separators between elements are empty strings per default, you can define them with the optional parameters.
  683. *
  684. * {{ [1, 2, 3]|join(', ', ' and ') }}
  685. * {# returns 1, 2 and 3 #}
  686. *
  687. * {{ [1, 2, 3]|join('|') }}
  688. * {# returns 1|2|3 #}
  689. *
  690. * {{ [1, 2, 3]|join }}
  691. * {# returns 123 #}
  692. *
  693. * @param array $value An array
  694. * @param string $glue The separator
  695. * @param string|null $and The separator for the last pair
  696. *
  697. * @internal
  698. */
  699. public static function join($value, $glue = '', $and = null): string
  700. {
  701. if (!is_iterable($value)) {
  702. $value = (array) $value;
  703. }
  704. $value = self::toArray($value, false);
  705. if (0 === \count($value)) {
  706. return '';
  707. }
  708. if (null === $and || $and === $glue) {
  709. return implode($glue, $value);
  710. }
  711. if (1 === \count($value)) {
  712. return $value[0];
  713. }
  714. return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
  715. }
  716. /**
  717. * Splits the string into an array.
  718. *
  719. * {{ "one,two,three"|split(',') }}
  720. * {# returns [one, two, three] #}
  721. *
  722. * {{ "one,two,three,four,five"|split(',', 3) }}
  723. * {# returns [one, two, "three,four,five"] #}
  724. *
  725. * {{ "123"|split('') }}
  726. * {# returns [1, 2, 3] #}
  727. *
  728. * {{ "aabbcc"|split('', 2) }}
  729. * {# returns [aa, bb, cc] #}
  730. *
  731. * @param string|null $value A string
  732. * @param string $delimiter The delimiter
  733. * @param int|null $limit The limit
  734. *
  735. * @internal
  736. */
  737. public static function split(string $charset, $value, $delimiter, $limit = null): array
  738. {
  739. $value = $value ?? '';
  740. if ('' !== $delimiter) {
  741. return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
  742. }
  743. if ($limit <= 1) {
  744. return preg_split('/(?<!^)(?!$)/u', $value);
  745. }
  746. $length = mb_strlen($value, $charset);
  747. if ($length < $limit) {
  748. return [$value];
  749. }
  750. $r = [];
  751. for ($i = 0; $i < $length; $i += $limit) {
  752. $r[] = mb_substr($value, $i, $limit, $charset);
  753. }
  754. return $r;
  755. }
  756. /**
  757. * @internal
  758. */
  759. public static function default($value, $default = '')
  760. {
  761. if (self::testEmpty($value)) {
  762. return $default;
  763. }
  764. return $value;
  765. }
  766. /**
  767. * Returns the keys for the given array.
  768. *
  769. * It is useful when you want to iterate over the keys of an array:
  770. *
  771. * {% for key in array|keys %}
  772. * {# ... #}
  773. * {% endfor %}
  774. *
  775. * @internal
  776. */
  777. public static function keys($array): array
  778. {
  779. if ($array instanceof \Traversable) {
  780. while ($array instanceof \IteratorAggregate) {
  781. $array = $array->getIterator();
  782. }
  783. $keys = [];
  784. if ($array instanceof \Iterator) {
  785. $array->rewind();
  786. while ($array->valid()) {
  787. $keys[] = $array->key();
  788. $array->next();
  789. }
  790. return $keys;
  791. }
  792. foreach ($array as $key => $item) {
  793. $keys[] = $key;
  794. }
  795. return $keys;
  796. }
  797. if (!\is_array($array)) {
  798. return [];
  799. }
  800. return array_keys($array);
  801. }
  802. /**
  803. * Reverses a variable.
  804. *
  805. * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string
  806. * @param bool $preserveKeys Whether to preserve key or not
  807. *
  808. * @return mixed The reversed input
  809. *
  810. * @internal
  811. */
  812. public static function reverse(string $charset, $item, $preserveKeys = false)
  813. {
  814. if ($item instanceof \Traversable) {
  815. return array_reverse(iterator_to_array($item), $preserveKeys);
  816. }
  817. if (\is_array($item)) {
  818. return array_reverse($item, $preserveKeys);
  819. }
  820. $string = (string) $item;
  821. if ('UTF-8' !== $charset) {
  822. $string = self::convertEncoding($string, 'UTF-8', $charset);
  823. }
  824. preg_match_all('/./us', $string, $matches);
  825. $string = implode('', array_reverse($matches[0]));
  826. if ('UTF-8' !== $charset) {
  827. $string = self::convertEncoding($string, $charset, 'UTF-8');
  828. }
  829. return $string;
  830. }
  831. /**
  832. * Shuffles an array, a \Traversable instance, or a string.
  833. * The function does not preserve keys.
  834. *
  835. * @param array|\Traversable|string|null $item
  836. *
  837. * @return mixed
  838. *
  839. * @internal
  840. */
  841. public static function shuffle(string $charset, $item)
  842. {
  843. if (\is_string($item)) {
  844. if ('UTF-8' !== $charset) {
  845. $item = self::convertEncoding($item, 'UTF-8', $charset);
  846. }
  847. $item = preg_split('/(?<!^)(?!$)/u', $item, -1);
  848. shuffle($item);
  849. $item = implode('', $item);
  850. if ('UTF-8' !== $charset) {
  851. $item = self::convertEncoding($item, $charset, 'UTF-8');
  852. }
  853. return $item;
  854. }
  855. if (is_iterable($item)) {
  856. $item = self::toArray($item, false);
  857. shuffle($item);
  858. }
  859. return $item;
  860. }
  861. /**
  862. * Sorts an array.
  863. *
  864. * @param array|\Traversable $array
  865. *
  866. * @internal
  867. */
  868. public static function sort(Environment $env, $array, $arrow = null): array
  869. {
  870. if ($array instanceof \Traversable) {
  871. $array = iterator_to_array($array);
  872. } elseif (!\is_array($array)) {
  873. throw new RuntimeError(\sprintf('The sort filter only works with sequences/mappings or "Traversable", got "%s".', \gettype($array)));
  874. }
  875. if (null !== $arrow) {
  876. self::checkArrowInSandbox($env, $arrow, 'sort', 'filter');
  877. uasort($array, $arrow);
  878. } else {
  879. asort($array);
  880. }
  881. return $array;
  882. }
  883. /**
  884. * @internal
  885. */
  886. public static function inFilter($value, $compare)
  887. {
  888. if ($value instanceof Markup) {
  889. $value = (string) $value;
  890. }
  891. if ($compare instanceof Markup) {
  892. $compare = (string) $compare;
  893. }
  894. if (\is_string($compare)) {
  895. if (\is_string($value) || \is_int($value) || \is_float($value)) {
  896. return '' === $value || str_contains($compare, (string) $value);
  897. }
  898. return false;
  899. }
  900. if (!is_iterable($compare)) {
  901. return false;
  902. }
  903. if (\is_object($value) || \is_resource($value)) {
  904. if (!\is_array($compare)) {
  905. foreach ($compare as $item) {
  906. if ($item === $value) {
  907. return true;
  908. }
  909. }
  910. return false;
  911. }
  912. return \in_array($value, $compare, true);
  913. }
  914. foreach ($compare as $item) {
  915. if (0 === self::compare($value, $item)) {
  916. return true;
  917. }
  918. }
  919. return false;
  920. }
  921. /**
  922. * Compares two values using a more strict version of the PHP non-strict comparison operator.
  923. *
  924. * @see https://wiki.php.net/rfc/string_to_number_comparison
  925. * @see https://wiki.php.net/rfc/trailing_whitespace_numerics
  926. *
  927. * @internal
  928. */
  929. public static function compare($a, $b)
  930. {
  931. // int <=> string
  932. if (\is_int($a) && \is_string($b)) {
  933. $bTrim = trim($b, " \t\n\r\v\f");
  934. if (!is_numeric($bTrim)) {
  935. return (string) $a <=> $b;
  936. }
  937. if ((int) $bTrim == $bTrim) {
  938. return $a <=> (int) $bTrim;
  939. } else {
  940. return (float) $a <=> (float) $bTrim;
  941. }
  942. }
  943. if (\is_string($a) && \is_int($b)) {
  944. $aTrim = trim($a, " \t\n\r\v\f");
  945. if (!is_numeric($aTrim)) {
  946. return $a <=> (string) $b;
  947. }
  948. if ((int) $aTrim == $aTrim) {
  949. return (int) $aTrim <=> $b;
  950. } else {
  951. return (float) $aTrim <=> (float) $b;
  952. }
  953. }
  954. // float <=> string
  955. if (\is_float($a) && \is_string($b)) {
  956. if (is_nan($a)) {
  957. return 1;
  958. }
  959. $bTrim = trim($b, " \t\n\r\v\f");
  960. if (!is_numeric($bTrim)) {
  961. return (string) $a <=> $b;
  962. }
  963. return $a <=> (float) $bTrim;
  964. }
  965. if (\is_string($a) && \is_float($b)) {
  966. if (is_nan($b)) {
  967. return 1;
  968. }
  969. $aTrim = trim($a, " \t\n\r\v\f");
  970. if (!is_numeric($aTrim)) {
  971. return $a <=> (string) $b;
  972. }
  973. return (float) $aTrim <=> $b;
  974. }
  975. // fallback to <=>
  976. return $a <=> $b;
  977. }
  978. /**
  979. * @throws RuntimeError When an invalid pattern is used
  980. *
  981. * @internal
  982. */
  983. public static function matches(string $regexp, ?string $str): int
  984. {
  985. set_error_handler(function ($t, $m) use ($regexp) {
  986. throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12));
  987. });
  988. try {
  989. return preg_match($regexp, $str ?? '');
  990. } finally {
  991. restore_error_handler();
  992. }
  993. }
  994. /**
  995. * Returns a trimmed string.
  996. *
  997. * @param string|null $string
  998. * @param string|null $characterMask
  999. * @param string $side
  1000. *
  1001. * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
  1002. *
  1003. * @internal
  1004. */
  1005. public static function trim($string, $characterMask = null, $side = 'both'): string
  1006. {
  1007. if (null === $characterMask) {
  1008. $characterMask = " \t\n\r\0\x0B";
  1009. }
  1010. switch ($side) {
  1011. case 'both':
  1012. return trim($string ?? '', $characterMask);
  1013. case 'left':
  1014. return ltrim($string ?? '', $characterMask);
  1015. case 'right':
  1016. return rtrim($string ?? '', $characterMask);
  1017. default:
  1018. throw new RuntimeError('Trimming side must be "left", "right" or "both".');
  1019. }
  1020. }
  1021. /**
  1022. * Inserts HTML line breaks before all newlines in a string.
  1023. *
  1024. * @param string|null $string
  1025. *
  1026. * @internal
  1027. */
  1028. public static function nl2br($string): string
  1029. {
  1030. return nl2br($string ?? '');
  1031. }
  1032. /**
  1033. * Removes whitespaces between HTML tags.
  1034. *
  1035. * @param string|null $content
  1036. *
  1037. * @internal
  1038. */
  1039. public static function spaceless($content): string
  1040. {
  1041. return trim(preg_replace('/>\s+</', '><', $content ?? ''));
  1042. }
  1043. /**
  1044. * @param string|null $string
  1045. * @param string $to
  1046. * @param string $from
  1047. *
  1048. * @internal
  1049. */
  1050. public static function convertEncoding($string, $to, $from): string
  1051. {
  1052. if (!\function_exists('iconv')) {
  1053. throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
  1054. }
  1055. return iconv($from, $to, $string ?? '');
  1056. }
  1057. /**
  1058. * Returns the length of a variable.
  1059. *
  1060. * @param mixed $thing A variable
  1061. *
  1062. * @internal
  1063. */
  1064. public static function length(string $charset, $thing): int
  1065. {
  1066. if (null === $thing) {
  1067. return 0;
  1068. }
  1069. if (\is_scalar($thing)) {
  1070. return mb_strlen($thing, $charset);
  1071. }
  1072. if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
  1073. return \count($thing);
  1074. }
  1075. if ($thing instanceof \Traversable) {
  1076. return iterator_count($thing);
  1077. }
  1078. if ($thing instanceof \Stringable) {
  1079. return mb_strlen((string) $thing, $charset);
  1080. }
  1081. return 1;
  1082. }
  1083. /**
  1084. * Converts a string to uppercase.
  1085. *
  1086. * @param string|null $string A string
  1087. *
  1088. * @internal
  1089. */
  1090. public static function upper(string $charset, $string): string
  1091. {
  1092. return mb_strtoupper($string ?? '', $charset);
  1093. }
  1094. /**
  1095. * Converts a string to lowercase.
  1096. *
  1097. * @param string|null $string A string
  1098. *
  1099. * @internal
  1100. */
  1101. public static function lower(string $charset, $string): string
  1102. {
  1103. return mb_strtolower($string ?? '', $charset);
  1104. }
  1105. /**
  1106. * Strips HTML and PHP tags from a string.
  1107. *
  1108. * @param string|null $string
  1109. * @param string[]|string|null $allowable_tags
  1110. *
  1111. * @internal
  1112. */
  1113. public static function striptags($string, $allowable_tags = null): string
  1114. {
  1115. return strip_tags($string ?? '', $allowable_tags);
  1116. }
  1117. /**
  1118. * Returns a titlecased string.
  1119. *
  1120. * @param string|null $string A string
  1121. *
  1122. * @internal
  1123. */
  1124. public static function titleCase(string $charset, $string): string
  1125. {
  1126. return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset);
  1127. }
  1128. /**
  1129. * Returns a capitalized string.
  1130. *
  1131. * @param string|null $string A string
  1132. *
  1133. * @internal
  1134. */
  1135. public static function capitalize(string $charset, $string): string
  1136. {
  1137. return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset);
  1138. }
  1139. /**
  1140. * @internal
  1141. */
  1142. public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
  1143. {
  1144. if (!method_exists($template, $method)) {
  1145. $parent = $template;
  1146. while ($parent = $parent->getParent($context)) {
  1147. if (method_exists($parent, $method)) {
  1148. return $parent->$method(...$args);
  1149. }
  1150. }
  1151. throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
  1152. }
  1153. return $template->$method(...$args);
  1154. }
  1155. /**
  1156. * @template TSequence
  1157. *
  1158. * @param TSequence $seq
  1159. *
  1160. * @return ($seq is iterable ? TSequence : array{})
  1161. *
  1162. * @internal
  1163. */
  1164. public static function ensureTraversable($seq)
  1165. {
  1166. if (is_iterable($seq)) {
  1167. return $seq;
  1168. }
  1169. return [];
  1170. }
  1171. /**
  1172. * @internal
  1173. */
  1174. public static function toArray($seq, $preserveKeys = true)
  1175. {
  1176. if ($seq instanceof \Traversable) {
  1177. return iterator_to_array($seq, $preserveKeys);
  1178. }
  1179. if (!\is_array($seq)) {
  1180. return $seq;
  1181. }
  1182. return $preserveKeys ? $seq : array_values($seq);
  1183. }
  1184. /**
  1185. * Checks if a variable is empty.
  1186. *
  1187. * {# evaluates to true if the foo variable is null, false, or the empty string #}
  1188. * {% if foo is empty %}
  1189. * {# ... #}
  1190. * {% endif %}
  1191. *
  1192. * @param mixed $value A variable
  1193. *
  1194. * @internal
  1195. */
  1196. public static function testEmpty($value): bool
  1197. {
  1198. if ($value instanceof \Countable) {
  1199. return 0 === \count($value);
  1200. }
  1201. if ($value instanceof \Traversable) {
  1202. return !iterator_count($value);
  1203. }
  1204. if ($value instanceof \Stringable) {
  1205. return '' === (string) $value;
  1206. }
  1207. return '' === $value || false === $value || null === $value || [] === $value;
  1208. }
  1209. /**
  1210. * Checks if a variable is a sequence.
  1211. *
  1212. * {# evaluates to true if the foo variable is a sequence #}
  1213. * {% if foo is sequence %}
  1214. * {# ... #}
  1215. * {% endif %}
  1216. *
  1217. * @param mixed $value
  1218. *
  1219. * @internal
  1220. */
  1221. public static function testSequence($value): bool
  1222. {
  1223. if ($value instanceof \ArrayObject) {
  1224. $value = $value->getArrayCopy();
  1225. }
  1226. if ($value instanceof \Traversable) {
  1227. $value = iterator_to_array($value);
  1228. }
  1229. return \is_array($value) && array_is_list($value);
  1230. }
  1231. /**
  1232. * Checks if a variable is a mapping.
  1233. *
  1234. * {# evaluates to true if the foo variable is a mapping #}
  1235. * {% if foo is mapping %}
  1236. * {# ... #}
  1237. * {% endif %}
  1238. *
  1239. * @param mixed $value
  1240. *
  1241. * @internal
  1242. */
  1243. public static function testMapping($value): bool
  1244. {
  1245. if ($value instanceof \ArrayObject) {
  1246. $value = $value->getArrayCopy();
  1247. }
  1248. if ($value instanceof \Traversable) {
  1249. $value = iterator_to_array($value);
  1250. }
  1251. return (\is_array($value) && !array_is_list($value)) || \is_object($value);
  1252. }
  1253. /**
  1254. * Renders a template.
  1255. *
  1256. * @param array $context
  1257. * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively
  1258. * @param array $variables The variables to pass to the template
  1259. * @param bool $withContext
  1260. * @param bool $ignoreMissing Whether to ignore missing templates or not
  1261. * @param bool $sandboxed Whether to sandbox the template or not
  1262. *
  1263. * @internal
  1264. */
  1265. public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string
  1266. {
  1267. $alreadySandboxed = false;
  1268. $sandbox = null;
  1269. if ($withContext) {
  1270. $variables = array_merge($context, $variables);
  1271. }
  1272. if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
  1273. $sandbox = $env->getExtension(SandboxExtension::class);
  1274. if (!$alreadySandboxed = $sandbox->isSandboxed()) {
  1275. $sandbox->enableSandbox();
  1276. }
  1277. }
  1278. try {
  1279. $loaded = null;
  1280. try {
  1281. $loaded = $env->resolveTemplate($template);
  1282. } catch (LoaderError $e) {
  1283. if (!$ignoreMissing) {
  1284. throw $e;
  1285. }
  1286. return '';
  1287. }
  1288. if ($isSandboxed) {
  1289. $loaded->unwrap()->checkSecurity();
  1290. }
  1291. return $loaded->render($variables);
  1292. } finally {
  1293. if ($isSandboxed && !$alreadySandboxed) {
  1294. $sandbox->disableSandbox();
  1295. }
  1296. }
  1297. }
  1298. /**
  1299. * Returns a template content without rendering it.
  1300. *
  1301. * @param string $name The template name
  1302. * @param bool $ignoreMissing Whether to ignore missing templates or not
  1303. *
  1304. * @internal
  1305. */
  1306. public static function source(Environment $env, $name, $ignoreMissing = false): string
  1307. {
  1308. $loader = $env->getLoader();
  1309. try {
  1310. return $loader->getSourceContext($name)->getCode();
  1311. } catch (LoaderError $e) {
  1312. if (!$ignoreMissing) {
  1313. throw $e;
  1314. }
  1315. return '';
  1316. }
  1317. }
  1318. /**
  1319. * Returns the list of cases of the enum.
  1320. *
  1321. * @template T of \UnitEnum
  1322. *
  1323. * @param class-string<T> $enum
  1324. *
  1325. * @return list<T>
  1326. *
  1327. * @internal
  1328. */
  1329. public static function enumCases(string $enum): array
  1330. {
  1331. if (!enum_exists($enum)) {
  1332. throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum));
  1333. }
  1334. return $enum::cases();
  1335. }
  1336. /**
  1337. * Provides the ability to get constants from instances as well as class/global constants.
  1338. *
  1339. * @param string $constant The name of the constant
  1340. * @param object|null $object The object to get the constant from
  1341. * @param bool $checkDefined Whether to check if the constant is defined or not
  1342. *
  1343. * @return mixed Class constants can return many types like scalars, arrays, and
  1344. * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.)
  1345. * When $checkDefined is true, returns true when the constant is defined, false otherwise
  1346. *
  1347. * @internal
  1348. */
  1349. public static function constant($constant, $object = null, bool $checkDefined = false)
  1350. {
  1351. if (null !== $object) {
  1352. if ('class' === $constant) {
  1353. return $checkDefined ? true : \get_class($object);
  1354. }
  1355. $constant = \get_class($object).'::'.$constant;
  1356. }
  1357. if (!\defined($constant)) {
  1358. if ($checkDefined) {
  1359. return false;
  1360. }
  1361. if ('::class' === strtolower(substr($constant, -7))) {
  1362. throw new RuntimeError(\sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant));
  1363. }
  1364. throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant));
  1365. }
  1366. return $checkDefined ? true : \constant($constant);
  1367. }
  1368. /**
  1369. * Batches item.
  1370. *
  1371. * @param array $items An array of items
  1372. * @param int $size The size of the batch
  1373. * @param mixed $fill A value used to fill missing items
  1374. *
  1375. * @internal
  1376. */
  1377. public static function batch($items, $size, $fill = null, $preserveKeys = true): array
  1378. {
  1379. if (!is_iterable($items)) {
  1380. throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items)));
  1381. }
  1382. $size = (int) ceil($size);
  1383. $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys);
  1384. if (null !== $fill && $result) {
  1385. $last = \count($result) - 1;
  1386. if ($fillCount = $size - \count($result[$last])) {
  1387. for ($i = 0; $i < $fillCount; ++$i) {
  1388. $result[$last][] = $fill;
  1389. }
  1390. }
  1391. }
  1392. return $result;
  1393. }
  1394. /**
  1395. * Returns the attribute value for a given array/object.
  1396. *
  1397. * @param mixed $object The object or array from where to get the item
  1398. * @param mixed $item The item to get from the array or object
  1399. * @param array $arguments An array of arguments to pass if the item is an object method
  1400. * @param string $type The type of attribute (@see \Twig\Template constants)
  1401. * @param bool $isDefinedTest Whether this is only a defined check
  1402. * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not
  1403. * @param int $lineno The template line where the attribute was called
  1404. *
  1405. * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
  1406. *
  1407. * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
  1408. *
  1409. * @internal
  1410. */
  1411. public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
  1412. {
  1413. $propertyNotAllowedError = null;
  1414. // array
  1415. if (Template::METHOD_CALL !== $type) {
  1416. $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
  1417. if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) {
  1418. try {
  1419. $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
  1420. } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
  1421. goto methodCheck;
  1422. }
  1423. }
  1424. if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
  1425. || ($object instanceof \ArrayAccess && isset($object[$arrayItem]))
  1426. ) {
  1427. if ($isDefinedTest) {
  1428. return true;
  1429. }
  1430. return $object[$arrayItem];
  1431. }
  1432. if (Template::ARRAY_CALL === $type || !\is_object($object)) {
  1433. if ($isDefinedTest) {
  1434. return false;
  1435. }
  1436. if ($ignoreStrictCheck || !$env->isStrictVariables()) {
  1437. return;
  1438. }
  1439. if ($object instanceof \ArrayAccess) {
  1440. $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object));
  1441. } elseif (\is_object($object)) {
  1442. $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object));
  1443. } elseif (\is_array($object)) {
  1444. if (empty($object)) {
  1445. $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem);
  1446. } else {
  1447. $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
  1448. }
  1449. } elseif (Template::ARRAY_CALL === $type) {
  1450. if (null === $object) {
  1451. $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item);
  1452. } else {
  1453. $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
  1454. }
  1455. } elseif (null === $object) {
  1456. $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
  1457. } else {
  1458. $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
  1459. }
  1460. throw new RuntimeError($message, $lineno, $source);
  1461. }
  1462. }
  1463. if (!\is_object($object)) {
  1464. if ($isDefinedTest) {
  1465. return false;
  1466. }
  1467. if ($ignoreStrictCheck || !$env->isStrictVariables()) {
  1468. return;
  1469. }
  1470. if (null === $object) {
  1471. $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
  1472. } elseif (\is_array($object)) {
  1473. $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item);
  1474. } else {
  1475. $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
  1476. }
  1477. throw new RuntimeError($message, $lineno, $source);
  1478. }
  1479. if ($object instanceof Template) {
  1480. throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
  1481. }
  1482. // object property
  1483. if (Template::METHOD_CALL !== $type) {
  1484. if ($sandboxed) {
  1485. try {
  1486. $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
  1487. } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) {
  1488. goto methodCheck;
  1489. }
  1490. }
  1491. if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
  1492. if ($isDefinedTest) {
  1493. return true;
  1494. }
  1495. return $object->$item;
  1496. }
  1497. }
  1498. methodCheck:
  1499. static $cache = [];
  1500. $class = \get_class($object);
  1501. // object method
  1502. // precedence: getXxx() > isXxx() > hasXxx()
  1503. if (!isset($cache[$class])) {
  1504. $methods = get_class_methods($object);
  1505. sort($methods);
  1506. $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods);
  1507. $classCache = [];
  1508. foreach ($methods as $i => $method) {
  1509. $classCache[$method] = $method;
  1510. $classCache[$lcName = $lcMethods[$i]] = $method;
  1511. if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) {
  1512. $name = substr($method, 3);
  1513. $lcName = substr($lcName, 3);
  1514. } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) {
  1515. $name = substr($method, 2);
  1516. $lcName = substr($lcName, 2);
  1517. } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) {
  1518. $name = substr($method, 3);
  1519. $lcName = substr($lcName, 3);
  1520. if (\in_array('is'.$lcName, $lcMethods)) {
  1521. continue;
  1522. }
  1523. } else {
  1524. continue;
  1525. }
  1526. // skip get() and is() methods (in which case, $name is empty)
  1527. if ($name) {
  1528. if (!isset($classCache[$name])) {
  1529. $classCache[$name] = $method;
  1530. }
  1531. if (!isset($classCache[$lcName])) {
  1532. $classCache[$lcName] = $method;
  1533. }
  1534. }
  1535. }
  1536. $cache[$class] = $classCache;
  1537. }
  1538. $call = false;
  1539. if (isset($cache[$class][$item])) {
  1540. $method = $cache[$class][$item];
  1541. } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) {
  1542. $method = $cache[$class][$lcItem];
  1543. } elseif (isset($cache[$class]['__call'])) {
  1544. $method = $item;
  1545. $call = true;
  1546. } else {
  1547. if ($isDefinedTest) {
  1548. return false;
  1549. }
  1550. if ($propertyNotAllowedError) {
  1551. throw $propertyNotAllowedError;
  1552. }
  1553. if ($ignoreStrictCheck || !$env->isStrictVariables()) {
  1554. return;
  1555. }
  1556. throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source);
  1557. }
  1558. if ($sandboxed) {
  1559. try {
  1560. $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
  1561. } catch (SecurityNotAllowedMethodError $e) {
  1562. if ($isDefinedTest) {
  1563. return false;
  1564. }
  1565. if ($propertyNotAllowedError) {
  1566. throw $propertyNotAllowedError;
  1567. }
  1568. throw $e;
  1569. }
  1570. }
  1571. if ($isDefinedTest) {
  1572. return true;
  1573. }
  1574. // Some objects throw exceptions when they have __call, and the method we try
  1575. // to call is not supported. If ignoreStrictCheck is true, we should return null.
  1576. try {
  1577. $ret = $object->$method(...$arguments);
  1578. } catch (\BadMethodCallException $e) {
  1579. if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
  1580. return;
  1581. }
  1582. throw $e;
  1583. }
  1584. return $ret;
  1585. }
  1586. /**
  1587. * Returns the values from a single column in the input array.
  1588. *
  1589. * <pre>
  1590. * {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
  1591. *
  1592. * {% set fruits = items|column('fruit') %}
  1593. *
  1594. * {# fruits now contains ['apple', 'orange'] #}
  1595. * </pre>
  1596. *
  1597. * @param array|\Traversable $array An array
  1598. * @param int|string $name The column name
  1599. * @param int|string|null $index The column to use as the index/keys for the returned array
  1600. *
  1601. * @return array The array of values
  1602. *
  1603. * @internal
  1604. */
  1605. public static function column($array, $name, $index = null): array
  1606. {
  1607. if ($array instanceof \Traversable) {
  1608. $array = iterator_to_array($array);
  1609. } elseif (!\is_array($array)) {
  1610. throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array)));
  1611. }
  1612. return array_column($array, $name, $index);
  1613. }
  1614. /**
  1615. * @internal
  1616. */
  1617. public static function filter(Environment $env, $array, $arrow)
  1618. {
  1619. if (!is_iterable($array)) {
  1620. throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
  1621. }
  1622. self::checkArrowInSandbox($env, $arrow, 'filter', 'filter');
  1623. if (\is_array($array)) {
  1624. return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
  1625. }
  1626. // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
  1627. return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
  1628. }
  1629. /**
  1630. * @internal
  1631. */
  1632. public static function find(Environment $env, $array, $arrow)
  1633. {
  1634. self::checkArrowInSandbox($env, $arrow, 'find', 'filter');
  1635. foreach ($array as $k => $v) {
  1636. if ($arrow($v, $k)) {
  1637. return $v;
  1638. }
  1639. }
  1640. return null;
  1641. }
  1642. /**
  1643. * @internal
  1644. */
  1645. public static function map(Environment $env, $array, $arrow)
  1646. {
  1647. if (!is_iterable($array)) {
  1648. throw new RuntimeError(\sprintf('The "map" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array)));
  1649. }
  1650. self::checkArrowInSandbox($env, $arrow, 'map', 'filter');
  1651. $r = [];
  1652. foreach ($array as $k => $v) {
  1653. $r[$k] = $arrow($v, $k);
  1654. }
  1655. return $r;
  1656. }
  1657. /**
  1658. * @internal
  1659. */
  1660. public static function reduce(Environment $env, $array, $arrow, $initial = null)
  1661. {
  1662. self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter');
  1663. if (!\is_array($array) && !$array instanceof \Traversable) {
  1664. throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array)));
  1665. }
  1666. $accumulator = $initial;
  1667. foreach ($array as $key => $value) {
  1668. $accumulator = $arrow($accumulator, $value, $key);
  1669. }
  1670. return $accumulator;
  1671. }
  1672. /**
  1673. * @internal
  1674. */
  1675. public static function arraySome(Environment $env, $array, $arrow)
  1676. {
  1677. self::checkArrowInSandbox($env, $arrow, 'has some', 'operator');
  1678. foreach ($array as $k => $v) {
  1679. if ($arrow($v, $k)) {
  1680. return true;
  1681. }
  1682. }
  1683. return false;
  1684. }
  1685. /**
  1686. * @internal
  1687. */
  1688. public static function arrayEvery(Environment $env, $array, $arrow)
  1689. {
  1690. self::checkArrowInSandbox($env, $arrow, 'has every', 'operator');
  1691. foreach ($array as $k => $v) {
  1692. if (!$arrow($v, $k)) {
  1693. return false;
  1694. }
  1695. }
  1696. return true;
  1697. }
  1698. /**
  1699. * @internal
  1700. */
  1701. public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type)
  1702. {
  1703. if (!$arrow instanceof \Closure && $env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) {
  1704. throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
  1705. }
  1706. }
  1707. /**
  1708. * @internal to be removed in Twig 4
  1709. */
  1710. public static function captureOutput(iterable $body): string
  1711. {
  1712. $level = ob_get_level();
  1713. ob_start();
  1714. try {
  1715. foreach ($body as $data) {
  1716. echo $data;
  1717. }
  1718. } catch (\Throwable $e) {
  1719. while (ob_get_level() > $level) {
  1720. ob_end_clean();
  1721. }
  1722. throw $e;
  1723. }
  1724. return ob_get_clean();
  1725. }
  1726. /**
  1727. * @internal
  1728. */
  1729. public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
  1730. {
  1731. if (!$blockName = $parser->peekBlockStack()) {
  1732. throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext());
  1733. }
  1734. if (!$parser->hasInheritance()) {
  1735. throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext());
  1736. }
  1737. return new ParentExpression($blockName, $line);
  1738. }
  1739. /**
  1740. * @internal
  1741. */
  1742. public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
  1743. {
  1744. $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null);
  1745. $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
  1746. return new BlockReferenceExpression($args[0], $args[1] ?? null, $line);
  1747. }
  1748. /**
  1749. * @internal
  1750. */
  1751. public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression
  1752. {
  1753. $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null);
  1754. $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args);
  1755. return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line);
  1756. }
  1757. }