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

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\ExpressionParser;
  12. use Twig\Node\Expression\Binary\AddBinary;
  13. use Twig\Node\Expression\Binary\AndBinary;
  14. use Twig\Node\Expression\Binary\BitwiseAndBinary;
  15. use Twig\Node\Expression\Binary\BitwiseOrBinary;
  16. use Twig\Node\Expression\Binary\BitwiseXorBinary;
  17. use Twig\Node\Expression\Binary\ConcatBinary;
  18. use Twig\Node\Expression\Binary\DivBinary;
  19. use Twig\Node\Expression\Binary\EndsWithBinary;
  20. use Twig\Node\Expression\Binary\EqualBinary;
  21. use Twig\Node\Expression\Binary\FloorDivBinary;
  22. use Twig\Node\Expression\Binary\GreaterBinary;
  23. use Twig\Node\Expression\Binary\GreaterEqualBinary;
  24. use Twig\Node\Expression\Binary\InBinary;
  25. use Twig\Node\Expression\Binary\LessBinary;
  26. use Twig\Node\Expression\Binary\LessEqualBinary;
  27. use Twig\Node\Expression\Binary\MatchesBinary;
  28. use Twig\Node\Expression\Binary\ModBinary;
  29. use Twig\Node\Expression\Binary\MulBinary;
  30. use Twig\Node\Expression\Binary\NotEqualBinary;
  31. use Twig\Node\Expression\Binary\NotInBinary;
  32. use Twig\Node\Expression\Binary\OrBinary;
  33. use Twig\Node\Expression\Binary\PowerBinary;
  34. use Twig\Node\Expression\Binary\RangeBinary;
  35. use Twig\Node\Expression\Binary\SpaceshipBinary;
  36. use Twig\Node\Expression\Binary\StartsWithBinary;
  37. use Twig\Node\Expression\Binary\SubBinary;
  38. use Twig\Node\Expression\Filter\DefaultFilter;
  39. use Twig\Node\Expression\NullCoalesceExpression;
  40. use Twig\Node\Expression\Test\ConstantTest;
  41. use Twig\Node\Expression\Test\DefinedTest;
  42. use Twig\Node\Expression\Test\DivisiblebyTest;
  43. use Twig\Node\Expression\Test\EvenTest;
  44. use Twig\Node\Expression\Test\NullTest;
  45. use Twig\Node\Expression\Test\OddTest;
  46. use Twig\Node\Expression\Test\SameasTest;
  47. use Twig\Node\Expression\Unary\NegUnary;
  48. use Twig\Node\Expression\Unary\NotUnary;
  49. use Twig\Node\Expression\Unary\PosUnary;
  50. use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
  51. use Twig\TokenParser\ApplyTokenParser;
  52. use Twig\TokenParser\BlockTokenParser;
  53. use Twig\TokenParser\DeprecatedTokenParser;
  54. use Twig\TokenParser\DoTokenParser;
  55. use Twig\TokenParser\EmbedTokenParser;
  56. use Twig\TokenParser\ExtendsTokenParser;
  57. use Twig\TokenParser\FilterTokenParser;
  58. use Twig\TokenParser\FlushTokenParser;
  59. use Twig\TokenParser\ForTokenParser;
  60. use Twig\TokenParser\FromTokenParser;
  61. use Twig\TokenParser\IfTokenParser;
  62. use Twig\TokenParser\ImportTokenParser;
  63. use Twig\TokenParser\IncludeTokenParser;
  64. use Twig\TokenParser\MacroTokenParser;
  65. use Twig\TokenParser\SetTokenParser;
  66. use Twig\TokenParser\SpacelessTokenParser;
  67. use Twig\TokenParser\UseTokenParser;
  68. use Twig\TokenParser\WithTokenParser;
  69. use Twig\TwigFilter;
  70. use Twig\TwigFunction;
  71. use Twig\TwigTest;
  72. final class CoreExtension extends AbstractExtension
  73. {
  74. private $dateFormats = ['F j, Y H:i', '%d days'];
  75. private $numberFormat = [0, '.', ','];
  76. private $timezone = null;
  77. private $escapers = [];
  78. /**
  79. * Defines a new escaper to be used via the escape filter.
  80. *
  81. * @param string $strategy The strategy name that should be used as a strategy in the escape call
  82. * @param callable $callable A valid PHP callable
  83. *
  84. * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
  85. */
  86. public function setEscaper($strategy, callable $callable)
  87. {
  88. @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::setEscaper" instead.', __METHOD__, EscaperExtension::class), \E_USER_DEPRECATED);
  89. $this->escapers[$strategy] = $callable;
  90. }
  91. /**
  92. * Gets all defined escapers.
  93. *
  94. * @return callable[] An array of escapers
  95. *
  96. * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
  97. */
  98. public function getEscapers(/* $triggerDeprecation = true */)
  99. {
  100. if (0 === \func_num_args() || \func_get_arg(0)) {
  101. @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::getEscapers" instead.', __METHOD__, EscaperExtension::class), \E_USER_DEPRECATED);
  102. }
  103. return $this->escapers;
  104. }
  105. /**
  106. * Sets the default format to be used by the date filter.
  107. *
  108. * @param string $format The default date format string
  109. * @param string $dateIntervalFormat The default date interval format string
  110. */
  111. public function setDateFormat($format = null, $dateIntervalFormat = null)
  112. {
  113. if (null !== $format) {
  114. $this->dateFormats[0] = $format;
  115. }
  116. if (null !== $dateIntervalFormat) {
  117. $this->dateFormats[1] = $dateIntervalFormat;
  118. }
  119. }
  120. /**
  121. * Gets the default format to be used by the date filter.
  122. *
  123. * @return array The default date format string and the default date interval format string
  124. */
  125. public function getDateFormat()
  126. {
  127. return $this->dateFormats;
  128. }
  129. /**
  130. * Sets the default timezone to be used by the date filter.
  131. *
  132. * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object
  133. */
  134. public function setTimezone($timezone)
  135. {
  136. $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone);
  137. }
  138. /**
  139. * Gets the default timezone to be used by the date filter.
  140. *
  141. * @return \DateTimeZone The default timezone currently in use
  142. */
  143. public function getTimezone()
  144. {
  145. if (null === $this->timezone) {
  146. $this->timezone = new \DateTimeZone(date_default_timezone_get());
  147. }
  148. return $this->timezone;
  149. }
  150. /**
  151. * Sets the default format to be used by the number_format filter.
  152. *
  153. * @param int $decimal the number of decimal places to use
  154. * @param string $decimalPoint the character(s) to use for the decimal point
  155. * @param string $thousandSep the character(s) to use for the thousands separator
  156. */
  157. public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
  158. {
  159. $this->numberFormat = [$decimal, $decimalPoint, $thousandSep];
  160. }
  161. /**
  162. * Get the default format used by the number_format filter.
  163. *
  164. * @return array The arguments for number_format()
  165. */
  166. public function getNumberFormat()
  167. {
  168. return $this->numberFormat;
  169. }
  170. public function getTokenParsers()
  171. {
  172. return [
  173. new ApplyTokenParser(),
  174. new ForTokenParser(),
  175. new IfTokenParser(),
  176. new ExtendsTokenParser(),
  177. new IncludeTokenParser(),
  178. new BlockTokenParser(),
  179. new UseTokenParser(),
  180. new FilterTokenParser(),
  181. new MacroTokenParser(),
  182. new ImportTokenParser(),
  183. new FromTokenParser(),
  184. new SetTokenParser(),
  185. new SpacelessTokenParser(),
  186. new FlushTokenParser(),
  187. new DoTokenParser(),
  188. new EmbedTokenParser(),
  189. new WithTokenParser(),
  190. new DeprecatedTokenParser(),
  191. ];
  192. }
  193. public function getFilters()
  194. {
  195. return [
  196. // formatting filters
  197. new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]),
  198. new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]),
  199. new TwigFilter('format', 'twig_sprintf'),
  200. new TwigFilter('replace', 'twig_replace_filter'),
  201. new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]),
  202. new TwigFilter('abs', 'abs'),
  203. new TwigFilter('round', 'twig_round'),
  204. // encoding
  205. new TwigFilter('url_encode', 'twig_urlencode_filter'),
  206. new TwigFilter('json_encode', 'json_encode'),
  207. new TwigFilter('convert_encoding', 'twig_convert_encoding'),
  208. // string filters
  209. new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]),
  210. new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]),
  211. new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]),
  212. new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]),
  213. new TwigFilter('striptags', 'twig_striptags'),
  214. new TwigFilter('trim', 'twig_trim_filter'),
  215. new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]),
  216. new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]),
  217. // array helpers
  218. new TwigFilter('join', 'twig_join_filter'),
  219. new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
  220. new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]),
  221. new TwigFilter('merge', 'twig_array_merge'),
  222. new TwigFilter('batch', 'twig_array_batch'),
  223. new TwigFilter('column', 'twig_array_column'),
  224. new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]),
  225. new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]),
  226. new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]),
  227. // string/array filters
  228. new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
  229. new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]),
  230. new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]),
  231. new TwigFilter('first', 'twig_first', ['needs_environment' => true]),
  232. new TwigFilter('last', 'twig_last', ['needs_environment' => true]),
  233. // iteration and runtime
  234. new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]),
  235. new TwigFilter('keys', 'twig_get_array_keys_filter'),
  236. ];
  237. }
  238. public function getFunctions()
  239. {
  240. return [
  241. new TwigFunction('max', 'max'),
  242. new TwigFunction('min', 'min'),
  243. new TwigFunction('range', 'range'),
  244. new TwigFunction('constant', 'twig_constant'),
  245. new TwigFunction('cycle', 'twig_cycle'),
  246. new TwigFunction('random', 'twig_random', ['needs_environment' => true]),
  247. new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]),
  248. new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
  249. new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]),
  250. ];
  251. }
  252. public function getTests()
  253. {
  254. return [
  255. new TwigTest('even', null, ['node_class' => EvenTest::class]),
  256. new TwigTest('odd', null, ['node_class' => OddTest::class]),
  257. new TwigTest('defined', null, ['node_class' => DefinedTest::class]),
  258. new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true]),
  259. new TwigTest('none', null, ['node_class' => NullTest::class]),
  260. new TwigTest('null', null, ['node_class' => NullTest::class]),
  261. new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]),
  262. new TwigTest('constant', null, ['node_class' => ConstantTest::class]),
  263. new TwigTest('empty', 'twig_test_empty'),
  264. new TwigTest('iterable', 'twig_test_iterable'),
  265. ];
  266. }
  267. public function getNodeVisitors()
  268. {
  269. return [new MacroAutoImportNodeVisitor()];
  270. }
  271. public function getOperators()
  272. {
  273. return [
  274. [
  275. 'not' => ['precedence' => 50, 'class' => NotUnary::class],
  276. '-' => ['precedence' => 500, 'class' => NegUnary::class],
  277. '+' => ['precedence' => 500, 'class' => PosUnary::class],
  278. ],
  279. [
  280. 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  281. 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  282. 'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  283. 'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  284. 'b-and' => ['precedence' => 18, 'class' => BitwiseAndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  285. '==' => ['precedence' => 20, 'class' => EqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  286. '!=' => ['precedence' => 20, 'class' => NotEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  287. '<=>' => ['precedence' => 20, 'class' => SpaceshipBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  288. '<' => ['precedence' => 20, 'class' => LessBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  289. '>' => ['precedence' => 20, 'class' => GreaterBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  290. '>=' => ['precedence' => 20, 'class' => GreaterEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  291. '<=' => ['precedence' => 20, 'class' => LessEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  292. 'not in' => ['precedence' => 20, 'class' => NotInBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  293. 'in' => ['precedence' => 20, 'class' => InBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  294. 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  295. 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  296. 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  297. '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  298. '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  299. '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  300. '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  301. '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  302. '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  303. '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  304. '%' => ['precedence' => 60, 'class' => ModBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  305. 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  306. 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
  307. '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
  308. '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
  309. ],
  310. ];
  311. }
  312. }
  313. class_alias('Twig\Extension\CoreExtension', 'Twig_Extension_Core');
  314. }
  315. namespace {
  316. use Twig\Environment;
  317. use Twig\Error\LoaderError;
  318. use Twig\Error\RuntimeError;
  319. use Twig\Extension\CoreExtension;
  320. use Twig\Extension\SandboxExtension;
  321. use Twig\Markup;
  322. use Twig\Source;
  323. use Twig\Template;
  324. use Twig\TemplateWrapper;
  325. /**
  326. * Cycles over a value.
  327. *
  328. * @param \ArrayAccess|array $values
  329. * @param int $position The cycle position
  330. *
  331. * @return string The next value in the cycle
  332. */
  333. function twig_cycle($values, $position)
  334. {
  335. if (!\is_array($values) && !$values instanceof \ArrayAccess) {
  336. return $values;
  337. }
  338. return $values[$position % \count($values)];
  339. }
  340. /**
  341. * Returns a random value depending on the supplied parameter type:
  342. * - a random item from a \Traversable or array
  343. * - a random character from a string
  344. * - a random integer between 0 and the integer parameter.
  345. *
  346. * @param \Traversable|array|int|float|string $values The values to pick a random item from
  347. * @param int|null $max Maximum value used when $values is an int
  348. *
  349. * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
  350. *
  351. * @return mixed A random value from the given sequence
  352. */
  353. function twig_random(Environment $env, $values = null, $max = null)
  354. {
  355. if (null === $values) {
  356. return null === $max ? mt_rand() : mt_rand(0, (int) $max);
  357. }
  358. if (\is_int($values) || \is_float($values)) {
  359. if (null === $max) {
  360. if ($values < 0) {
  361. $max = 0;
  362. $min = $values;
  363. } else {
  364. $max = $values;
  365. $min = 0;
  366. }
  367. } else {
  368. $min = $values;
  369. $max = $max;
  370. }
  371. return mt_rand((int) $min, (int) $max);
  372. }
  373. if (\is_string($values)) {
  374. if ('' === $values) {
  375. return '';
  376. }
  377. $charset = $env->getCharset();
  378. if ('UTF-8' !== $charset) {
  379. $values = twig_convert_encoding($values, 'UTF-8', $charset);
  380. }
  381. // unicode version of str_split()
  382. // split at all positions, but not after the start and not before the end
  383. $values = preg_split('/(?<!^)(?!$)/u', $values);
  384. if ('UTF-8' !== $charset) {
  385. foreach ($values as $i => $value) {
  386. $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
  387. }
  388. }
  389. }
  390. if (!twig_test_iterable($values)) {
  391. return $values;
  392. }
  393. $values = twig_to_array($values);
  394. if (0 === \count($values)) {
  395. throw new RuntimeError('The random function cannot pick from an empty array.');
  396. }
  397. return $values[array_rand($values, 1)];
  398. }
  399. /**
  400. * Converts a date to the given format.
  401. *
  402. * {{ post.published_at|date("m/d/Y") }}
  403. *
  404. * @param \DateTimeInterface|\DateInterval|string $date A date
  405. * @param string|null $format The target format, null to use the default
  406. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  407. *
  408. * @return string The formatted date
  409. */
  410. function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
  411. {
  412. if (null === $format) {
  413. $formats = $env->getExtension(CoreExtension::class)->getDateFormat();
  414. $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
  415. }
  416. if ($date instanceof \DateInterval) {
  417. return $date->format($format);
  418. }
  419. return twig_date_converter($env, $date, $timezone)->format($format);
  420. }
  421. /**
  422. * Returns a new date object modified.
  423. *
  424. * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
  425. *
  426. * @param \DateTimeInterface|string $date A date
  427. * @param string $modifier A modifier string
  428. *
  429. * @return \DateTimeInterface
  430. */
  431. function twig_date_modify_filter(Environment $env, $date, $modifier)
  432. {
  433. $date = twig_date_converter($env, $date, false);
  434. return $date->modify($modifier);
  435. }
  436. /**
  437. * Returns a formatted string.
  438. *
  439. * @param string|null $format
  440. * @param ...$values
  441. *
  442. * @return string
  443. */
  444. function twig_sprintf($format, ...$values)
  445. {
  446. return sprintf($format ?? '', ...$values);
  447. }
  448. /**
  449. * Converts an input to a \DateTime instance.
  450. *
  451. * {% if date(user.created_at) < date('+2days') %}
  452. * {# do something #}
  453. * {% endif %}
  454. *
  455. * @param \DateTimeInterface|string|null $date A date or null to use the current time
  456. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  457. *
  458. * @return \DateTimeInterface
  459. */
  460. function twig_date_converter(Environment $env, $date = null, $timezone = null)
  461. {
  462. // determine the timezone
  463. if (false !== $timezone) {
  464. if (null === $timezone) {
  465. $timezone = $env->getExtension(CoreExtension::class)->getTimezone();
  466. } elseif (!$timezone instanceof \DateTimeZone) {
  467. $timezone = new \DateTimeZone($timezone);
  468. }
  469. }
  470. // immutable dates
  471. if ($date instanceof \DateTimeImmutable) {
  472. return false !== $timezone ? $date->setTimezone($timezone) : $date;
  473. }
  474. if ($date instanceof \DateTimeInterface) {
  475. $date = clone $date;
  476. if (false !== $timezone) {
  477. $date->setTimezone($timezone);
  478. }
  479. return $date;
  480. }
  481. if (null === $date || 'now' === $date) {
  482. if (null === $date) {
  483. $date = 'now';
  484. }
  485. return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone());
  486. }
  487. $asString = (string) $date;
  488. if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
  489. $date = new \DateTime('@'.$date);
  490. } else {
  491. $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone());
  492. }
  493. if (false !== $timezone) {
  494. $date->setTimezone($timezone);
  495. }
  496. return $date;
  497. }
  498. /**
  499. * Replaces strings within a string.
  500. *
  501. * @param string|null $str String to replace in
  502. * @param array|\Traversable $from Replace values
  503. *
  504. * @return string
  505. */
  506. function twig_replace_filter($str, $from)
  507. {
  508. if (!twig_test_iterable($from)) {
  509. throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
  510. }
  511. return strtr($str ?? '', twig_to_array($from));
  512. }
  513. /**
  514. * Rounds a number.
  515. *
  516. * @param int|float|string|null $value The value to round
  517. * @param int|float $precision The rounding precision
  518. * @param string $method The method to use for rounding
  519. *
  520. * @return int|float The rounded number
  521. */
  522. function twig_round($value, $precision = 0, $method = 'common')
  523. {
  524. $value = (float) $value;
  525. if ('common' === $method) {
  526. return round($value, $precision);
  527. }
  528. if ('ceil' !== $method && 'floor' !== $method) {
  529. throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
  530. }
  531. return $method($value * 10 ** $precision) / 10 ** $precision;
  532. }
  533. /**
  534. * Number format filter.
  535. *
  536. * All of the formatting options can be left null, in that case the defaults will
  537. * be used. Supplying any of the parameters will override the defaults set in the
  538. * environment object.
  539. *
  540. * @param mixed $number A float/int/string of the number to format
  541. * @param int $decimal the number of decimal points to display
  542. * @param string $decimalPoint the character(s) to use for the decimal point
  543. * @param string $thousandSep the character(s) to use for the thousands separator
  544. *
  545. * @return string The formatted number
  546. */
  547. function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
  548. {
  549. $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat();
  550. if (null === $decimal) {
  551. $decimal = $defaults[0];
  552. }
  553. if (null === $decimalPoint) {
  554. $decimalPoint = $defaults[1];
  555. }
  556. if (null === $thousandSep) {
  557. $thousandSep = $defaults[2];
  558. }
  559. return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
  560. }
  561. /**
  562. * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
  563. *
  564. * @param string|array|null $url A URL or an array of query parameters
  565. *
  566. * @return string The URL encoded value
  567. */
  568. function twig_urlencode_filter($url)
  569. {
  570. if (\is_array($url)) {
  571. return http_build_query($url, '', '&', \PHP_QUERY_RFC3986);
  572. }
  573. return rawurlencode($url ?? '');
  574. }
  575. /**
  576. * Merges an array with another one.
  577. *
  578. * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
  579. *
  580. * {% set items = items|merge({ 'peugeot': 'car' }) %}
  581. *
  582. * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
  583. *
  584. * @param array|\Traversable $arr1 An array
  585. * @param array|\Traversable $arr2 An array
  586. *
  587. * @return array The merged array
  588. */
  589. function twig_array_merge($arr1, $arr2)
  590. {
  591. if (!twig_test_iterable($arr1)) {
  592. throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1)));
  593. }
  594. if (!twig_test_iterable($arr2)) {
  595. throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2)));
  596. }
  597. return array_merge(twig_to_array($arr1), twig_to_array($arr2));
  598. }
  599. /**
  600. * Slices a variable.
  601. *
  602. * @param mixed $item A variable
  603. * @param int $start Start of the slice
  604. * @param int $length Size of the slice
  605. * @param bool $preserveKeys Whether to preserve key or not (when the input is an array)
  606. *
  607. * @return mixed The sliced variable
  608. */
  609. function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
  610. {
  611. if ($item instanceof \Traversable) {
  612. while ($item instanceof \IteratorAggregate) {
  613. $item = $item->getIterator();
  614. }
  615. if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
  616. try {
  617. return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys);
  618. } catch (\OutOfBoundsException $e) {
  619. return [];
  620. }
  621. }
  622. $item = iterator_to_array($item, $preserveKeys);
  623. }
  624. if (\is_array($item)) {
  625. return \array_slice($item, $start, $length, $preserveKeys);
  626. }
  627. return (string) mb_substr((string) $item, $start, $length, $env->getCharset());
  628. }
  629. /**
  630. * Returns the first element of the item.
  631. *
  632. * @param mixed $item A variable
  633. *
  634. * @return mixed The first element of the item
  635. */
  636. function twig_first(Environment $env, $item)
  637. {
  638. $elements = twig_slice($env, $item, 0, 1, false);
  639. return \is_string($elements) ? $elements : current($elements);
  640. }
  641. /**
  642. * Returns the last element of the item.
  643. *
  644. * @param mixed $item A variable
  645. *
  646. * @return mixed The last element of the item
  647. */
  648. function twig_last(Environment $env, $item)
  649. {
  650. $elements = twig_slice($env, $item, -1, 1, false);
  651. return \is_string($elements) ? $elements : current($elements);
  652. }
  653. /**
  654. * Joins the values to a string.
  655. *
  656. * The separators between elements are empty strings per default, you can define them with the optional parameters.
  657. *
  658. * {{ [1, 2, 3]|join(', ', ' and ') }}
  659. * {# returns 1, 2 and 3 #}
  660. *
  661. * {{ [1, 2, 3]|join('|') }}
  662. * {# returns 1|2|3 #}
  663. *
  664. * {{ [1, 2, 3]|join }}
  665. * {# returns 123 #}
  666. *
  667. * @param array $value An array
  668. * @param string $glue The separator
  669. * @param string|null $and The separator for the last pair
  670. *
  671. * @return string The concatenated string
  672. */
  673. function twig_join_filter($value, $glue = '', $and = null)
  674. {
  675. if (!twig_test_iterable($value)) {
  676. $value = (array) $value;
  677. }
  678. $value = twig_to_array($value, false);
  679. if (0 === \count($value)) {
  680. return '';
  681. }
  682. if (null === $and || $and === $glue) {
  683. return implode($glue, $value);
  684. }
  685. if (1 === \count($value)) {
  686. return $value[0];
  687. }
  688. return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
  689. }
  690. /**
  691. * Splits the string into an array.
  692. *
  693. * {{ "one,two,three"|split(',') }}
  694. * {# returns [one, two, three] #}
  695. *
  696. * {{ "one,two,three,four,five"|split(',', 3) }}
  697. * {# returns [one, two, "three,four,five"] #}
  698. *
  699. * {{ "123"|split('') }}
  700. * {# returns [1, 2, 3] #}
  701. *
  702. * {{ "aabbcc"|split('', 2) }}
  703. * {# returns [aa, bb, cc] #}
  704. *
  705. * @param string|null $value A string
  706. * @param string $delimiter The delimiter
  707. * @param int $limit The limit
  708. *
  709. * @return array The split string as an array
  710. */
  711. function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
  712. {
  713. $value = $value ?? '';
  714. if (\strlen($delimiter) > 0) {
  715. return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
  716. }
  717. if ($limit <= 1) {
  718. return preg_split('/(?<!^)(?!$)/u', $value);
  719. }
  720. $length = mb_strlen($value, $env->getCharset());
  721. if ($length < $limit) {
  722. return [$value];
  723. }
  724. $r = [];
  725. for ($i = 0; $i < $length; $i += $limit) {
  726. $r[] = mb_substr($value, $i, $limit, $env->getCharset());
  727. }
  728. return $r;
  729. }
  730. // The '_default' filter is used internally to avoid using the ternary operator
  731. // which costs a lot for big contexts (before PHP 5.4). So, on average,
  732. // a function call is cheaper.
  733. /**
  734. * @internal
  735. */
  736. function _twig_default_filter($value, $default = '')
  737. {
  738. if (twig_test_empty($value)) {
  739. return $default;
  740. }
  741. return $value;
  742. }
  743. /**
  744. * Returns the keys for the given array.
  745. *
  746. * It is useful when you want to iterate over the keys of an array:
  747. *
  748. * {% for key in array|keys %}
  749. * {# ... #}
  750. * {% endfor %}
  751. *
  752. * @param array $array An array
  753. *
  754. * @return array The keys
  755. */
  756. function twig_get_array_keys_filter($array)
  757. {
  758. if ($array instanceof \Traversable) {
  759. while ($array instanceof \IteratorAggregate) {
  760. $array = $array->getIterator();
  761. }
  762. if ($array instanceof \Iterator) {
  763. $keys = [];
  764. $array->rewind();
  765. while ($array->valid()) {
  766. $keys[] = $array->key();
  767. $array->next();
  768. }
  769. return $keys;
  770. }
  771. $keys = [];
  772. foreach ($array as $key => $item) {
  773. $keys[] = $key;
  774. }
  775. return $keys;
  776. }
  777. if (!\is_array($array)) {
  778. return [];
  779. }
  780. return array_keys($array);
  781. }
  782. /**
  783. * Reverses a variable.
  784. *
  785. * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string
  786. * @param bool $preserveKeys Whether to preserve key or not
  787. *
  788. * @return mixed The reversed input
  789. */
  790. function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
  791. {
  792. if ($item instanceof \Traversable) {
  793. return array_reverse(iterator_to_array($item), $preserveKeys);
  794. }
  795. if (\is_array($item)) {
  796. return array_reverse($item, $preserveKeys);
  797. }
  798. $string = (string) $item;
  799. $charset = $env->getCharset();
  800. if ('UTF-8' !== $charset) {
  801. $string = twig_convert_encoding($string, 'UTF-8', $charset);
  802. }
  803. preg_match_all('/./us', $string, $matches);
  804. $string = implode('', array_reverse($matches[0]));
  805. if ('UTF-8' !== $charset) {
  806. $string = twig_convert_encoding($string, $charset, 'UTF-8');
  807. }
  808. return $string;
  809. }
  810. /**
  811. * Sorts an array.
  812. *
  813. * @param array|\Traversable $array
  814. *
  815. * @return array
  816. */
  817. function twig_sort_filter(Environment $env, $array, $arrow = null)
  818. {
  819. if ($array instanceof \Traversable) {
  820. $array = iterator_to_array($array);
  821. } elseif (!\is_array($array)) {
  822. throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
  823. }
  824. if (null !== $arrow) {
  825. twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter');
  826. uasort($array, $arrow);
  827. } else {
  828. asort($array);
  829. }
  830. return $array;
  831. }
  832. /**
  833. * @internal
  834. */
  835. function twig_in_filter($value, $compare)
  836. {
  837. if ($value instanceof Markup) {
  838. $value = (string) $value;
  839. }
  840. if ($compare instanceof Markup) {
  841. $compare = (string) $compare;
  842. }
  843. if (\is_array($compare)) {
  844. return \in_array($value, $compare, \is_object($value) || \is_resource($value));
  845. } elseif (\is_string($compare) && (\is_string($value) || \is_int($value) || \is_float($value))) {
  846. return '' === $value || false !== strpos($compare, (string) $value);
  847. } elseif ($compare instanceof \Traversable) {
  848. if (\is_object($value) || \is_resource($value)) {
  849. foreach ($compare as $item) {
  850. if ($item === $value) {
  851. return true;
  852. }
  853. }
  854. } else {
  855. foreach ($compare as $item) {
  856. if ($item == $value) {
  857. return true;
  858. }
  859. }
  860. }
  861. return false;
  862. }
  863. return false;
  864. }
  865. /**
  866. * Returns a trimmed string.
  867. *
  868. * @param string|null $string
  869. * @param string|null $characterMask
  870. * @param string $side
  871. *
  872. * @return string
  873. *
  874. * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
  875. */
  876. function twig_trim_filter($string, $characterMask = null, $side = 'both')
  877. {
  878. if (null === $characterMask) {
  879. $characterMask = " \t\n\r\0\x0B";
  880. }
  881. switch ($side) {
  882. case 'both':
  883. return trim($string ?? '', $characterMask);
  884. case 'left':
  885. return ltrim($string ?? '', $characterMask);
  886. case 'right':
  887. return rtrim($string ?? '', $characterMask);
  888. default:
  889. throw new RuntimeError('Trimming side must be "left", "right" or "both".');
  890. }
  891. }
  892. /**
  893. * Inserts HTML line breaks before all newlines in a string.
  894. *
  895. * @param string|null $string
  896. *
  897. * @return string
  898. */
  899. function twig_nl2br($string)
  900. {
  901. return nl2br($string ?? '');
  902. }
  903. /**
  904. * Removes whitespaces between HTML tags.
  905. *
  906. * @param string|null $string
  907. *
  908. * @return string
  909. */
  910. function twig_spaceless($content)
  911. {
  912. return trim(preg_replace('/>\s+</', '><', $content ?? ''));
  913. }
  914. /**
  915. * @param string|null $string
  916. * @param string $to
  917. * @param string $from
  918. *
  919. * @return string
  920. */
  921. function twig_convert_encoding($string, $to, $from)
  922. {
  923. if (!\function_exists('iconv')) {
  924. throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
  925. }
  926. return iconv($from, $to, $string ?? '');
  927. }
  928. /**
  929. * Returns the length of a variable.
  930. *
  931. * @param mixed $thing A variable
  932. *
  933. * @return int The length of the value
  934. */
  935. function twig_length_filter(Environment $env, $thing)
  936. {
  937. if (null === $thing) {
  938. return 0;
  939. }
  940. if (is_scalar($thing)) {
  941. return mb_strlen($thing, $env->getCharset());
  942. }
  943. if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) {
  944. return \count($thing);
  945. }
  946. if ($thing instanceof \Traversable) {
  947. return iterator_count($thing);
  948. }
  949. if (method_exists($thing, '__toString') && !$thing instanceof \Countable) {
  950. return mb_strlen((string) $thing, $env->getCharset());
  951. }
  952. return 1;
  953. }
  954. /**
  955. * Converts a string to uppercase.
  956. *
  957. * @param string|null $string A string
  958. *
  959. * @return string The uppercased string
  960. */
  961. function twig_upper_filter(Environment $env, $string)
  962. {
  963. return mb_strtoupper($string ?? '', $env->getCharset());
  964. }
  965. /**
  966. * Converts a string to lowercase.
  967. *
  968. * @param string|null $string A string
  969. *
  970. * @return string The lowercased string
  971. */
  972. function twig_lower_filter(Environment $env, $string)
  973. {
  974. return mb_strtolower($string ?? '', $env->getCharset());
  975. }
  976. /**
  977. * Strips HTML and PHP tags from a string.
  978. *
  979. * @param string|null $string
  980. * @param string[]|string|null $string
  981. *
  982. * @return string
  983. */
  984. function twig_striptags($string, $allowable_tags = null)
  985. {
  986. return strip_tags($string ?? '', $allowable_tags);
  987. }
  988. /**
  989. * Returns a titlecased string.
  990. *
  991. * @param string|null $string A string
  992. *
  993. * @return string The titlecased string
  994. */
  995. function twig_title_string_filter(Environment $env, $string)
  996. {
  997. if (null !== $charset = $env->getCharset()) {
  998. return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset);
  999. }
  1000. return ucwords(strtolower($string ?? ''));
  1001. }
  1002. /**
  1003. * Returns a capitalized string.
  1004. *
  1005. * @param string|null $string A string
  1006. *
  1007. * @return string The capitalized string
  1008. */
  1009. function twig_capitalize_string_filter(Environment $env, $string)
  1010. {
  1011. $charset = $env->getCharset();
  1012. return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset);
  1013. }
  1014. /**
  1015. * @internal
  1016. */
  1017. function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
  1018. {
  1019. if (!method_exists($template, $method)) {
  1020. $parent = $template;
  1021. while ($parent = $parent->getParent($context)) {
  1022. if (method_exists($parent, $method)) {
  1023. return $parent->$method(...$args);
  1024. }
  1025. }
  1026. throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source);
  1027. }
  1028. return $template->$method(...$args);
  1029. }
  1030. /**
  1031. * @internal
  1032. */
  1033. function twig_ensure_traversable($seq)
  1034. {
  1035. if ($seq instanceof \Traversable || \is_array($seq)) {
  1036. return $seq;
  1037. }
  1038. return [];
  1039. }
  1040. /**
  1041. * @internal
  1042. */
  1043. function twig_to_array($seq, $preserveKeys = true)
  1044. {
  1045. if ($seq instanceof \Traversable) {
  1046. return iterator_to_array($seq, $preserveKeys);
  1047. }
  1048. if (!\is_array($seq)) {
  1049. return $seq;
  1050. }
  1051. return $preserveKeys ? $seq : array_values($seq);
  1052. }
  1053. /**
  1054. * Checks if a variable is empty.
  1055. *
  1056. * {# evaluates to true if the foo variable is null, false, or the empty string #}
  1057. * {% if foo is empty %}
  1058. * {# ... #}
  1059. * {% endif %}
  1060. *
  1061. * @param mixed $value A variable
  1062. *
  1063. * @return bool true if the value is empty, false otherwise
  1064. */
  1065. function twig_test_empty($value)
  1066. {
  1067. if ($value instanceof \Countable) {
  1068. return 0 === \count($value);
  1069. }
  1070. if ($value instanceof \Traversable) {
  1071. return !iterator_count($value);
  1072. }
  1073. if (\is_object($value) && method_exists($value, '__toString')) {
  1074. return '' === (string) $value;
  1075. }
  1076. return '' === $value || false === $value || null === $value || [] === $value;
  1077. }
  1078. /**
  1079. * Checks if a variable is traversable.
  1080. *
  1081. * {# evaluates to true if the foo variable is an array or a traversable object #}
  1082. * {% if foo is iterable %}
  1083. * {# ... #}
  1084. * {% endif %}
  1085. *
  1086. * @param mixed $value A variable
  1087. *
  1088. * @return bool true if the value is traversable
  1089. */
  1090. function twig_test_iterable($value)
  1091. {
  1092. return $value instanceof \Traversable || \is_array($value);
  1093. }
  1094. /**
  1095. * Renders a template.
  1096. *
  1097. * @param array $context
  1098. * @param string|array $template The template to render or an array of templates to try consecutively
  1099. * @param array $variables The variables to pass to the template
  1100. * @param bool $withContext
  1101. * @param bool $ignoreMissing Whether to ignore missing templates or not
  1102. * @param bool $sandboxed Whether to sandbox the template or not
  1103. *
  1104. * @return string The rendered template
  1105. */
  1106. function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
  1107. {
  1108. $alreadySandboxed = false;
  1109. $sandbox = null;
  1110. if ($withContext) {
  1111. $variables = array_merge($context, $variables);
  1112. }
  1113. if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
  1114. $sandbox = $env->getExtension(SandboxExtension::class);
  1115. if (!$alreadySandboxed = $sandbox->isSandboxed()) {
  1116. $sandbox->enableSandbox();
  1117. }
  1118. }
  1119. try {
  1120. $loaded = null;
  1121. try {
  1122. $loaded = $env->resolveTemplate($template);
  1123. } catch (LoaderError $e) {
  1124. if (!$ignoreMissing) {
  1125. throw $e;
  1126. }
  1127. }
  1128. if ($isSandboxed && $loaded) {
  1129. $loaded->unwrap()->checkSecurity();
  1130. }
  1131. return $loaded ? $loaded->render($variables) : '';
  1132. } finally {
  1133. if ($isSandboxed && !$alreadySandboxed) {
  1134. $sandbox->disableSandbox();
  1135. }
  1136. }
  1137. }
  1138. /**
  1139. * Returns a template content without rendering it.
  1140. *
  1141. * @param string $name The template name
  1142. * @param bool $ignoreMissing Whether to ignore missing templates or not
  1143. *
  1144. * @return string The template source
  1145. */
  1146. function twig_source(Environment $env, $name, $ignoreMissing = false)
  1147. {
  1148. $loader = $env->getLoader();
  1149. try {
  1150. return $loader->getSourceContext($name)->getCode();
  1151. } catch (LoaderError $e) {
  1152. if (!$ignoreMissing) {
  1153. throw $e;
  1154. }
  1155. }
  1156. }
  1157. /**
  1158. * Provides the ability to get constants from instances as well as class/global constants.
  1159. *
  1160. * @param string $constant The name of the constant
  1161. * @param object|null $object The object to get the constant from
  1162. *
  1163. * @return string
  1164. */
  1165. function twig_constant($constant, $object = null)
  1166. {
  1167. if (null !== $object) {
  1168. $constant = \get_class($object).'::'.$constant;
  1169. }
  1170. if (!\defined($constant)) {
  1171. throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant));
  1172. }
  1173. return \constant($constant);
  1174. }
  1175. /**
  1176. * Checks if a constant exists.
  1177. *
  1178. * @param string $constant The name of the constant
  1179. * @param object|null $object The object to get the constant from
  1180. *
  1181. * @return bool
  1182. */
  1183. function twig_constant_is_defined($constant, $object = null)
  1184. {
  1185. if (null !== $object) {
  1186. $constant = \get_class($object).'::'.$constant;
  1187. }
  1188. return \defined($constant);
  1189. }
  1190. /**
  1191. * Batches item.
  1192. *
  1193. * @param array $items An array of items
  1194. * @param int $size The size of the batch
  1195. * @param mixed $fill A value used to fill missing items
  1196. *
  1197. * @return array
  1198. */
  1199. function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
  1200. {
  1201. if (!twig_test_iterable($items)) {
  1202. throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items)));
  1203. }
  1204. $size = ceil($size);
  1205. $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys);
  1206. if (null !== $fill && $result) {
  1207. $last = \count($result) - 1;
  1208. if ($fillCount = $size - \count($result[$last])) {
  1209. for ($i = 0; $i < $fillCount; ++$i) {
  1210. $result[$last][] = $fill;
  1211. }
  1212. }
  1213. }
  1214. return $result;
  1215. }
  1216. /**
  1217. * Returns the attribute value for a given array/object.
  1218. *
  1219. * @param mixed $object The object or array from where to get the item
  1220. * @param mixed $item The item to get from the array or object
  1221. * @param array $arguments An array of arguments to pass if the item is an object method
  1222. * @param string $type The type of attribute (@see \Twig\Template constants)
  1223. * @param bool $isDefinedTest Whether this is only a defined check
  1224. * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not
  1225. * @param int $lineno The template line where the attribute was called
  1226. *
  1227. * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true
  1228. *
  1229. * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false
  1230. *
  1231. * @internal
  1232. */
  1233. function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
  1234. {
  1235. // array
  1236. if (/* Template::METHOD_CALL */ 'method' !== $type) {
  1237. $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
  1238. if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object)))
  1239. || ($object instanceof ArrayAccess && isset($object[$arrayItem]))
  1240. ) {
  1241. if ($isDefinedTest) {
  1242. return true;
  1243. }
  1244. return $object[$arrayItem];
  1245. }
  1246. if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) {
  1247. if ($isDefinedTest) {
  1248. return false;
  1249. }
  1250. if ($ignoreStrictCheck || !$env->isStrictVariables()) {
  1251. return;
  1252. }
  1253. if ($object instanceof ArrayAccess) {
  1254. $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object));
  1255. } elseif (\is_object($object)) {
  1256. $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object));
  1257. } elseif (\is_array($object)) {
  1258. if (empty($object)) {
  1259. $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem);
  1260. } else {
  1261. $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object)));
  1262. }
  1263. } elseif (/* Template::ARRAY_CALL */ 'array' === $type) {
  1264. if (null === $object) {
  1265. $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item);
  1266. } else {
  1267. $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
  1268. }
  1269. } elseif (null === $object) {
  1270. $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item);
  1271. } else {
  1272. $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
  1273. }
  1274. throw new RuntimeError($message, $lineno, $source);
  1275. }
  1276. }
  1277. if (!\is_object($object)) {
  1278. if ($isDefinedTest) {
  1279. return false;
  1280. }
  1281. if ($ignoreStrictCheck || !$env->isStrictVariables()) {
  1282. return;
  1283. }
  1284. if (null === $object) {
  1285. $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item);
  1286. } elseif (\is_array($object)) {
  1287. $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item);
  1288. } else {
  1289. $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object);
  1290. }
  1291. throw new RuntimeError($message, $lineno, $source);
  1292. }
  1293. if ($object instanceof Template) {
  1294. throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source);
  1295. }
  1296. // object property
  1297. if (/* Template::METHOD_CALL */ 'method' !== $type) {
  1298. if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) {
  1299. if ($isDefinedTest) {
  1300. return true;
  1301. }
  1302. if ($sandboxed) {
  1303. $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source);
  1304. }
  1305. return $object->$item;
  1306. }
  1307. }
  1308. static $cache = [];
  1309. $class = \get_class($object);
  1310. // object method
  1311. // precedence: getXxx() > isXxx() > hasXxx()
  1312. if (!isset($cache[$class])) {
  1313. $methods = get_class_methods($object);
  1314. sort($methods);
  1315. $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods);
  1316. $classCache = [];
  1317. foreach ($methods as $i => $method) {
  1318. $classCache[$method] = $method;
  1319. $classCache[$lcName = $lcMethods[$i]] = $method;
  1320. if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) {
  1321. $name = substr($method, 3);
  1322. $lcName = substr($lcName, 3);
  1323. } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) {
  1324. $name = substr($method, 2);
  1325. $lcName = substr($lcName, 2);
  1326. } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) {
  1327. $name = substr($method, 3);
  1328. $lcName = substr($lcName, 3);
  1329. if (\in_array('is'.$lcName, $lcMethods)) {
  1330. continue;
  1331. }
  1332. } else {
  1333. continue;
  1334. }
  1335. // skip get() and is() methods (in which case, $name is empty)
  1336. if ($name) {
  1337. if (!isset($classCache[$name])) {
  1338. $classCache[$name] = $method;
  1339. }
  1340. if (!isset($classCache[$lcName])) {
  1341. $classCache[$lcName] = $method;
  1342. }
  1343. }
  1344. }
  1345. $cache[$class] = $classCache;
  1346. }
  1347. $call = false;
  1348. if (isset($cache[$class][$item])) {
  1349. $method = $cache[$class][$item];
  1350. } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) {
  1351. $method = $cache[$class][$lcItem];
  1352. } elseif (isset($cache[$class]['__call'])) {
  1353. $method = $item;
  1354. $call = true;
  1355. } else {
  1356. if ($isDefinedTest) {
  1357. return false;
  1358. }
  1359. if ($ignoreStrictCheck || !$env->isStrictVariables()) {
  1360. return;
  1361. }
  1362. 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);
  1363. }
  1364. if ($isDefinedTest) {
  1365. return true;
  1366. }
  1367. if ($sandboxed) {
  1368. $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source);
  1369. }
  1370. // Some objects throw exceptions when they have __call, and the method we try
  1371. // to call is not supported. If ignoreStrictCheck is true, we should return null.
  1372. try {
  1373. $ret = $object->$method(...$arguments);
  1374. } catch (\BadMethodCallException $e) {
  1375. if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
  1376. return;
  1377. }
  1378. throw $e;
  1379. }
  1380. return $ret;
  1381. }
  1382. /**
  1383. * Returns the values from a single column in the input array.
  1384. *
  1385. * <pre>
  1386. * {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
  1387. *
  1388. * {% set fruits = items|column('fruit') %}
  1389. *
  1390. * {# fruits now contains ['apple', 'orange'] #}
  1391. * </pre>
  1392. *
  1393. * @param array|Traversable $array An array
  1394. * @param mixed $name The column name
  1395. * @param mixed $index The column to use as the index/keys for the returned array
  1396. *
  1397. * @return array The array of values
  1398. */
  1399. function twig_array_column($array, $name, $index = null): array
  1400. {
  1401. if ($array instanceof Traversable) {
  1402. $array = iterator_to_array($array);
  1403. } elseif (!\is_array($array)) {
  1404. throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
  1405. }
  1406. return array_column($array, $name, $index);
  1407. }
  1408. function twig_array_filter(Environment $env, $array, $arrow)
  1409. {
  1410. if (!twig_test_iterable($array)) {
  1411. throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
  1412. }
  1413. twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter');
  1414. if (\is_array($array)) {
  1415. return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
  1416. }
  1417. // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
  1418. return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
  1419. }
  1420. function twig_array_map(Environment $env, $array, $arrow)
  1421. {
  1422. twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter');
  1423. $r = [];
  1424. foreach ($array as $k => $v) {
  1425. $r[$k] = $arrow($v, $k);
  1426. }
  1427. return $r;
  1428. }
  1429. function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
  1430. {
  1431. twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter');
  1432. if (!\is_array($array)) {
  1433. if (!$array instanceof \Traversable) {
  1434. throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
  1435. }
  1436. $array = iterator_to_array($array);
  1437. }
  1438. return array_reduce($array, $arrow, $initial);
  1439. }
  1440. function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
  1441. {
  1442. if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
  1443. throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
  1444. }
  1445. }
  1446. }