This commit is contained in:
marco
2026-09-01 10:46:43 -03:00
parent bf96b77e9c
commit 34d33e1520
297 changed files with 14973 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Profiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Profile implements \IteratorAggregate, \Serializable
{
public const ROOT = 'ROOT';
public const BLOCK = 'block';
public const TEMPLATE = 'template';
public const MACRO = 'macro';
private $starts = [];
private $ends = [];
private $profiles = [];
public function __construct(
private string $template = 'main',
private string $type = self::ROOT,
private string $name = 'main',
) {
$this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name;
$this->enter();
}
public function getTemplate(): string
{
return $this->template;
}
public function getType(): string
{
return $this->type;
}
public function getName(): string
{
return $this->name;
}
public function isRoot(): bool
{
return self::ROOT === $this->type;
}
public function isTemplate(): bool
{
return self::TEMPLATE === $this->type;
}
public function isBlock(): bool
{
return self::BLOCK === $this->type;
}
public function isMacro(): bool
{
return self::MACRO === $this->type;
}
/**
* @return Profile[]
*/
public function getProfiles(): array
{
return $this->profiles;
}
public function addProfile(self $profile): void
{
$this->profiles[] = $profile;
}
/**
* Returns the duration in microseconds.
*/
public function getDuration(): float
{
if ($this->isRoot() && $this->profiles) {
// for the root node with children, duration is the sum of all child durations
$duration = 0;
foreach ($this->profiles as $profile) {
$duration += $profile->getDuration();
}
return $duration;
}
return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0;
}
/**
* Returns the start time in microseconds.
*/
public function getStartTime(): float
{
return $this->starts['wt'] ?? 0.0;
}
/**
* Returns the end time in microseconds.
*/
public function getEndTime(): float
{
return $this->ends['wt'] ?? 0.0;
}
/**
* Returns the memory usage in bytes.
*/
public function getMemoryUsage(): int
{
return isset($this->ends['mu']) && isset($this->starts['mu']) ? $this->ends['mu'] - $this->starts['mu'] : 0;
}
/**
* Returns the peak memory usage in bytes.
*/
public function getPeakMemoryUsage(): int
{
return isset($this->ends['pmu']) && isset($this->starts['pmu']) ? $this->ends['pmu'] - $this->starts['pmu'] : 0;
}
/**
* Starts the profiling.
*/
public function enter(): void
{
$this->starts = [
'wt' => microtime(true),
'mu' => memory_get_usage(),
'pmu' => memory_get_peak_usage(),
];
}
/**
* Stops the profiling.
*/
public function leave(): void
{
$this->ends = [
'wt' => microtime(true),
'mu' => memory_get_usage(),
'pmu' => memory_get_peak_usage(),
];
}
public function reset(): void
{
$this->starts = $this->ends = $this->profiles = [];
$this->enter();
}
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->profiles);
}
public function serialize(): string
{
return serialize($this->__serialize());
}
public function unserialize($data): void
{
$this->__unserialize(unserialize($data));
}
/**
* @internal
*/
public function __serialize(): array
{
return [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles];
}
/**
* @internal
*/
public function __unserialize(array $data): void
{
[$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data;
}
}
+541
View File
@@ -0,0 +1,541 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Extension\CoreExtension;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_cycle($values, $position)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::cycle($values, $position);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_random(Environment $env, $values = null, $max = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::random($env->getCharset(), $values, $max);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_date_modify_filter(Environment $env, $date, $modifier)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_sprintf($format, ...$values)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::sprintf($format, ...$values);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_date_converter(Environment $env, $date = null, $timezone = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_replace_filter($str, $from)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::replace($str, $from);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_round($value, $precision = 0, $method = 'common')
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::round($value, $precision, $method);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_urlencode_filter($url)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::urlencode($url);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_merge(...$arrays)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::merge(...$arrays);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_first(Environment $env, $item)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::first($env->getCharset(), $item);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_last(Environment $env, $item)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::last($env->getCharset(), $item);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_join_filter($value, $glue = '', $and = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::join($value, $glue, $and);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_get_array_keys_filter($array)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::keys($array);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_sort_filter(Environment $env, $array, $arrow = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::sort($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_matches(string $regexp, ?string $str)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::matches($regexp, $str);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_trim_filter($string, $characterMask = null, $side = 'both')
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::trim($string, $characterMask, $side);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_nl2br($string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::nl2br($string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_spaceless($content)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::spaceless($content);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_convert_encoding($string, $to, $from)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::convertEncoding($string, $to, $from);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_length_filter(Environment $env, $thing)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::length($env->getCharset(), $thing);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_upper_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::upper($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_lower_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::lower($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_striptags($string, $allowable_tags = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::striptags($string, $allowable_tags);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_title_string_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::titleCase($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_capitalize_string_filter(Environment $env, $string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::capitalize($env->getCharset(), $string);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_test_empty($value)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::testEmpty($value);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_test_iterable($value)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return is_iterable($value);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_source(Environment $env, $name, $ignoreMissing = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::source($env, $name, $ignoreMissing);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_constant($constant, $object = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::constant($constant, $object);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_constant_is_defined($constant, $object = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::constant($constant, $object, true);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::batch($items, $size, $fill, $preserveKeys);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_column($array, $name, $index = null): array
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::column($array, $name, $index);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_filter(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::filter($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_map(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::map($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::reduce($env, $array, $arrow, $initial);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_some(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::arraySome($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_array_every(Environment $env, $array, $arrow)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return CoreExtension::arrayEvery($env, $array, $arrow);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
CoreExtension::checkArrow($env, $arrow, $thing, $type);
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Extension\DebugExtension;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_var_dump(Environment $env, $context, ...$vars)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
DebugExtension::dump($env, $context, ...$vars);
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Extension\EscaperExtension;
use Twig\Node\Node;
use Twig\Runtime\EscaperRuntime;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_raw_filter($string)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $string;
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return $env->getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape);
}
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_escape_filter_is_safe(Node $filterArgs)
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return EscaperExtension::escapeFilterIsSafe($filterArgs);
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\Extension\StringLoaderExtension;
use Twig\TemplateWrapper;
/**
* @internal
*
* @deprecated since Twig 3.9
*/
function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper
{
trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
return StringLoaderExtension::templateFromString($env, $template, $name);
}
+340
View File
@@ -0,0 +1,340 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Runtime;
use Twig\Error\RuntimeError;
use Twig\Extension\RuntimeExtensionInterface;
use Twig\Markup;
final class EscaperRuntime implements RuntimeExtensionInterface
{
/** @var array<string, callable(string, string): string> */
private $escapers = [];
/** @internal */
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
public function __construct(
private $charset = 'UTF-8',
) {
}
/**
* Defines a new escaper to be used via the escape filter.
*
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable(string $string, string $charset): string $callable A valid PHP callable
*
* @return void
*/
public function setEscaper($strategy, callable $callable)
{
$this->escapers[$strategy] = $callable;
}
/**
* Gets all defined escapers.
*
* @return array<string, callable(string $string, string $charset): string> An array of escapers
*/
public function getEscapers()
{
return $this->escapers;
}
/**
* @param array<class-string<\Stringable>, string[]> $safeClasses
*
* @return void
*/
public function setSafeClasses(array $safeClasses = [])
{
$this->safeClasses = [];
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $strategies);
}
}
/**
* @param class-string<\Stringable> $class
* @param string[] $strategies
*
* @return void
*/
public function addSafeClass(string $class, array $strategies)
{
$class = ltrim($class, '\\');
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
foreach ($strategies as $strategy) {
$this->safeLookup[$strategy][$class] = true;
}
}
/**
* Escapes a string.
*
* @param mixed $string The value to be escaped
* @param string $strategy The escaping strategy
* @param string|null $charset The charset
* @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
*
* @throws RuntimeError
*/
public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false)
{
if ($autoescape && $string instanceof Markup) {
return $string;
}
if (!\is_string($string)) {
if ($string instanceof \Stringable) {
if ($autoescape) {
$c = $string::class;
if (!isset($this->safeClasses[$c])) {
$this->safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($this->safeClasses[$class])) {
$this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class]));
foreach ($this->safeClasses[$class] as $s) {
$this->safeLookup[$s][$c] = true;
}
}
}
}
if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) {
return (string) $string;
}
}
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
// we return the input as is (which can be of any type)
return $string;
}
}
if ('' === $string) {
return '';
}
$charset = $charset ?: $this->charset;
switch ($strategy) {
case 'html':
// see https://www.php.net/htmlspecialchars
if ('UTF-8' === $charset) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
}
// Using a static variable to avoid initializing the array
// each time the function is called. Moving the declaration on the
// top of the function slow downs other escaping strategies.
static $htmlspecialcharsCharsets = [
'ISO-8859-1' => true, 'ISO8859-1' => true,
'ISO-8859-15' => true, 'ISO8859-15' => true,
'utf-8' => true, 'UTF-8' => true,
'CP866' => true, 'IBM866' => true, '866' => true,
'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
'1251' => true,
'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
'BIG5' => true, '950' => true,
'GB2312' => true, '936' => true,
'BIG5-HKSCS' => true,
'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
'EUC-JP' => true, 'EUCJP' => true,
'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
];
if (isset($htmlspecialcharsCharsets[$charset])) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
// cache the lowercase variant for future iterations
$htmlspecialcharsCharsets[$charset] = true;
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
$string = $this->convertEncoding($string, 'UTF-8', $charset);
$string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
return iconv('UTF-8', $charset, $string);
case 'js':
// escape all non-alphanumeric characters
// into their \x or \uHHHH representations
if ('UTF-8' !== $charset) {
$string = $this->convertEncoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
$char = $matches[0];
/*
* A few characters have short escape sequences in JSON and JavaScript.
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
$short = match ($char) {
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
default => false,
};
if ($short) {
return $short;
}
$codepoint = mb_ord($char, 'UTF-8');
if (0x10000 > $codepoint) {
return \sprintf('\u%04X', $codepoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
$u = $codepoint - 0x10000;
$high = 0xD800 | ($u >> 10);
$low = 0xDC00 | ($u & 0x3FF);
return \sprintf('\u%04X\u%04X', $high, $low);
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'css':
if ('UTF-8' !== $charset) {
$string = $this->convertEncoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
$char = $matches[0];
return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'html_attr':
if ('UTF-8' !== $charset) {
$string = $this->convertEncoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
/**
* This function is adapted from code coming from Zend Framework.
*
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://framework.zend.com/license/new-bsd New BSD License
*/
$chr = $matches[0];
$ord = \ord($chr[0]);
/*
* The following replaces characters undefined in HTML with the
* hex entity for the Unicode replacement character.
*/
if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) {
return '&#xFFFD;';
}
/*
* Check if the current character to escape has a name entity we should
* replace it with while grabbing the hex value of the character.
*/
if (1 === \strlen($chr)) {
/*
* While HTML supports far more named entities, the lowest common denominator
* has become HTML5's XML Serialisation which is restricted to the those named
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*/
return match ($ord) {
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
default => \sprintf('&#x%02X;', $ord),
};
}
/*
* Per OWASP recommendations, we'll use hex entities for any other
* characters where a named entity does not exist.
*/
return \sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'url':
return rawurlencode($string);
default:
if (\array_key_exists($strategy, $this->escapers)) {
return $this->escapers[$strategy]($string, $charset);
}
$validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers)));
throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies));
}
}
private function convertEncoding(string $string, string $to, string $from)
{
if (!\function_exists('iconv')) {
throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.');
}
return iconv($from, $to, $string);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\RuntimeLoader;
use Psr\Container\ContainerInterface;
/**
* Lazily loads Twig runtime implementations from a PSR-11 container.
*
* Note that the runtime services MUST use their class names as identifiers.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ContainerRuntimeLoader implements RuntimeLoaderInterface
{
public function __construct(
private ContainerInterface $container,
) {
}
public function load(string $class)
{
return $this->container->has($class) ? $this->container->get($class) : null;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\RuntimeLoader;
/**
* Lazy loads the runtime implementations for a Twig element.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class FactoryRuntimeLoader implements RuntimeLoaderInterface
{
/**
* @param array $map An array where keys are class names and values factory callables
*/
public function __construct(
private array $map = [],
) {
}
public function load(string $class)
{
if (!isset($this->map[$class])) {
return null;
}
$runtimeFactory = $this->map[$class];
return $runtimeFactory();
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\RuntimeLoader;
/**
* Creates runtime implementations for Twig elements (filters/functions/tests).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface RuntimeLoaderInterface
{
/**
* Creates the runtime implementation of a Twig element (filter/function/test).
*
* @return object|null The runtime instance or null if the loader does not know how to create the runtime for this class
*/
public function load(string $class);
}
+23
View File
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
use Twig\Error\Error;
/**
* Exception thrown when a security error occurs at runtime.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class SecurityError extends Error
{
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed filter is used in a template.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
final class SecurityNotAllowedFilterError extends SecurityError
{
private string $filterName;
public function __construct(string $message, string $functionName)
{
parent::__construct($message);
$this->filterName = $functionName;
}
public function getFilterName(): string
{
return $this->filterName;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed function is used in a template.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
final class SecurityNotAllowedFunctionError extends SecurityError
{
private string $functionName;
public function __construct(string $message, string $functionName)
{
parent::__construct($message);
$this->functionName = $functionName;
}
public function getFunctionName(): string
{
return $this->functionName;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed class method is used in a template.
*
* @author Kit Burton-Senior <mail@kitbs.com>
*/
final class SecurityNotAllowedMethodError extends SecurityError
{
private string $className;
private string $methodName;
public function __construct(string $message, string $className, string $methodName)
{
parent::__construct($message);
$this->className = $className;
$this->methodName = $methodName;
}
public function getClassName(): string
{
return $this->className;
}
public function getMethodName(): string
{
return $this->methodName;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed class property is used in a template.
*
* @author Kit Burton-Senior <mail@kitbs.com>
*/
final class SecurityNotAllowedPropertyError extends SecurityError
{
private string $className;
private string $propertyName;
public function __construct(string $message, string $className, string $propertyName)
{
parent::__construct($message);
$this->className = $className;
$this->propertyName = $propertyName;
}
public function getClassName(): string
{
return $this->className;
}
public function getPropertyName(): string
{
return $this->propertyName;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Exception thrown when a not allowed tag is used in a template.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
final class SecurityNotAllowedTagError extends SecurityError
{
private string $tagName;
public function __construct(string $message, string $tagName)
{
parent::__construct($message);
$this->tagName = $tagName;
}
public function getTagName(): string
{
return $this->tagName;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
use Twig\Markup;
use Twig\Template;
/**
* Represents a security policy which need to be enforced when sandbox mode is enabled.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SecurityPolicy implements SecurityPolicyInterface
{
private $allowedTags;
private $allowedFilters;
private $allowedMethods;
private $allowedProperties;
private $allowedFunctions;
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
{
$this->allowedTags = $allowedTags;
$this->allowedFilters = $allowedFilters;
$this->setAllowedMethods($allowedMethods);
$this->allowedProperties = $allowedProperties;
$this->allowedFunctions = $allowedFunctions;
}
public function setAllowedTags(array $tags): void
{
$this->allowedTags = $tags;
}
public function setAllowedFilters(array $filters): void
{
$this->allowedFilters = $filters;
}
public function setAllowedMethods(array $methods): void
{
$this->allowedMethods = [];
foreach ($methods as $class => $m) {
$this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]);
}
}
public function setAllowedProperties(array $properties): void
{
$this->allowedProperties = $properties;
}
public function setAllowedFunctions(array $functions): void
{
$this->allowedFunctions = $functions;
}
public function checkSecurity($tags, $filters, $functions): void
{
foreach ($tags as $tag) {
if (!\in_array($tag, $this->allowedTags, true)) {
if ('extends' === $tag) {
trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
} elseif ('use' === $tag) {
trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
} else {
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
}
}
}
foreach ($filters as $filter) {
if (!\in_array($filter, $this->allowedFilters, true)) {
throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
}
}
foreach ($functions as $function) {
if (!\in_array($function, $this->allowedFunctions, true)) {
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
}
}
}
public function checkMethodAllowed($obj, $method): void
{
if ($obj instanceof Template || $obj instanceof Markup) {
return;
}
$allowed = false;
$method = strtolower($method);
foreach ($this->allowedMethods as $class => $methods) {
if ($obj instanceof $class && \in_array($method, $methods, true)) {
$allowed = true;
break;
}
}
if (!$allowed) {
$class = $obj::class;
throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
}
}
public function checkPropertyAllowed($obj, $property): void
{
$allowed = false;
foreach ($this->allowedProperties as $class => $properties) {
if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties], true)) {
$allowed = true;
break;
}
}
if (!$allowed) {
$class = $obj::class;
throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
}
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
/**
* Interface that all security policy classes must implements.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface SecurityPolicyInterface
{
/**
* @param string[] $tags
* @param string[] $filters
* @param string[] $functions
*
* @throws SecurityError
*/
public function checkSecurity($tags, $filters, $functions): void;
/**
* @param object $obj
* @param string $method
*
* @throws SecurityNotAllowedMethodError
*/
public function checkMethodAllowed($obj, $method): void;
/**
* @param object $obj
* @param string $property
*
* @throws SecurityNotAllowedPropertyError
*/
public function checkPropertyAllowed($obj, $property): void;
}
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
use Twig\Source;
/**
* Interface for a class that can optionally enable the sandbox mode based on a template's Twig\Source.
*
* @author Yaakov Saxon
*/
interface SourcePolicyInterface
{
public function enableSandbox(Source $source): bool;
}
+47
View File
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* Holds information about a non-compiled Twig template.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Source
{
/**
* @param string $code The template source code
* @param string $name The template logical name
* @param string $path The filesystem path of the template if any
*/
public function __construct(
private string $code,
private string $name,
private string $path = '',
) {
}
public function getCode(): string
{
return $this->code;
}
public function getName(): string
{
return $this->name;
}
public function getPath(): string
{
return $this->path;
}
}
+535
View File
@@ -0,0 +1,535 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
use Twig\Error\Error;
use Twig\Error\RuntimeError;
/**
* Default base class for compiled templates.
*
* This class is an implementation detail of how template compilation currently
* works, which might change. It should never be used directly. Use $twig->load()
* instead, which returns an instance of \Twig\TemplateWrapper.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
abstract class Template
{
public const ANY_CALL = 'any';
public const ARRAY_CALL = 'array';
public const METHOD_CALL = 'method';
protected $parent;
protected $parents = [];
protected $blocks = [];
protected $traits = [];
protected $traitAliases = [];
protected $extensions = [];
protected $sandbox;
private $useYield;
public function __construct(
protected Environment $env,
) {
$this->useYield = $env->useYield();
$this->extensions = $env->getExtensions();
}
/**
* Returns the template name.
*/
abstract public function getTemplateName(): string;
/**
* Returns debug information about the template.
*
* @return array<int, int> Debug information
*/
abstract public function getDebugInfo(): array;
/**
* Returns information about the original template source code.
*/
abstract public function getSourceContext(): Source;
/**
* Returns the parent template.
*
* This method is for internal use only and should never be called
* directly.
*
* @return self|TemplateWrapper|false The parent template or false if there is no parent
*/
public function getParent(array $context): self|TemplateWrapper|false
{
if (null !== $this->parent) {
return $this->parent;
}
if (!$parent = $this->doGetParent($context)) {
return false;
}
if ($parent instanceof self || $parent instanceof TemplateWrapper) {
return $this->parents[$parent->getSourceContext()->getName()] = $parent;
}
if (!isset($this->parents[$parent])) {
$this->parents[$parent] = $this->load($parent, -1);
}
return $this->parents[$parent];
}
protected function doGetParent(array $context): bool|string|self|TemplateWrapper
{
return false;
}
public function isTraitable(): bool
{
return true;
}
/**
* Displays a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*/
public function displayParentBlock($name, array $context, array $blocks = []): void
{
foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
echo $data;
}
}
/**
* Displays a block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display
* @param array $context The context
* @param array $blocks The current set of blocks
* @param bool $useBlocks Whether to use the current set of blocks
*/
public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void
{
foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) {
echo $data;
}
}
/**
* Renders a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to render from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return string The rendered block
*/
public function renderParentBlock($name, array $context, array $blocks = []): string
{
if (!$this->useYield) {
if ($this->env->isDebug()) {
ob_start();
} else {
ob_start(function () { return ''; });
}
$this->displayParentBlock($name, $context, $blocks);
return ob_get_clean();
}
$content = '';
foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) {
$content .= $data;
}
return $content;
}
/**
* Renders a block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to render
* @param array $context The context
* @param array $blocks The current set of blocks
* @param bool $useBlocks Whether to use the current set of blocks
*
* @return string The rendered block
*/
public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string
{
if (!$this->useYield) {
$level = ob_get_level();
if ($this->env->isDebug()) {
ob_start();
} else {
ob_start(function () { return ''; });
}
try {
$this->displayBlock($name, $context, $blocks, $useBlocks);
} catch (\Throwable $e) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $e;
}
return ob_get_clean();
}
$content = '';
foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) {
$content .= $data;
}
return $content;
}
/**
* Returns whether a block exists or not in the current context of the template.
*
* This method checks blocks defined in the current template
* or defined in "used" traits or defined in parent templates.
*
* @param string $name The block name
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return bool true if the block exists, false otherwise
*/
public function hasBlock($name, array $context, array $blocks = []): bool
{
if (isset($blocks[$name])) {
return $blocks[$name][0] instanceof self;
}
if (isset($this->blocks[$name])) {
return true;
}
if ($parent = $this->getParent($context)) {
return $parent->hasBlock($name, $context);
}
return false;
}
/**
* Returns all block names in the current context of the template.
*
* This method checks blocks defined in the current template
* or defined in "used" traits or defined in parent templates.
*
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return array<string> An array of block names
*/
public function getBlockNames(array $context, array $blocks = []): array
{
$names = array_merge(array_keys($blocks), array_keys($this->blocks));
if ($parent = $this->getParent($context)) {
$names = array_merge($names, $parent->getBlockNames($context));
}
return array_unique($names);
}
/**
* @param string|TemplateWrapper|array<string|TemplateWrapper> $template
*/
protected function load(string|TemplateWrapper|array $template, int $line, ?int $index = null): self
{
try {
if (\is_array($template)) {
return $this->env->resolveTemplate($template)->unwrap();
}
if ($template instanceof TemplateWrapper) {
return $template->unwrap();
}
if ($template === $this->getTemplateName()) {
$class = static::class;
if (false !== $pos = strrpos($class, '___', -1)) {
$class = substr($class, 0, $pos);
}
} else {
$class = $this->env->getTemplateClass($template);
}
return $this->env->loadTemplate($class, $template, $index);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($this->getSourceContext());
}
if ($e->getTemplateLine() > 0) {
throw $e;
}
if (-1 === $line) {
$e->guess();
} else {
$e->setTemplateLine($line);
}
throw $e;
}
}
/**
* @param string|TemplateWrapper|array<string|TemplateWrapper> $template
*
* @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead.
*/
protected function loadTemplate($template, $templateName = null, ?int $line = null, ?int $index = null): self|TemplateWrapper
{
trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__);
if (null === $line) {
$line = -1;
}
if ($template instanceof self) {
return $template;
}
return $this->load($template, $line, $index);
}
/**
* @internal
*
* @return $this
*/
public function unwrap(): self
{
return $this;
}
/**
* Returns all blocks.
*
* This method is for internal use only and should never be called
* directly.
*
* @return array An array of blocks
*/
public function getBlocks(): array
{
return $this->blocks;
}
public function display(array $context, array $blocks = []): void
{
foreach ($this->yield($context, $blocks) as $data) {
echo $data;
}
}
public function render(array $context): string
{
if (!$this->useYield) {
$level = ob_get_level();
if ($this->env->isDebug()) {
ob_start();
} else {
ob_start(function () { return ''; });
}
try {
$this->display($context);
} catch (\Throwable $e) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $e;
}
return ob_get_clean();
}
$content = '';
foreach ($this->yield($context) as $data) {
$content .= $data;
}
return $content;
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function yield(array $context, array $blocks = []): iterable
{
$context += $this->env->getGlobals();
$blocks = array_merge($this->blocks, $blocks);
try {
yield from $this->doDisplay($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($this->getSourceContext());
}
// this is mostly useful for \Twig\Error\LoaderError exceptions
// see \Twig\Error\LoaderError
if (-1 === $e->getTemplateLine()) {
$e->guess();
}
throw $e;
} catch (\Throwable $e) {
$e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e);
$e->guess();
throw $e;
}
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable
{
if ($useBlocks && isset($blocks[$name])) {
$template = $blocks[$name][0];
$block = $blocks[$name][1];
} elseif (isset($this->blocks[$name])) {
$template = $this->blocks[$name][0];
$block = $this->blocks[$name][1];
} else {
$template = null;
$block = null;
}
// avoid RCEs when sandbox is enabled
if (null !== $template && !$template instanceof self) {
throw new \LogicException('A block must be a method on a \Twig\Template instance.');
}
if (null !== $template) {
try {
yield from $template->$block($context, $blocks);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($template->getSourceContext());
}
// this is mostly useful for \Twig\Error\LoaderError exceptions
// see \Twig\Error\LoaderError
if (-1 === $e->getTemplateLine()) {
$e->guess();
}
throw $e;
} catch (\Throwable $e) {
$e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e);
$e->guess();
throw $e;
}
} elseif ($parent = $this->getParent($context)) {
yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
} elseif (isset($blocks[$name])) {
throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
} else {
throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
}
}
/**
* Yields a parent block.
*
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The block name to display from the parent
* @param array $context The context
* @param array $blocks The current set of blocks
*
* @return iterable<scalar|\Stringable|null>
*/
public function yieldParentBlock($name, array $context, array $blocks = []): iterable
{
if (isset($this->traits[$name])) {
yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false);
} elseif ($parent = $this->getParent($context)) {
yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false);
} else {
throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext());
}
}
protected function hasMacro(string $name, array $context): bool
{
if (method_exists($this, $name)) {
return true;
}
if (!$parent = $this->getParent($context)) {
return false;
}
return $parent->hasMacro($name, $context);
}
protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self
{
if (method_exists($this, $name)) {
return $this;
}
$parent = $this;
while ($parent = $parent->getParent($context)) {
if (method_exists($parent, $name)) {
return $parent;
}
}
throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source);
}
/**
* Auto-generated method to display the template with the given context.
*
* @param array $context An array of parameters to pass to the template
* @param array $blocks An array of blocks to pass to the template
*
* @return iterable<scalar|\Stringable|null>
*/
abstract protected function doDisplay(array $context, array $blocks = []): iterable;
}
+112
View File
@@ -0,0 +1,112 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* Exposes a template to userland.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TemplateWrapper
{
/**
* This method is for internal use only and should never be called
* directly (use Twig\Environment::load() instead).
*
* @internal
*/
public function __construct(
private Environment $env,
private Template $template,
) {
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function stream(array $context = []): iterable
{
yield from $this->template->yield($context);
}
/**
* @return iterable<scalar|\Stringable|null>
*/
public function streamBlock(string $name, array $context = []): iterable
{
yield from $this->template->yieldBlock($name, $context);
}
public function render(array $context = []): string
{
return $this->template->render($context);
}
/**
* @return void
*/
public function display(array $context = [])
{
// using func_get_args() allows to not expose the blocks argument
// as it should only be used by internal code
$this->template->display($context, \func_get_args()[1] ?? []);
}
public function hasBlock(string $name, array $context = []): bool
{
return $this->template->hasBlock($name, $context);
}
/**
* @return string[] An array of defined template block names
*/
public function getBlockNames(array $context = []): array
{
return $this->template->getBlockNames($context);
}
public function renderBlock(string $name, array $context = []): string
{
return $this->template->renderBlock($name, $context + $this->env->getGlobals());
}
/**
* @return void
*/
public function displayBlock(string $name, array $context = [])
{
$context += $this->env->getGlobals();
foreach ($this->template->yieldBlock($name, $context) as $data) {
echo $data;
}
}
public function getSourceContext(): Source
{
return $this->template->getSourceContext();
}
public function getTemplateName(): string
{
return $this->template->getTemplateName();
}
/**
* @internal
*
* @return Template
*/
public function unwrap()
{
return $this->template;
}
}
+351
View File
@@ -0,0 +1,351 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Test;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Error\Error;
use Twig\Extension\ExtensionInterface;
use Twig\Loader\ArrayLoader;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* Integration test helper.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Karma Dordrak <drak@zikula.org>
*/
abstract class IntegrationTestCase extends TestCase
{
/**
* @deprecated since Twig 3.13, use getFixturesDirectory() instead.
*
* @return string
*/
protected function getFixturesDir()
{
throw new \BadMethodCallException('Not implemented.');
}
protected static function getFixturesDirectory(): string
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* @return RuntimeLoaderInterface[]
*/
protected function getRuntimeLoaders()
{
return [];
}
/**
* @return ExtensionInterface[]
*/
protected function getExtensions()
{
return [];
}
/**
* @return TwigFilter[]
*/
protected function getTwigFilters()
{
return [];
}
/**
* @return TwigFunction[]
*/
protected function getTwigFunctions()
{
return [];
}
/**
* @return TwigTest[]
*/
protected function getTwigTests()
{
return [];
}
/**
* @return array<callable(string): (TwigFilter|false)>
*/
protected function getUndefinedFilterCallbacks(): array
{
return [];
}
/**
* @return array<callable(string): (TwigFunction|false)>
*/
protected function getUndefinedFunctionCallbacks(): array
{
return [];
}
/**
* @return array<callable(string): (TwigTest|false)>
*/
protected function getUndefinedTestCallbacks(): array
{
return [];
}
/**
* @return array<callable(string): (TokenParserInterface|false)>
*/
protected function getUndefinedTokenParserCallbacks(): array
{
return [];
}
/**
* @dataProvider getTests
*
* @return void
*/
public function testIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '')
{
$this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation);
}
/**
* @dataProvider getLegacyTests
*
* @group legacy
*
* @return void
*/
public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '')
{
$this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation);
}
/**
* @return iterable
*
* @final since Twig 3.13
*/
public function getTests($name, $legacyTests = false)
{
try {
$fixturesDir = static::getFixturesDirectory();
} catch (\BadMethodCallException) {
trigger_deprecation('twig/twig', '3.13', 'Not overriding "%s::getFixturesDirectory()" in "%s" is deprecated. This method will be abstract in 4.0.', self::class, static::class);
$fixturesDir = $this->getFixturesDir();
}
$fixturesDir = realpath($fixturesDir);
$tests = [];
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if (!preg_match('/\.test$/', $file)) {
continue;
}
if ($legacyTests xor str_contains($file->getRealpath(), '.legacy.test')) {
continue;
}
$test = file_get_contents($file->getRealpath());
if (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*(?:--DEPRECATION--\s*(.*?))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)\s*(?:--DATA--\s*(.*))?\s*--EXCEPTION--\s*(.*)/sx', $test, $match)) {
$message = $match[1];
$condition = $match[2];
$deprecation = $match[3];
$templates = self::parseTemplates($match[4]);
$exception = $match[6];
$outputs = [[null, $match[5], null, '']];
} elseif (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*(?:--DEPRECATION--\s*(.*?))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match)) {
$message = $match[1];
$condition = $match[2];
$deprecation = $match[3];
$templates = self::parseTemplates($match[4]);
$exception = false;
preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, \PREG_SET_ORDER);
} else {
throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file)));
}
$tests[str_replace($fixturesDir.'/', '', $file)] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation];
}
if ($legacyTests && !$tests) {
// add a dummy test to avoid a PHPUnit message
return [['not', '-', '', [], '', []]];
}
return $tests;
}
/**
* @final since Twig 3.13
*
* @return iterable
*/
public function getLegacyTests()
{
return $this->getTests('testLegacyIntegration', true);
}
/**
* @return void
*/
protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '')
{
if (!$outputs) {
$this->markTestSkipped('no tests to run');
}
if ($condition) {
$ret = '';
eval('$ret = '.$condition.';');
if (!$ret) {
$this->markTestSkipped($condition);
}
}
foreach ($outputs as $i => $match) {
$config = array_merge([
'cache' => false,
'strict_variables' => true,
], $match[2] ? eval($match[2].';') : []);
// make sure that template are always compiled even if they are the same (useful when testing with more than one data/expect sections)
foreach ($templates as $j => $template) {
$templates[$j] = $template.str_repeat(' ', $i);
}
$loader = new ArrayLoader($templates);
$twig = new Environment($loader, $config);
$twig->addGlobal('global', 'global');
foreach ($this->getRuntimeLoaders() as $runtimeLoader) {
$twig->addRuntimeLoader($runtimeLoader);
}
foreach ($this->getExtensions() as $extension) {
$twig->addExtension($extension);
}
foreach ($this->getTwigFilters() as $filter) {
$twig->addFilter($filter);
}
foreach ($this->getTwigTests() as $test) {
$twig->addTest($test);
}
foreach ($this->getTwigFunctions() as $function) {
$twig->addFunction($function);
}
foreach ($this->getUndefinedFilterCallbacks() as $callback) {
$twig->registerUndefinedFilterCallback($callback);
}
foreach ($this->getUndefinedFunctionCallbacks() as $callback) {
$twig->registerUndefinedFunctionCallback($callback);
}
foreach ($this->getUndefinedTestCallbacks() as $callback) {
$twig->registerUndefinedTestCallback($callback);
}
foreach ($this->getUndefinedTokenParserCallbacks() as $callback) {
$twig->registerUndefinedTokenParserCallback($callback);
}
$deprecations = [];
try {
$prevHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$prevHandler) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
return true;
}
return $prevHandler ? $prevHandler($type, $msg, $file, $line, $context) : false;
});
$template = $twig->load('index.twig');
} catch (\Exception $e) {
if (false !== $exception) {
$message = $e->getMessage();
$this->assertSame(trim($exception), trim(\sprintf('%s: %s', $e::class, $message)));
$last = substr($message, \strlen($message) - 1);
$this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.');
return;
}
throw new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e);
} finally {
restore_error_handler();
}
$this->assertSame($deprecation, implode("\n", $deprecations));
try {
$output = trim($template->render(eval($match[1].';')), "\n ");
} catch (\Exception $e) {
if (false !== $exception) {
$this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', $e::class, $e->getMessage())));
return;
}
$e = new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e);
$output = trim(\sprintf('%s: %s', $e::class, $e->getMessage()));
}
if (false !== $exception) {
[$class] = explode(':', $exception);
$constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception';
$this->assertThat(null, new $constraintClass($class));
}
$expected = trim($match[3], "\n ");
if ($expected !== $output) {
printf("Compiled templates that failed on case %d:\n", $i + 1);
foreach (array_keys($templates) as $name) {
echo "Template: $name\n";
echo $twig->compile($twig->parse($twig->tokenize($twig->getLoader()->getSourceContext($name))));
}
}
$this->assertEquals($expected, $output, $message.' (in '.$file.')');
}
}
/**
* @return array<string, string>
*/
protected static function parseTemplates($test)
{
$templates = [];
preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, \PREG_SET_ORDER);
foreach ($matches as $match) {
$templates[$match[1] ?: 'index.twig'] = $match[2];
}
return $templates;
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Test;
use PHPUnit\Framework\Attributes\BeforeClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Twig\Compiler;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Node\Node;
abstract class NodeTestCase extends TestCase
{
/**
* @var Environment
*/
private $currentEnv;
/**
* @return iterable<array{0: Node, 1: string, 2?: Environment|null, 3?: bool}>
*/
public function getTests()
{
return [];
}
/**
* @return iterable<array{0: Node, 1: string, 2?: Environment|null, 3?: bool}>
*/
public static function provideTests(): iterable
{
trigger_deprecation('twig/twig', '3.13', 'Not implementing "%s()" in "%s" is deprecated. This method will be abstract in 4.0.', __METHOD__, static::class);
return [];
}
/**
* @dataProvider getTests
* @dataProvider provideTests
*
* @return void
*/
#[DataProvider('getTests'), DataProvider('provideTests')]
public function testCompile($node, $source, $environment = null, $isPattern = false)
{
$this->assertNodeCompilation($source, $node, $environment, $isPattern);
}
/**
* @return void
*/
public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false)
{
$compiler = $this->getCompiler($environment);
$compiler->compile($node);
if ($isPattern) {
$this->assertStringMatchesFormat($source, trim($compiler->getSource()));
} else {
$this->assertEquals($source, trim($compiler->getSource()));
}
}
/**
* @return Compiler
*/
protected function getCompiler(?Environment $environment = null)
{
return new Compiler($environment ?? $this->getEnvironment());
}
/**
* @return Environment
*
* @final since Twig 3.13
*/
protected function getEnvironment()
{
return $this->currentEnv ??= static::createEnvironment();
}
protected static function createEnvironment(): Environment
{
return new Environment(new ArrayLoader());
}
/**
* @return string
*
* @deprecated since Twig 3.13, use createVariableGetter() instead.
*/
protected function getVariableGetter($name, $line = false)
{
trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createVariableGetter()" instead.', __METHOD__);
return self::createVariableGetter($name, $line);
}
final protected static function createVariableGetter(string $name, bool $line = false): string
{
$line = $line > 0 ? "// line $line\n" : '';
return \sprintf('%s($context["%s"] ?? null)', $line, $name);
}
/**
* @return string
*
* @deprecated since Twig 3.13, use createAttributeGetter() instead.
*/
protected function getAttributeGetter()
{
trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createAttributeGetter()" instead.', __METHOD__);
return self::createAttributeGetter();
}
final protected static function createAttributeGetter(): string
{
return 'CoreExtension::getAttribute($this->env, $this->source, ';
}
/** @beforeClass */
#[BeforeClass]
final public static function checkDataProvider(): void
{
$r = new \ReflectionMethod(static::class, 'getTests');
if (self::class !== $r->getDeclaringClass()->getName()) {
trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class);
}
}
}
+239
View File
@@ -0,0 +1,239 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Token
{
public const EOF_TYPE = -1;
public const TEXT_TYPE = 0;
public const BLOCK_START_TYPE = 1;
public const VAR_START_TYPE = 2;
public const BLOCK_END_TYPE = 3;
public const VAR_END_TYPE = 4;
public const NAME_TYPE = 5;
public const NUMBER_TYPE = 6;
public const STRING_TYPE = 7;
public const OPERATOR_TYPE = 8;
public const PUNCTUATION_TYPE = 9;
public const INTERPOLATION_START_TYPE = 10;
public const INTERPOLATION_END_TYPE = 11;
/**
* @deprecated since Twig 3.21, "arrow" is now an operator
*/
public const ARROW_TYPE = 12;
/**
* @deprecated since Twig 3.21, "spread" is now an operator
*/
public const SPREAD_TYPE = 13;
public function __construct(
private int $type,
private $value,
private int $lineno,
) {
if (self::ARROW_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE);
}
if (self::SPREAD_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::SPREAD_TYPE);
}
}
public function __toString(): string
{
return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value);
}
/**
* Tests the current token for a type and/or a value.
*
* Parameters may be:
* * just type
* * type and value (or array of possible values)
* * just value (or array of possible values) (NAME_TYPE is used as type)
*
* @param array|string|int $type The type to test
* @param array|string|null $values The token value
*/
public function test($type, $values = null): bool
{
if (null === $values && !\is_int($type)) {
$values = $type;
$type = self::NAME_TYPE;
}
if (self::ARROW_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE));
return self::OPERATOR_TYPE === $this->type && '=>' === $this->value;
}
if (self::SPREAD_TYPE === $type) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::typeToEnglish(self::SPREAD_TYPE));
return self::OPERATOR_TYPE === $this->type && '...' === $this->value;
}
$typeMatches = $this->type === $type;
if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:'], true) && $values) {
foreach ((array) $values as $value) {
if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) {
trigger_deprecation('twig/twig', '3.21', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish());
break;
}
}
}
if (!$typeMatches) {
if (self::OPERATOR_TYPE === $type && self::PUNCTUATION_TYPE === $this->type) {
if ($values) {
foreach ((array) $values as $value) {
if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) {
$typeMatches = true;
break;
}
}
} else {
$typeMatches = true;
}
}
}
return $typeMatches && (
null === $values
|| (\is_array($values) && \in_array($this->value, $values, true))
|| $this->value == $values
);
}
public function getLine(): int
{
return $this->lineno;
}
/**
* @deprecated since Twig 3.19
*/
public function getType(): int
{
trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated.', __METHOD__));
return $this->type;
}
public function getValue()
{
return $this->value;
}
public function toEnglish(): string
{
return self::typeToEnglish($this->type);
}
public static function typeToString(int $type, bool $short = false): string
{
switch ($type) {
case self::EOF_TYPE:
$name = 'EOF_TYPE';
break;
case self::TEXT_TYPE:
$name = 'TEXT_TYPE';
break;
case self::BLOCK_START_TYPE:
$name = 'BLOCK_START_TYPE';
break;
case self::VAR_START_TYPE:
$name = 'VAR_START_TYPE';
break;
case self::BLOCK_END_TYPE:
$name = 'BLOCK_END_TYPE';
break;
case self::VAR_END_TYPE:
$name = 'VAR_END_TYPE';
break;
case self::NAME_TYPE:
$name = 'NAME_TYPE';
break;
case self::NUMBER_TYPE:
$name = 'NUMBER_TYPE';
break;
case self::STRING_TYPE:
$name = 'STRING_TYPE';
break;
case self::OPERATOR_TYPE:
$name = 'OPERATOR_TYPE';
break;
case self::PUNCTUATION_TYPE:
$name = 'PUNCTUATION_TYPE';
break;
case self::INTERPOLATION_START_TYPE:
$name = 'INTERPOLATION_START_TYPE';
break;
case self::INTERPOLATION_END_TYPE:
$name = 'INTERPOLATION_END_TYPE';
break;
case self::ARROW_TYPE:
$name = 'ARROW_TYPE';
break;
case self::SPREAD_TYPE:
$name = 'SPREAD_TYPE';
break;
default:
throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type));
}
return $short ? $name : 'Twig\Token::'.$name;
}
public static function typeToEnglish(int $type): string
{
switch ($type) {
case self::EOF_TYPE:
return 'end of template';
case self::TEXT_TYPE:
return 'text';
case self::BLOCK_START_TYPE:
return 'begin of statement block';
case self::VAR_START_TYPE:
return 'begin of print statement';
case self::BLOCK_END_TYPE:
return 'end of statement block';
case self::VAR_END_TYPE:
return 'end of print statement';
case self::NAME_TYPE:
return 'name';
case self::NUMBER_TYPE:
return 'number';
case self::STRING_TYPE:
return 'string';
case self::OPERATOR_TYPE:
return 'operator';
case self::PUNCTUATION_TYPE:
return 'punctuation';
case self::INTERPOLATION_START_TYPE:
return 'begin of string interpolation';
case self::INTERPOLATION_END_TYPE:
return 'end of string interpolation';
case self::ARROW_TYPE:
return 'arrow function';
case self::SPREAD_TYPE:
return 'spread operator';
default:
throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type));
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Lexer;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Nodes;
use Twig\Parser;
use Twig\Token;
/**
* Base class for all token parsers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractTokenParser implements TokenParserInterface
{
/**
* @var Parser
*/
protected $parser;
public function setParser(Parser $parser): void
{
$this->parser = $parser;
}
/**
* Parses an assignment expression like "a, b".
*/
protected function parseAssignmentExpression(): Nodes
{
$stream = $this->parser->getStream();
$targets = [];
while (true) {
$token = $stream->getCurrent();
if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
// in this context, string operators are variable names
$stream->next();
} else {
$stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
}
$targets[] = new AssignContextVariable($token->getValue(), $token->getLine());
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
return new Nodes($targets);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\ExpressionParser\Infix\FilterExpressionParser;
use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Token;
/**
* Applies filters on a section of a template.
*
* {% apply upper %}
* This text becomes uppercase
* {% endapply %}
*
* @internal
*/
final class ApplyTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$ref = new LocalVariable(null, $lineno);
$filter = $ref;
$op = $this->parser->getEnvironment()->getExpressionParsers()->getByClass(FilterExpressionParser::class);
while (true) {
$filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken());
if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) {
break;
}
$this->parser->getStream()->next();
}
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideApplyEnd'], true);
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new Nodes([
new SetNode(true, $ref, $body, $lineno),
new PrintNode($filter, $lineno),
], $lineno);
}
public function decideApplyEnd(Token $token): bool
{
return $token->test('endapply');
}
public function getTag(): string
{
return 'apply';
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\AutoEscapeNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
use Twig\Token;
/**
* Marks a section of a template to be escaped or not.
*
* @internal
*/
final class AutoEscapeTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
if ($stream->test(Token::BLOCK_END_TYPE)) {
$value = 'html';
} else {
$expr = $this->parser->parseExpression();
if (!$expr instanceof ConstantExpression) {
throw new SyntaxError('An escaping strategy must be a string or false.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$value = $expr->getAttribute('value');
}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new AutoEscapeNode($value, $body, $lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endautoescape');
}
public function getTag(): string
{
return 'autoescape';
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Token;
/**
* Marks a section of a template as being reusable.
*
* {% block head %}
* <link rel="stylesheet" href="style.css" />
* <title>{% block title %}{% endblock %} - My Webpage</title>
* {% endblock %}
*
* @internal
*/
final class BlockTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno));
$this->parser->pushLocalScope();
$this->parser->pushBlockStack($name);
if ($stream->nextIf(Token::BLOCK_END_TYPE)) {
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
} else {
$body = new Nodes([
new PrintNode($this->parser->parseExpression(), $lineno),
]);
}
$stream->expect(Token::BLOCK_END_TYPE);
$block->setNode('body', $body);
$this->parser->popBlockStack();
$this->parser->popLocalScope();
return new BlockReferenceNode($name, $lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endblock');
}
public function getTag(): string
{
return 'block';
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\DeprecatedNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Deprecates a section of a template.
*
* {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %}
* {% extends 'layout.html.twig' %}
*
* {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package="foo/bar" version="1.1" %}
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
final class DeprecatedTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$expr = $this->parser->parseExpression();
$node = new DeprecatedNode($expr, $token->getLine());
while ($stream->test(Token::NAME_TYPE)) {
$k = $stream->getCurrent()->getValue();
$stream->next();
$stream->expect(Token::OPERATOR_TYPE, '=');
switch ($k) {
case 'package':
$node->setNode('package', $this->parser->parseExpression());
break;
case 'version':
$node->setNode('version', $this->parser->parseExpression());
break;
default:
throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$stream->expect(Token::BLOCK_END_TYPE);
return $node;
}
public function getTag(): string
{
return 'deprecated';
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\DoNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Evaluates an expression, discarding the returned value.
*
* @internal
*/
final class DoTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$expr = $this->parser->parseExpression();
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new DoNode($expr, $token->getLine());
}
public function getTag(): string
{
return 'do';
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\EmbedNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
use Twig\Token;
/**
* Embeds a template.
*
* @internal
*/
final class EmbedTokenParser extends IncludeTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$parent = $this->parser->parseExpression();
[$variables, $only, $ignoreMissing] = $this->parseArguments();
$parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine());
if ($parent instanceof ConstantExpression) {
$parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine());
} elseif ($parent instanceof ContextVariable) {
$parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine());
}
// inject a fake parent to make the parent() function work
$stream->injectTokens([
new Token(Token::BLOCK_START_TYPE, '', $token->getLine()),
new Token(Token::NAME_TYPE, 'extends', $token->getLine()),
$parentToken,
new Token(Token::BLOCK_END_TYPE, '', $token->getLine()),
]);
$module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true);
// override the parent with the correct one
if ($fakeParentToken === $parentToken) {
$module->setNode('parent', $parent);
}
$this->parser->embedTemplate($module);
$stream->expect(Token::BLOCK_END_TYPE);
return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine());
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endembed');
}
public function getTag(): string
{
return 'embed';
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Extends a template by another one.
*
* {% extends "base.html" %}
*
* @internal
*/
final class ExtendsTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
if ($this->parser->peekBlockStack()) {
throw new SyntaxError('Cannot use "extend" in a block.', $token->getLine(), $stream->getSourceContext());
} elseif (!$this->parser->isMainScope()) {
throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
}
$this->parser->setParent($this->parser->parseExpression());
$stream->expect(Token::BLOCK_END_TYPE);
return new EmptyNode($token->getLine());
}
public function getTag(): string
{
return 'extends';
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\FlushNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Flushes the output to the client.
*
* @see flush()
*
* @internal
*/
final class FlushTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new FlushNode($token->getLine());
}
public function getTag(): string
{
return 'flush';
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\ForElseNode;
use Twig\Node\ForNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Loops over each item of a sequence.
*
* <ul>
* {% for user in users %}
* <li>{{ user.username|e }}</li>
* {% endfor %}
* </ul>
*
* @internal
*/
final class ForTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$targets = $this->parseAssignmentExpression();
$stream->expect(Token::OPERATOR_TYPE, 'in');
$seq = $this->parser->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideForFork']);
if ('else' == $stream->next()->getValue()) {
$elseLineno = $stream->getCurrent()->getLine();
$stream->expect(Token::BLOCK_END_TYPE);
$else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $elseLineno);
} else {
$else = null;
}
$stream->expect(Token::BLOCK_END_TYPE);
if (\count($targets) > 1) {
$keyTarget = $targets->getNode('0');
$keyTarget = new AssignContextVariable($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine());
$valueTarget = $targets->getNode('1');
} else {
$keyTarget = new AssignContextVariable('_key', $lineno);
$valueTarget = $targets->getNode('0');
}
$valueTarget = new AssignContextVariable($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine());
return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno);
}
public function decideForFork(Token $token): bool
{
return $token->test(['else', 'endfor']);
}
public function decideForEnd(Token $token): bool
{
return $token->test('endfor');
}
public function getTag(): string
{
return 'for';
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\AssignTemplateVariable;
use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\ImportNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Imports macros.
*
* {% from 'forms.html.twig' import forms %}
*
* @internal
*/
final class FromTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$macro = $this->parser->parseExpression();
$stream = $this->parser->getStream();
$stream->expect(Token::NAME_TYPE, 'import');
$targets = [];
while (true) {
$name = $stream->expect(Token::NAME_TYPE)->getValue();
if ($stream->nextIf('as')) {
$alias = new AssignContextVariable($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine());
} else {
$alias = new AssignContextVariable($name, $token->getLine());
}
$targets[$name] = $alias;
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
$stream->expect(Token::BLOCK_END_TYPE);
$internalRef = new AssignTemplateVariable(new TemplateVariable(null, $token->getLine()), $this->parser->isMainScope());
$node = new ImportNode($macro, $internalRef, $token->getLine());
foreach ($targets as $name => $alias) {
$this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef);
}
return $node;
}
public function getTag(): string
{
return 'from';
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\EmptyNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Token;
/**
* @internal
*/
final class GuardTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$typeToken = $stream->expect(Token::NAME_TYPE);
if (!\in_array($typeToken->getValue(), ['function', 'filter', 'test'], true)) {
throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext());
}
$method = 'get'.$typeToken->getValue();
$nameToken = $stream->expect(Token::NAME_TYPE);
$name = $nameToken->getValue();
if ('test' === $typeToken->getValue() && $stream->test(Token::NAME_TYPE)) {
// try 2-words tests
$name .= ' '.$stream->getCurrent()->getValue();
$stream->next();
}
try {
$exists = null !== $this->parser->getEnvironment()->$method($name);
} catch (SyntaxError) {
$exists = false;
}
$stream->expect(Token::BLOCK_END_TYPE);
if ($exists) {
$body = $this->parser->subparse([$this, 'decideGuardFork']);
} else {
$body = new EmptyNode();
$this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']);
}
$else = new EmptyNode();
if ('else' === $stream->next()->getValue()) {
$stream->expect(Token::BLOCK_END_TYPE);
$else = $this->parser->subparse([$this, 'decideGuardEnd'], true);
}
$stream->expect(Token::BLOCK_END_TYPE);
return new Nodes([$exists ? $body : $else]);
}
public function decideGuardFork(Token $token): bool
{
return $token->test(['else', 'endguard']);
}
public function decideGuardEnd(Token $token): bool
{
return $token->test(['endguard']);
}
public function getTag(): string
{
return 'guard';
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\IfNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Token;
/**
* Tests a condition.
*
* {% if users %}
* <ul>
* {% for user in users %}
* <li>{{ user.username|e }}</li>
* {% endfor %}
* </ul>
* {% endif %}
*
* @internal
*/
final class IfTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$expr = $this->parser->parseExpression();
$stream = $this->parser->getStream();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfFork']);
$tests = [$expr, $body];
$else = null;
$end = false;
while (!$end) {
switch ($stream->next()->getValue()) {
case 'else':
$stream->expect(Token::BLOCK_END_TYPE);
$else = $this->parser->subparse([$this, 'decideIfEnd']);
break;
case 'elseif':
$expr = $this->parser->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideIfFork']);
$tests[] = $expr;
$tests[] = $body;
break;
case 'endif':
$end = true;
break;
default:
throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$stream->expect(Token::BLOCK_END_TYPE);
return new IfNode(new Nodes($tests), $else, $lineno);
}
public function decideIfFork(Token $token): bool
{
return $token->test(['elseif', 'else', 'endif']);
}
public function decideIfEnd(Token $token): bool
{
return $token->test(['endif']);
}
public function getTag(): string
{
return 'if';
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\Expression\Variable\AssignTemplateVariable;
use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\ImportNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Imports macros.
*
* {% import 'forms.html.twig' as forms %}
*
* @internal
*/
final class ImportTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$macro = $this->parser->parseExpression();
$this->parser->getStream()->expect(Token::NAME_TYPE, 'as');
$name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue();
$var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope());
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$this->parser->addImportedSymbol('template', $name);
return new ImportNode($macro, $var, $token->getLine());
}
public function getTag(): string
{
return 'import';
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\IncludeNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Includes a template.
*
* {% include 'header.html.twig' %}
* Body
* {% include 'footer.html.twig' %}
*
* @internal
*/
class IncludeTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$expr = $this->parser->parseExpression();
[$variables, $only, $ignoreMissing] = $this->parseArguments();
return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine());
}
/**
* @return array{0: ?AbstractExpression, 1: bool, 2: bool}
*/
protected function parseArguments()
{
$stream = $this->parser->getStream();
$ignoreMissing = false;
if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) {
$stream->expect(Token::NAME_TYPE, 'missing');
$ignoreMissing = true;
}
$variables = null;
if ($stream->nextIf(Token::NAME_TYPE, 'with')) {
$variables = $this->parser->parseExpression();
}
$only = false;
if ($stream->nextIf(Token::NAME_TYPE, 'only')) {
$only = true;
}
$stream->expect(Token::BLOCK_END_TYPE);
return [$variables, $only, $ignoreMissing];
}
public function getTag(): string
{
return 'include';
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\BodyNode;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\PosUnary;
use Twig\Node\Expression\Variable\LocalVariable;
use Twig\Node\MacroNode;
use Twig\Node\Node;
use Twig\Token;
/**
* Defines a macro.
*
* {% macro input(name, value, type, size) %}
* <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
* {% endmacro %}
*
* @internal
*/
final class MacroTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$arguments = $this->parseDefinition();
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->pushLocalScope();
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
if ($token = $stream->nextIf(Token::NAME_TYPE)) {
$value = $token->getValue();
if ($value != $name) {
throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
$this->parser->popLocalScope();
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno));
return new EmptyNode($lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endmacro');
}
public function getTag(): string
{
return 'macro';
}
private function parseDefinition(): ArrayExpression
{
$arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine());
$stream = $this->parser->getStream();
$stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
if (\count($arguments)) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
// if the comma above was a trailing comma, early exit the argument parse loop
if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {
break;
}
}
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
$name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) {
$default = $this->parser->parseExpression();
} else {
$default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
$default->setAttribute('is_implicit', true);
}
if (!$this->checkConstantExpression($default)) {
throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());
}
$arguments->addElement($default, $name);
}
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
return $arguments;
}
// checks that the node only contains "constant" elements
private function checkConstantExpression(Node $node): bool
{
if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
|| $node instanceof NegUnary || $node instanceof PosUnary
)) {
return false;
}
foreach ($node as $n) {
if (!$this->checkConstantExpression($n)) {
return false;
}
}
return true;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\IncludeNode;
use Twig\Node\Node;
use Twig\Node\SandboxNode;
use Twig\Node\TextNode;
use Twig\Token;
/**
* Marks a section of a template as untrusted code that must be evaluated in the sandbox mode.
*
* {% sandbox %}
* {% include 'user.html.twig' %}
* {% endsandbox %}
*
* @see https://twig.symfony.com/doc/api.html#sandbox-extension for details
*
* @internal
*/
final class SandboxTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
trigger_deprecation('twig/twig', '3.15', \sprintf('The "sandbox" tag is deprecated in "%s" at line %d.', $stream->getSourceContext()->getName(), $token->getLine()));
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
// in a sandbox tag, only include tags are allowed
if (!$body instanceof IncludeNode) {
foreach ($body as $node) {
if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
continue;
}
if (!$node instanceof IncludeNode) {
throw new SyntaxError('Only "include" tags are allowed within a "sandbox" section.', $node->getTemplateLine(), $stream->getSourceContext());
}
}
}
return new SandboxNode($body, $token->getLine());
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endsandbox');
}
public function getTag(): string
{
return 'sandbox';
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\SetNode;
use Twig\Token;
/**
* Defines a variable.
*
* {% set foo = 'foo' %}
* {% set foo = [1, 2] %}
* {% set foo = {'foo': 'bar'} %}
* {% set foo = 'foo' ~ 'bar' %}
* {% set foo, bar = 'foo', 'bar' %}
* {% set foo %}Some content{% endset %}
*
* @internal
*/
final class SetTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$names = $this->parseAssignmentExpression();
$capture = false;
if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) {
$values = $this->parseMultitargetExpression();
$stream->expect(Token::BLOCK_END_TYPE);
if (\count($names) !== \count($values)) {
throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
} else {
$capture = true;
if (\count($names) > 1) {
throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$stream->expect(Token::BLOCK_END_TYPE);
$values = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
}
return new SetNode($capture, $names, $values, $lineno);
}
public function decideBlockEnd(Token $token): bool
{
return $token->test('endset');
}
public function getTag(): string
{
return 'set';
}
private function parseMultitargetExpression(): Nodes
{
$targets = [];
while (true) {
$targets[] = $this->parser->parseExpression();
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
return new Nodes($targets);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\Node;
use Twig\Parser;
use Twig\Token;
/**
* Interface implemented by token parsers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TokenParserInterface
{
/**
* Sets the parser associated with this token parser.
*/
public function setParser(Parser $parser): void;
/**
* Parses a token and returns a node.
*
* @return Node
*
* @throws SyntaxError
*/
public function parse(Token $token);
/**
* Gets the tag name associated with this token parser.
*
* @return string
*/
public function getTag();
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\Node;
use Twig\Node\TypesNode;
use Twig\Token;
use Twig\TokenStream;
/**
* Declare variable types.
*
* {% types {foo: 'number', bar?: 'string'} %}
*
* @author Jeroen Versteeg <jeroen@alisqi.com>
*
* @internal
*/
final class TypesTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$types = $this->parseSimpleMappingExpression($stream);
$stream->expect(Token::BLOCK_END_TYPE);
return new TypesNode($types, $token->getLine());
}
/**
* @return array<string, array{type: string, optional: bool}>
*
* @throws SyntaxError
*/
private function parseSimpleMappingExpression(TokenStream $stream): array
{
$enclosed = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '{');
$types = [];
$first = true;
while (!($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE))) {
if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma');
// trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE)) {
break;
}
}
$first = false;
$nameToken = $stream->expect(Token::NAME_TYPE);
if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) {
$isOptional = true;
} else {
$isOptional = null !== $stream->nextIf(Token::OPERATOR_TYPE, '?');
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)');
}
$valueToken = $stream->expect(Token::STRING_TYPE);
$types[$nameToken->getValue()] = [
'type' => $valueToken->getValue(),
'optional' => $isOptional,
];
}
if ($enclosed) {
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
}
return $types;
}
public function getTag(): string
{
return 'types';
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Token;
/**
* Imports blocks defined in another template into the current template.
*
* {% extends "base.html" %}
*
* {% use "blocks.html" %}
*
* {% block title %}{% endblock %}
* {% block content %}{% endblock %}
*
* @see https://twig.symfony.com/doc/templates.html#horizontal-reuse for details.
*
* @internal
*/
final class UseTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$template = $this->parser->parseExpression();
$stream = $this->parser->getStream();
if (!$template instanceof ConstantExpression) {
throw new SyntaxError('The template references in a "use" statement must be a string.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
$targets = [];
if ($stream->nextIf('with')) {
while (true) {
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$alias = $name;
if ($stream->nextIf('as')) {
$alias = $stream->expect(Token::NAME_TYPE)->getValue();
}
$targets[$name] = new ConstantExpression($alias, -1);
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break;
}
}
}
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->addTrait(new Nodes(['template' => $template, 'targets' => new Nodes($targets)]));
return new EmptyNode($token->getLine());
}
public function getTag(): string
{
return 'use';
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\TokenParser;
use Twig\Node\Node;
use Twig\Node\WithNode;
use Twig\Token;
/**
* Creates a nested scope.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class WithTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
$variables = null;
$only = false;
if (!$stream->test(Token::BLOCK_END_TYPE)) {
$variables = $this->parser->parseExpression();
$only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only');
}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideWithEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new WithNode($body, $variables, $only, $token->getLine());
}
public function decideWithEnd(Token $token): bool
{
return $token->test('endwith');
}
public function getTag(): string
{
return 'with';
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
use Twig\Error\SyntaxError;
/**
* Represents a token stream.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TokenStream
{
private $current = 0;
public function __construct(
private array $tokens,
private ?Source $source = null,
) {
if (null === $this->source) {
trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__));
$this->source = new Source('', '');
}
}
public function __toString(): string
{
return implode("\n", $this->tokens);
}
/**
* @return void
*/
public function injectTokens(array $tokens)
{
$this->tokens = array_merge(\array_slice($this->tokens, 0, $this->current), $tokens, \array_slice($this->tokens, $this->current));
}
/**
* Sets the pointer to the next token and returns the old one.
*/
public function next(): Token
{
if (!isset($this->tokens[++$this->current])) {
throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current - 1]->getLine(), $this->source);
}
return $this->tokens[$this->current - 1];
}
/**
* Tests a token, sets the pointer to the next one and returns it or throws a syntax error.
*
* @return Token|null The next token if the condition is true, null otherwise
*/
public function nextIf($primary, $secondary = null)
{
return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null;
}
/**
* Tests a token and returns it or throws a syntax error.
*/
public function expect($type, $value = null, ?string $message = null): Token
{
$token = $this->tokens[$this->current];
if (!$token->test($type, $value)) {
$line = $token->getLine();
throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).',
$message ? $message.'. ' : '',
$token->toEnglish(),
$token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '',
Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''),
$line,
$this->source
);
}
$this->next();
return $token;
}
/**
* Looks at the next token.
*/
public function look(int $number = 1): Token
{
if (!isset($this->tokens[$this->current + $number])) {
throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current + $number - 1]->getLine(), $this->source);
}
return $this->tokens[$this->current + $number];
}
/**
* Tests the current token.
*/
public function test($primary, $secondary = null): bool
{
return $this->tokens[$this->current]->test($primary, $secondary);
}
/**
* Checks if end of stream was reached.
*/
public function isEOF(): bool
{
return $this->tokens[$this->current]->test(Token::EOF_TYPE);
}
public function getCurrent(): Token
{
return $this->tokens[$this->current];
}
public function getSourceContext(): Source
{
return $this->source;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TwigCallableInterface extends \Stringable
{
public function getName(): string;
public function getType(): string;
public function getDynamicName(): string;
/**
* @return callable|array{class-string, string}|null
*/
public function getCallable();
public function getNodeClass(): string;
public function needsCharset(): bool;
public function needsEnvironment(): bool;
public function needsContext(): bool;
public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self;
public function getArguments(): array;
public function isVariadic(): bool;
public function isDeprecated(): bool;
public function getDeprecatingPackage(): string;
public function getDeprecatedVersion(): string;
public function getAlternative(): ?string;
public function getMinimalNumberOfRequiredArguments(): int;
}
+74
View File
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Node;
/**
* Represents a template filter.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see https://twig.symfony.com/doc/templates.html#filters
*/
final class TwigFilter extends AbstractTwigCallable
{
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
parent::__construct($name, $callable, $options);
$this->options = array_merge([
'is_safe' => null,
'is_safe_callback' => null,
'pre_escape' => null,
'preserves_safety' => null,
'node_class' => FilterExpression::class,
], $this->options);
}
public function getType(): string
{
return 'filter';
}
public function getSafe(Node $filterArgs): ?array
{
if (null !== $this->options['is_safe']) {
return $this->options['is_safe'];
}
if (null !== $this->options['is_safe_callback']) {
return $this->options['is_safe_callback']($filterArgs);
}
return [];
}
public function getPreservesSafety(): array
{
return $this->options['preserves_safety'] ?? [];
}
public function getPreEscape(): ?string
{
return $this->options['pre_escape'];
}
public function getMinimalNumberOfRequiredArguments(): int
{
return parent::getMinimalNumberOfRequiredArguments() + 1;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Node;
/**
* Represents a template function.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see https://twig.symfony.com/doc/templates.html#functions
*/
final class TwigFunction extends AbstractTwigCallable
{
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
parent::__construct($name, $callable, $options);
$this->options = array_merge([
'is_safe' => null,
'is_safe_callback' => null,
'node_class' => FunctionExpression::class,
'parser_callable' => null,
], $this->options);
}
public function getType(): string
{
return 'function';
}
public function getParserCallable(): ?callable
{
return $this->options['parser_callable'];
}
public function getSafe(Node $functionArgs): ?array
{
if (null !== $this->options['is_safe']) {
return $this->options['is_safe'];
}
if (null !== $this->options['is_safe_callback']) {
return $this->options['is_safe_callback']($functionArgs);
}
return [];
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
use Twig\Node\Expression\TestExpression;
/**
* Represents a template test.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @see https://twig.symfony.com/doc/templates.html#test-operator
*/
final class TwigTest extends AbstractTwigCallable
{
/**
* @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation.
*/
public function __construct(string $name, $callable = null, array $options = [])
{
parent::__construct($name, $callable, $options);
$this->options = array_merge([
'node_class' => TestExpression::class,
'one_mandatory_argument' => false,
], $this->options);
}
public function getType(): string
{
return 'test';
}
public function needsCharset(): bool
{
return false;
}
public function needsEnvironment(): bool
{
return false;
}
public function needsContext(): bool
{
return false;
}
public function hasOneMandatoryArgument(): bool
{
return (bool) $this->options['one_mandatory_argument'];
}
public function getMinimalNumberOfRequiredArguments(): int
{
return parent::getMinimalNumberOfRequiredArguments() + 1;
}
}
+219
View File
@@ -0,0 +1,219 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Util;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\VariadicExpression;
use Twig\Node\Node;
use Twig\TwigCallableInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class CallableArgumentsExtractor
{
private ReflectionCallable $rc;
public function __construct(
private Node $node,
private TwigCallableInterface $twigCallable,
) {
$this->rc = new ReflectionCallable($twigCallable);
}
/**
* @return array<Node>
*/
public function extractArguments(Node $arguments): array
{
$extractedArguments = [];
$extractedArgumentNameMap = [];
$named = false;
foreach ($arguments as $name => $node) {
if (!\is_int($name)) {
$named = true;
} elseif ($named) {
throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
$extractedArguments[$normalizedName = $this->normalizeName($name)] = $node;
$extractedArgumentNameMap[$normalizedName] = $name;
}
if (!$named && !$this->twigCallable->isVariadic()) {
$min = $this->twigCallable->getMinimalNumberOfRequiredArguments();
if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) {
$argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName());
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
return $extractedArguments;
}
if (!$callable = $this->twigCallable->getCallable()) {
if ($named) {
throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()));
}
throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()));
}
[$callableParameters, $isPhpVariadic] = $this->getCallableParameters();
$arguments = [];
$callableParameterNames = [];
$missingArguments = [];
$optionalArguments = [];
$pos = 0;
foreach ($callableParameters as $callableParameter) {
$callableParameterName = $callableParameter->name;
if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) {
if ('start' === $callableParameterName) {
$callableParameterName = 'low';
} elseif ('end' === $callableParameterName) {
$callableParameterName = 'high';
}
}
$callableParameterNames[] = $callableParameterName;
$normalizedCallableParameterName = $this->normalizeName($callableParameterName);
if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) {
if (\array_key_exists($pos, $extractedArguments)) {
throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
if (\count($missingArguments)) {
throw new SyntaxError(\sprintf(
'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
$callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
$arguments = array_merge($arguments, $optionalArguments);
$arguments[] = $extractedArguments[$normalizedCallableParameterName];
unset($extractedArguments[$normalizedCallableParameterName]);
$optionalArguments = [];
} elseif (\array_key_exists($pos, $extractedArguments)) {
$arguments = array_merge($arguments, $optionalArguments);
$arguments[] = $extractedArguments[$pos];
unset($extractedArguments[$pos]);
$optionalArguments = [];
++$pos;
} elseif ($callableParameter->isDefaultValueAvailable()) {
$optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine());
} elseif ($callableParameter->isOptional()) {
if (!$extractedArguments) {
break;
}
$missingArguments[] = $callableParameterName;
} else {
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());
}
}
if ($this->twigCallable->isVariadic()) {
$arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine());
foreach ($extractedArguments as $key => $value) {
if (\is_int($key)) {
$arbitraryArguments->addElement($value);
} else {
$originalKey = $extractedArgumentNameMap[$key];
if ($originalKey !== $this->toSnakeCase($originalKey)) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine()));
}
$arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine()));
// I Twig 4.0, don't convert the key:
// $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine()));
}
unset($extractedArguments[$key]);
}
if ($arbitraryArguments->count()) {
$arguments = array_merge($arguments, $optionalArguments);
$arguments[] = $arbitraryArguments;
}
}
if ($extractedArguments) {
$unknownArgument = null;
foreach ($extractedArguments as $extractedArgument) {
if ($extractedArgument instanceof Node) {
$unknownArgument = $extractedArgument;
break;
}
}
throw new SyntaxError(
\sprintf(
'Unknown argument%s "%s" for %s "%s(%s)".',
\count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames))
),
$unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(),
$unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext()
);
}
return $arguments;
}
private function normalizeName(string $name): string
{
return strtolower(str_replace('_', '', $name));
}
private function toSnakeCase(string $name): string
{
return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name));
}
private function getCallableParameters(): array
{
$parameters = $this->rc->getReflector()->getParameters();
if ($this->node->hasNode('node')) {
array_shift($parameters);
}
if ($this->twigCallable->needsCharset()) {
array_shift($parameters);
}
if ($this->twigCallable->needsEnvironment()) {
array_shift($parameters);
}
if ($this->twigCallable->needsContext()) {
array_shift($parameters);
}
foreach ($this->twigCallable->getArguments() as $argument) {
array_shift($parameters);
}
$isPhpVariadic = false;
if ($this->twigCallable->isVariadic()) {
$argument = end($parameters);
$isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName();
if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
array_pop($parameters);
} elseif ($argument && $argument->isVariadic()) {
array_pop($parameters);
$isPhpVariadic = true;
} else {
throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()));
}
}
return [$parameters, $isPhpVariadic];
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Util;
use Twig\Environment;
use Twig\Error\SyntaxError;
use Twig\Source;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DeprecationCollector
{
public function __construct(
private Environment $twig,
) {
}
/**
* Returns deprecations for templates contained in a directory.
*
* @param string $dir A directory where templates are stored
* @param string $ext Limit the loaded templates by extension
*
* @return array An array of deprecations
*/
public function collectDir(string $dir, string $ext = '.twig'): array
{
$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY
), '{'.preg_quote($ext).'$}'
);
return $this->collect(new TemplateDirIterator($iterator));
}
/**
* Returns deprecations for passed templates.
*
* @param \Traversable $iterator An iterator of templates (where keys are template names and values the contents of the template)
*
* @return array An array of deprecations
*/
public function collect(\Traversable $iterator): array
{
$deprecations = [];
set_error_handler(function ($type, $msg) use (&$deprecations) {
if (\E_USER_DEPRECATED === $type) {
$deprecations[] = $msg;
}
return false;
});
foreach ($iterator as $name => $contents) {
try {
$this->twig->parse($this->twig->tokenize(new Source($contents, $name)));
} catch (SyntaxError $e) {
// ignore templates containing syntax errors
}
}
restore_error_handler();
return $deprecations;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Util;
use Twig\TwigCallableInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class ReflectionCallable
{
private $reflector;
private $callable;
private $name;
public function __construct(
TwigCallableInterface $twigCallable,
) {
$callable = $twigCallable->getCallable();
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
}
if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]);
$this->callable = $callable;
$this->name = $r->class.'::'.$r->name;
return;
}
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e);
}
$this->reflector = $r = new \ReflectionFunction($closure);
if (str_contains($r->name, '{closure')) {
$this->callable = $callable;
$this->name = 'Closure';
return;
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$this->name = get_debug_type($object).'::'.$r->name;
} elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
$callable = [$class->name, $r->name];
$this->name = $class->name.'::'.$r->name;
} elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
$callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name];
$this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else {
$callable = $this->name = $r->name;
}
if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
$callable = $r->getClosure();
}
$this->callable = $callable;
}
public function getReflector(): \ReflectionFunctionAbstract
{
return $this->reflector;
}
/**
* @return callable
*/
public function getCallable()
{
return $this->callable;
}
public function getName(): string
{
return $this->name;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Util;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class TemplateDirIterator extends \IteratorIterator
{
/**
* @return string
*/
#[\ReturnTypeWillChange]
public function current()
{
return file_get_contents(parent::current());
}
/**
* @return string
*/
#[\ReturnTypeWillChange]
public function key()
{
return (string) parent::key();
}
}
+30
View File
@@ -0,0 +1,30 @@
BSD 3-Clause License
Copyright (c) 2014, Graham Campbell.
Copyright (c) 2013, Vance Lucas.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+60
View File
@@ -0,0 +1,60 @@
{
"name": "vlucas/phpdotenv",
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": ["env", "dotenv", "environment"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"require": {
"php": "^7.2.5 || ^8.0",
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.4",
"phpoption/phpoption": "^1.9.5",
"symfony/polyfill-ctype": "^1.26",
"symfony/polyfill-mbstring": "^1.26",
"symfony/polyfill-php80": "^1.26"
},
"require-dev": {
"ext-filter": "*",
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit":"^8.5.34 || ^9.6.13 || ^10.4.2"
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Dotenv\\Tests\\": "tests/Dotenv/"
}
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist"
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace Dotenv;
use Dotenv\Exception\InvalidPathException;
use Dotenv\Loader\Loader;
use Dotenv\Loader\LoaderInterface;
use Dotenv\Parser\Parser;
use Dotenv\Parser\ParserInterface;
use Dotenv\Repository\Adapter\ArrayAdapter;
use Dotenv\Repository\Adapter\PutenvAdapter;
use Dotenv\Repository\RepositoryBuilder;
use Dotenv\Repository\RepositoryInterface;
use Dotenv\Store\StoreBuilder;
use Dotenv\Store\StoreInterface;
use Dotenv\Store\StringStore;
class Dotenv
{
/**
* The store instance.
*
* @var \Dotenv\Store\StoreInterface
*/
private $store;
/**
* The parser instance.
*
* @var \Dotenv\Parser\ParserInterface
*/
private $parser;
/**
* The loader instance.
*
* @var \Dotenv\Loader\LoaderInterface
*/
private $loader;
/**
* The repository instance.
*
* @var \Dotenv\Repository\RepositoryInterface
*/
private $repository;
/**
* Create a new dotenv instance.
*
* @param \Dotenv\Store\StoreInterface $store
* @param \Dotenv\Parser\ParserInterface $parser
* @param \Dotenv\Loader\LoaderInterface $loader
* @param \Dotenv\Repository\RepositoryInterface $repository
*
* @return void
*/
public function __construct(
StoreInterface $store,
ParserInterface $parser,
LoaderInterface $loader,
RepositoryInterface $repository
) {
$this->store = $store;
$this->parser = $parser;
$this->loader = $loader;
$this->repository = $repository;
}
/**
* Create a new dotenv instance.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames();
foreach ((array) $paths as $path) {
$builder = $builder->addPath($path);
}
foreach ((array) $names as $name) {
$builder = $builder->addName($name);
}
if ($shortCircuit) {
$builder = $builder->shortCircuit();
}
return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository);
}
/**
* Create a new mutable dotenv instance with default repository.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new mutable dotenv instance with default repository with the putenv adapter.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()
->addAdapter(PutenvAdapter::class)
->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new immutable dotenv instance with default repository.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new immutable dotenv instance with default repository with the putenv adapter.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()
->addAdapter(PutenvAdapter::class)
->immutable()
->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new dotenv instance with an array backed repository.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Parse the given content and resolve nested variables.
*
* This method behaves just like load(), only without mutating your actual
* environment. We do this by using an array backed repository.
*
* @param string $content
*
* @throws \Dotenv\Exception\InvalidFileException
*
* @return array<string, string|null>
*/
public static function parse(string $content)
{
$repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make();
$phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository);
return $phpdotenv->load();
}
/**
* Read and load environment file(s).
*
* @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException
*
* @return array<string, string|null>
*/
public function load()
{
$entries = $this->parser->parse($this->store->read());
return $this->loader->load($this->repository, $entries);
}
/**
* Read and load environment file(s), silently failing if no files can be read.
*
* @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException
*
* @return array<string, string|null>
*/
public function safeLoad()
{
try {
return $this->load();
} catch (InvalidPathException $e) {
// suppressing exception
return [];
}
}
/**
* Required ensures that the specified variables exist, and returns a new validator object.
*
* @param string|string[] $variables
*
* @return \Dotenv\Validator
*/
public function required($variables)
{
return (new Validator($this->repository, (array) $variables))->required();
}
/**
* Returns a new validator object that won't check if the specified variables exist.
*
* @param string|string[] $variables
*
* @return \Dotenv\Validator
*/
public function ifPresent($variables)
{
return new Validator($this->repository, (array) $variables);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
//
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use InvalidArgumentException;
final class InvalidEncodingException extends InvalidArgumentException implements ExceptionInterface
{
//
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use InvalidArgumentException;
final class InvalidFileException extends InvalidArgumentException implements ExceptionInterface
{
//
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use InvalidArgumentException;
final class InvalidPathException extends InvalidArgumentException implements ExceptionInterface
{
//
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use RuntimeException;
final class ValidationException extends RuntimeException implements ExceptionInterface
{
//
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Dotenv\Loader;
use Dotenv\Parser\Entry;
use Dotenv\Parser\Value;
use Dotenv\Repository\RepositoryInterface;
final class Loader implements LoaderInterface
{
/**
* Load the given entries into the repository.
*
* We'll substitute any nested variables, and send each variable to the
* repository, with the effect of actually mutating the environment.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param \Dotenv\Parser\Entry[] $entries
*
* @return array<string, string|null>
*/
public function load(RepositoryInterface $repository, array $entries)
{
/** @var array<string, string|null> */
return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) {
$name = $entry->getName();
$value = $entry->getValue()->map(static function (Value $value) use ($repository) {
return Resolver::resolve($repository, $value);
});
if ($value->isDefined()) {
$inner = $value->get();
if ($repository->set($name, $inner)) {
return \array_merge($vars, [$name => $inner]);
}
} else {
if ($repository->clear($name)) {
return \array_merge($vars, [$name => null]);
}
}
return $vars;
}, []);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Dotenv\Loader;
use Dotenv\Repository\RepositoryInterface;
interface LoaderInterface
{
/**
* Load the given entries into the repository.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param \Dotenv\Parser\Entry[] $entries
*
* @return array<string, string|null>
*/
public function load(RepositoryInterface $repository, array $entries);
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Dotenv\Loader;
use Dotenv\Parser\Value;
use Dotenv\Repository\RepositoryInterface;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
use PhpOption\Option;
final class Resolver
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Resolve the nested variables in the given value.
*
* Replaces ${varname} patterns in the allowed positions in the variable
* value by an existing environment variable.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param \Dotenv\Parser\Value $value
*
* @return string
*/
public static function resolve(RepositoryInterface $repository, Value $value)
{
return \array_reduce($value->getVars(), static function (string $s, int $i) use ($repository) {
return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i));
}, $value->getChars());
}
/**
* Resolve a single nested variable.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param string $str
*
* @return string
*/
private static function resolveVariable(RepositoryInterface $repository, string $str)
{
return Regex::replaceCallback(
'/\A\${([a-zA-Z0-9_.]+)}/',
static function (array $matches) use ($repository) {
/** @var string */
return Option::fromValue($repository->get($matches[1]))->getOrElse($matches[0]);
},
$str,
1
)->success()->getOrElse($str);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use PhpOption\Option;
final class Entry
{
/**
* The entry name.
*
* @var string
*/
private $name;
/**
* The entry value.
*
* @var \Dotenv\Parser\Value|null
*/
private $value;
/**
* Create a new entry instance.
*
* @param string $name
* @param \Dotenv\Parser\Value|null $value
*
* @return void
*/
public function __construct(string $name, ?Value $value = null)
{
$this->name = $name;
$this->value = $value;
}
/**
* Get the entry name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get the entry value.
*
* @return \PhpOption\Option<\Dotenv\Parser\Value>
*/
public function getValue()
{
/** @var \PhpOption\Option<\Dotenv\Parser\Value> */
return Option::fromValue($this->value);
}
}
+299
View File
@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
use GrahamCampbell\ResultType\Error;
use GrahamCampbell\ResultType\Result;
use GrahamCampbell\ResultType\Success;
final class EntryParser
{
private const INITIAL_STATE = 0;
private const UNQUOTED_STATE = 1;
private const SINGLE_QUOTED_STATE = 2;
private const DOUBLE_QUOTED_STATE = 3;
private const ESCAPE_SEQUENCE_STATE = 4;
private const WHITESPACE_STATE = 5;
private const COMMENT_STATE = 6;
private const REJECT_STATES = [self::SINGLE_QUOTED_STATE, self::DOUBLE_QUOTED_STATE, self::ESCAPE_SEQUENCE_STATE];
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Parse a raw entry into a proper entry.
*
* That is, turn a raw environment variable entry into a name and possibly
* a value. We wrap the answer in a result type.
*
* @param string $entry
*
* @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry, string>
*/
public static function parse(string $entry)
{
return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) {
[$name, $value] = $parts;
return self::parseName($name)->flatMap(static function (string $name) use ($value) {
/** @var Result<Value|null, string> */
$parsedValue = $value === null ? Success::create(null) : self::parseValue($value);
return $parsedValue->map(static function (?Value $value) use ($name) {
return new Entry($name, $value);
});
});
});
}
/**
* Split the compound string into parts.
*
* @param string $line
*
* @return \GrahamCampbell\ResultType\Result<array{string, string|null},string>
*/
private static function splitStringIntoParts(string $line)
{
/** @var array{string, string|null} */
$result = Str::pos($line, '=')->map(static function () use ($line) {
return \array_map('trim', \explode('=', $line, 2));
})->getOrElse([$line, null]);
if ($result[0] === '') {
/** @var \GrahamCampbell\ResultType\Result<array{string, string|null},string> */
return Error::create(self::getErrorMessage('an unexpected equals', $line));
}
/** @var \GrahamCampbell\ResultType\Result<array{string, string|null},string> */
return Success::create($result);
}
/**
* Parse the given variable name.
*
* That is, strip the optional quotes and leading "export" from the
* variable name. We wrap the answer in a result type.
*
* @param string $name
*
* @return \GrahamCampbell\ResultType\Result<string, string>
*/
private static function parseName(string $name)
{
if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) {
$name = \ltrim(Str::substr($name, 6));
}
if (self::isQuotedName($name)) {
$name = Str::substr($name, 1, -1);
}
if (!self::isValidName($name)) {
/** @var \GrahamCampbell\ResultType\Result<string, string> */
return Error::create(self::getErrorMessage('an invalid name', $name));
}
/** @var \GrahamCampbell\ResultType\Result<string, string> */
return Success::create($name);
}
/**
* Is the given variable name quoted?
*
* @param string $name
*
* @return bool
*/
private static function isQuotedName(string $name)
{
if (Str::len($name) < 3) {
return false;
}
$first = Str::substr($name, 0, 1);
$last = Str::substr($name, -1, 1);
return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\'');
}
/**
* Is the given variable name valid?
*
* @param string $name
*
* @return bool
*/
private static function isValidName(string $name)
{
return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false);
}
/**
* Parse the given variable value.
*
* This has the effect of stripping quotes and comments, dealing with
* special characters, and locating nested variables, but not resolving
* them. Formally, we run a finite state automaton with an output tape: a
* transducer. We wrap the answer in a result type.
*
* @param string $value
*
* @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string>
*/
private static function parseValue(string $value)
{
if (\trim($value) === '') {
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */
return Success::create(Value::blank());
}
return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) {
return $data->flatMap(static function (array $data) use ($token) {
return self::processToken($data[1], $token)->map(static function (array $val) use ($data) {
return [$data[0]->append($val[0], $val[1]), $val[2]];
});
});
}, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) {
if (in_array($result[1], self::REJECT_STATES, true)) {
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */
return Error::create('a missing closing quote');
}
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */
return Success::create($result[0]);
})->mapError(static function (string $err) use ($value) {
return self::getErrorMessage($err, $value);
});
}
/**
* Process the given token.
*
* @param int $state
* @param string $token
*
* @return \GrahamCampbell\ResultType\Result<array{string, bool, int}, string>
*/
private static function processToken(int $state, string $token)
{
switch ($state) {
case self::INITIAL_STATE:
if ($token === '\'') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::SINGLE_QUOTED_STATE]);
} elseif ($token === '"') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::DOUBLE_QUOTED_STATE]);
} elseif ($token === '#') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::COMMENT_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, true, self::UNQUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, false, self::UNQUOTED_STATE]);
}
case self::UNQUOTED_STATE:
if ($token === '#') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::COMMENT_STATE]);
} elseif (\ctype_space($token)) {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, true, self::UNQUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, false, self::UNQUOTED_STATE]);
}
case self::SINGLE_QUOTED_STATE:
if ($token === '\'') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, false, self::SINGLE_QUOTED_STATE]);
}
case self::DOUBLE_QUOTED_STATE:
if ($token === '"') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
} elseif ($token === '\\') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
}
case self::ESCAPE_SEQUENCE_STATE:
if ($token === '"' || $token === '\\') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
} else {
$first = Str::substr($token, 0, 1);
if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Error::create('an unexpected escape sequence');
}
}
case self::WHITESPACE_STATE:
if ($token === '#') {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::COMMENT_STATE]);
} elseif (!\ctype_space($token)) {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Error::create('unexpected whitespace');
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
}
case self::COMMENT_STATE:
/** @var \GrahamCampbell\ResultType\Result<array{string, bool, int}, string> */
return Success::create(['', false, self::COMMENT_STATE]);
default:
throw new \Error('Parser entered invalid state.');
}
}
/**
* Generate a friendly error message.
*
* @param string $cause
* @param string $subject
*
* @return string
*/
private static function getErrorMessage(string $cause, string $subject)
{
return \sprintf(
'Encountered %s at [%s].',
$cause,
\strtok($subject, "\n")
);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
final class Lexer
{
/**
* The regex for each type of token.
*/
private const PATTERNS = [
'[\r\n]{1,1000}', '[^\S\r\n]{1,1000}', '\\\\', '\'', '"', '\\#', '\\$', '([^(\s\\\\\'"\\#\\$)]|\\(|\\)){1,1000}',
];
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Convert content into a token stream.
*
* Multibyte string processing is not needed here, and nether is error
* handling, for performance reasons.
*
* @param string $content
*
* @return \Generator<string>
*/
public static function lex(string $content)
{
static $regex;
if ($regex === null) {
$regex = '(('.\implode(')|(', self::PATTERNS).'))A';
}
$offset = 0;
while (isset($content[$offset])) {
if (!\preg_match($regex, $content, $matches, 0, $offset)) {
throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset]));
}
$offset += \strlen($matches[0]);
yield $matches[0];
}
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
final class Lines
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Process the array of lines of environment variables.
*
* This will produce an array of raw entries, one per variable.
*
* @param string[] $lines
*
* @return string[]
*/
public static function process(array $lines)
{
$output = [];
$multiline = false;
$multilineBuffer = [];
foreach ($lines as $line) {
[$multiline, $line, $multilineBuffer] = self::multilineProcess($multiline, $line, $multilineBuffer);
if (!$multiline && !self::isCommentOrWhitespace($line)) {
$output[] = $line;
}
}
return $output;
}
/**
* Used to make all multiline variable process.
*
* @param bool $multiline
* @param string $line
* @param string[] $buffer
*
* @return array{bool,string, string[]}
*/
private static function multilineProcess(bool $multiline, string $line, array $buffer)
{
$startsOnCurrentLine = $multiline ? false : self::looksLikeMultilineStart($line);
// check if $line can be multiline variable
if ($startsOnCurrentLine) {
$multiline = true;
}
if ($multiline) {
\array_push($buffer, $line);
if (self::looksLikeMultilineStop($line, $startsOnCurrentLine)) {
$multiline = false;
$line = \implode("\n", $buffer);
$buffer = [];
}
}
return [$multiline, $line, $buffer];
}
/**
* Determine if the given line can be the start of a multiline variable.
*
* @param string $line
*
* @return bool
*/
private static function looksLikeMultilineStart(string $line)
{
return Str::pos($line, '="')->map(static function () use ($line) {
return self::looksLikeMultilineStop($line, true) === false;
})->getOrElse(false);
}
/**
* Determine if the given line can be the start of a multiline variable.
*
* @param string $line
* @param bool $started
*
* @return bool
*/
private static function looksLikeMultilineStop(string $line, bool $started)
{
if ($line === '"') {
return true;
}
return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) {
return $started ? $count > 1 : $count >= 1;
})->success()->getOrElse(false);
}
/**
* Determine if the line in the file is a comment or whitespace.
*
* @param string $line
*
* @return bool
*/
private static function isCommentOrWhitespace(string $line)
{
$line = \trim($line);
return $line === '' || (isset($line[0]) && $line[0] === '#');
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Exception\InvalidFileException;
use Dotenv\Util\Regex;
use GrahamCampbell\ResultType\Result;
use GrahamCampbell\ResultType\Success;
final class Parser implements ParserInterface
{
/**
* Parse content into an entry array.
*
* @param string $content
*
* @throws \Dotenv\Exception\InvalidFileException
*
* @return \Dotenv\Parser\Entry[]
*/
public function parse(string $content)
{
return Regex::split("/(\r\n|\n|\r)/", $content)->mapError(static function () {
return 'Could not split into separate lines.';
})->flatMap(static function (array $lines) {
return self::process(Lines::process($lines));
})->mapError(static function (string $error) {
throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error));
})->success()->get();
}
/**
* Convert the raw entries into proper entries.
*
* @param string[] $entries
*
* @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string>
*/
private static function process(array $entries)
{
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> */
return \array_reduce($entries, static function (Result $result, string $raw) {
return $result->flatMap(static function (array $entries) use ($raw) {
return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) {
/** @var \Dotenv\Parser\Entry[] */
return \array_merge($entries, [$entry]);
});
});
}, Success::create([]));
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
interface ParserInterface
{
/**
* Parse content into an entry array.
*
* @param string $content
*
* @throws \Dotenv\Exception\InvalidFileException
*
* @return \Dotenv\Parser\Entry[]
*/
public function parse(string $content);
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Util\Str;
final class Value
{
/**
* The string representation of the parsed value.
*
* @var string
*/
private $chars;
/**
* The locations of the variables in the value.
*
* @var int[]
*/
private $vars;
/**
* Internal constructor for a value.
*
* @param string $chars
* @param int[] $vars
*
* @return void
*/
private function __construct(string $chars, array $vars)
{
$this->chars = $chars;
$this->vars = $vars;
}
/**
* Create an empty value instance.
*
* @return \Dotenv\Parser\Value
*/
public static function blank()
{
return new self('', []);
}
/**
* Create a new value instance, appending the characters.
*
* @param string $chars
* @param bool $var
*
* @return \Dotenv\Parser\Value
*/
public function append(string $chars, bool $var)
{
return new self(
$this->chars.$chars,
$var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars
);
}
/**
* Get the string representation of the parsed value.
*
* @return string
*/
public function getChars()
{
return $this->chars;
}
/**
* Get the locations of the variables in the value.
*
* @return int[]
*/
public function getVars()
{
$vars = $this->vars;
\rsort($vars);
return $vars;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
interface AdapterInterface extends ReaderInterface, WriterInterface
{
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create();
}
+89
View File
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\None;
use PhpOption\Option;
use PhpOption\Some;
final class ApacheAdapter implements AdapterInterface
{
/**
* Create a new apache adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
if (self::isSupported()) {
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
return None::create();
}
/**
* Determines if the adapter is supported.
*
* This happens if PHP is running as an Apache module.
*
* @return bool
*/
private static function isSupported()
{
return \function_exists('apache_getenv') && \function_exists('apache_setenv');
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromValue(apache_getenv($name))->filter(static function ($value) {
return \is_string($value) && $value !== '';
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
return apache_setenv($name, $value);
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
return apache_setenv($name, '');
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\Option;
use PhpOption\Some;
final class ArrayAdapter implements AdapterInterface
{
/**
* The variables and their values.
*
* @var array<string, string>
*/
private $variables;
/**
* Create a new array adapter instance.
*
* @return void
*/
private function __construct()
{
$this->variables = [];
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
return Option::fromArraysValue($this->variables, $name);
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
$this->variables[$name] = $value;
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
unset($this->variables[$name]);
return true;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\Option;
use PhpOption\Some;
final class EnvConstAdapter implements AdapterInterface
{
/**
* Create a new env const adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromArraysValue($_ENV, $name)
->filter(static function ($value) {
return \is_scalar($value);
})
->map(static function ($value) {
if ($value === false) {
return 'false';
}
if ($value === true) {
return 'true';
}
return (string) $value;
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
$_ENV[$name] = $value;
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
unset($_ENV[$name]);
return true;
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class GuardedWriter implements WriterInterface
{
/**
* The inner writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* The variable name allow list.
*
* @var string[]
*/
private $allowList;
/**
* Create a new guarded writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
* @param string[] $allowList
*
* @return void
*/
public function __construct(WriterInterface $writer, array $allowList)
{
$this->writer = $writer;
$this->allowList = $allowList;
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
// Don't set non-allowed variables
if (!$this->isAllowed($name)) {
return false;
}
// Set the value on the inner writer
return $this->writer->write($name, $value);
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
// Don't clear non-allowed variables
if (!$this->isAllowed($name)) {
return false;
}
// Set the value on the inner writer
return $this->writer->delete($name);
}
/**
* Determine if the given variable is allowed.
*
* @param non-empty-string $name
*
* @return bool
*/
private function isAllowed(string $name)
{
return \in_array($name, $this->allowList, true);
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class ImmutableWriter implements WriterInterface
{
/**
* The inner writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* The inner reader to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface
*/
private $reader;
/**
* The record of loaded variables.
*
* @var array<string, string>
*/
private $loaded;
/**
* Create a new immutable writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
* @param \Dotenv\Repository\Adapter\ReaderInterface $reader
*
* @return void
*/
public function __construct(WriterInterface $writer, ReaderInterface $reader)
{
$this->writer = $writer;
$this->reader = $reader;
$this->loaded = [];
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
// Don't overwrite existing environment variables
// Ruby's dotenv does this with `ENV[key] ||= value`
if ($this->isExternallyDefined($name)) {
return false;
}
// Set the value on the inner writer
if (!$this->writer->write($name, $value)) {
return false;
}
// Record that we have loaded the variable
$this->loaded[$name] = '';
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
// Don't clear existing environment variables
if ($this->isExternallyDefined($name)) {
return false;
}
// Clear the value on the inner writer
if (!$this->writer->delete($name)) {
return false;
}
// Leave the variable as fair game
unset($this->loaded[$name]);
return true;
}
/**
* Determine if the given variable is externally defined.
*
* That is, is it an "existing" variable.
*
* @param non-empty-string $name
*
* @return bool
*/
private function isExternallyDefined(string $name)
{
return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\None;
final class MultiReader implements ReaderInterface
{
/**
* The set of readers to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface[]
*/
private $readers;
/**
* Create a new multi-reader instance.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface[] $readers
*
* @return void
*/
public function __construct(array $readers)
{
$this->readers = $readers;
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
foreach ($this->readers as $reader) {
$result = $reader->read($name);
if ($result->isDefined()) {
return $result;
}
}
return None::create();
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class MultiWriter implements WriterInterface
{
/**
* The set of writers to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface[]
*/
private $writers;
/**
* Create a new multi-writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface[] $writers
*
* @return void
*/
public function __construct(array $writers)
{
$this->writers = $writers;
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
foreach ($this->writers as $writers) {
if (!$writers->write($name, $value)) {
return false;
}
}
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
foreach ($this->writers as $writers) {
if (!$writers->delete($name)) {
return false;
}
}
return true;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\None;
use PhpOption\Option;
use PhpOption\Some;
final class PutenvAdapter implements AdapterInterface
{
/**
* Create a new putenv adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
if (self::isSupported()) {
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
return None::create();
}
/**
* Determines if the adapter is supported.
*
* @return bool
*/
private static function isSupported()
{
return \function_exists('getenv') && \function_exists('putenv');
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromValue(\getenv($name), false)->filter(static function ($value) {
return \is_string($value);
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
\putenv("$name=$value");
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
\putenv($name);
return true;
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
interface ReaderInterface
{
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name);
}
+104
View File
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class ReplacingWriter implements WriterInterface
{
/**
* The inner writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* The inner reader to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface
*/
private $reader;
/**
* The record of seen variables.
*
* @var array<string, string>
*/
private $seen;
/**
* Create a new replacement writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
* @param \Dotenv\Repository\Adapter\ReaderInterface $reader
*
* @return void
*/
public function __construct(WriterInterface $writer, ReaderInterface $reader)
{
$this->writer = $writer;
$this->reader = $reader;
$this->seen = [];
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
if ($this->exists($name)) {
return $this->writer->write($name, $value);
}
// succeed if nothing to do
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
if ($this->exists($name)) {
return $this->writer->delete($name);
}
// succeed if nothing to do
return true;
}
/**
* Does the given environment variable exist.
*
* Returns true if it currently exists, or existed at any point in the past
* that we are aware of.
*
* @param non-empty-string $name
*
* @return bool
*/
private function exists(string $name)
{
if (isset($this->seen[$name])) {
return true;
}
if ($this->reader->read($name)->isDefined()) {
$this->seen[$name] = '';
return true;
}
return false;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\Option;
use PhpOption\Some;
final class ServerConstAdapter implements AdapterInterface
{
/**
* Create a new server const adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromArraysValue($_SERVER, $name)
->filter(static function ($value) {
return \is_scalar($value);
})
->map(static function ($value) {
if ($value === false) {
return 'false';
}
if ($value === true) {
return 'true';
}
return (string) $value;
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
$_SERVER[$name] = $value;
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
unset($_SERVER[$name]);
return true;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
interface WriterInterface
{
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value);
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name);
}
+107
View File
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository;
use Dotenv\Repository\Adapter\ReaderInterface;
use Dotenv\Repository\Adapter\WriterInterface;
use InvalidArgumentException;
final class AdapterRepository implements RepositoryInterface
{
/**
* The reader to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface
*/
private $reader;
/**
* The writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* Create a new adapter repository instance.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface $reader
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
*
* @return void
*/
public function __construct(ReaderInterface $reader, WriterInterface $writer)
{
$this->reader = $reader;
$this->writer = $writer;
}
/**
* Determine if the given environment variable is defined.
*
* @param string $name
*
* @return bool
*/
public function has(string $name)
{
return '' !== $name && $this->reader->read($name)->isDefined();
}
/**
* Get an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return string|null
*/
public function get(string $name)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->reader->read($name)->getOrElse(null);
}
/**
* Set an environment variable.
*
* @param string $name
* @param string $value
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function set(string $name, string $value)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->writer->write($name, $value);
}
/**
* Clear an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function clear(string $name)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->writer->delete($name);
}
}
+272
View File
@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository;
use Dotenv\Repository\Adapter\AdapterInterface;
use Dotenv\Repository\Adapter\EnvConstAdapter;
use Dotenv\Repository\Adapter\GuardedWriter;
use Dotenv\Repository\Adapter\ImmutableWriter;
use Dotenv\Repository\Adapter\MultiReader;
use Dotenv\Repository\Adapter\MultiWriter;
use Dotenv\Repository\Adapter\ReaderInterface;
use Dotenv\Repository\Adapter\ServerConstAdapter;
use Dotenv\Repository\Adapter\WriterInterface;
use InvalidArgumentException;
use PhpOption\Some;
use ReflectionClass;
final class RepositoryBuilder
{
/**
* The set of default adapters.
*/
private const DEFAULT_ADAPTERS = [
ServerConstAdapter::class,
EnvConstAdapter::class,
];
/**
* The set of readers to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface[]
*/
private $readers;
/**
* The set of writers to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface[]
*/
private $writers;
/**
* Are we immutable?
*
* @var bool
*/
private $immutable;
/**
* The variable name allow list.
*
* @var string[]|null
*/
private $allowList;
/**
* Create a new repository builder instance.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface[] $readers
* @param \Dotenv\Repository\Adapter\WriterInterface[] $writers
* @param bool $immutable
* @param string[]|null $allowList
*
* @return void
*/
private function __construct(array $readers = [], array $writers = [], bool $immutable = false, ?array $allowList = null)
{
$this->readers = $readers;
$this->writers = $writers;
$this->immutable = $immutable;
$this->allowList = $allowList;
}
/**
* Create a new repository builder instance with no adapters added.
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public static function createWithNoAdapters()
{
return new self();
}
/**
* Create a new repository builder instance with the default adapters added.
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public static function createWithDefaultAdapters()
{
$adapters = \iterator_to_array(self::defaultAdapters());
return new self($adapters, $adapters);
}
/**
* Return the array of default adapters.
*
* @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface>
*/
private static function defaultAdapters()
{
foreach (self::DEFAULT_ADAPTERS as $adapter) {
$instance = $adapter::create();
if ($instance->isDefined()) {
yield $instance->get();
}
}
}
/**
* Determine if the given name if of an adapterclass.
*
* @param string $name
*
* @return bool
*/
private static function isAnAdapterClass(string $name)
{
if (!\class_exists($name)) {
return false;
}
return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class);
}
/**
* Creates a repository builder with the given reader added.
*
* Accepts either a reader instance, or a class-string for an adapter. If
* the adapter is not supported, then we silently skip adding it.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader
*
* @throws \InvalidArgumentException
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function addReader($reader)
{
if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) {
throw new InvalidArgumentException(
\sprintf(
'Expected either an instance of %s or a class-string implementing %s',
ReaderInterface::class,
AdapterInterface::class
)
);
}
$optional = Some::create($reader)->flatMap(static function ($reader) {
return \is_string($reader) ? $reader::create() : Some::create($reader);
});
$readers = \array_merge($this->readers, \iterator_to_array($optional));
return new self($readers, $this->writers, $this->immutable, $this->allowList);
}
/**
* Creates a repository builder with the given writer added.
*
* Accepts either a writer instance, or a class-string for an adapter. If
* the adapter is not supported, then we silently skip adding it.
*
* @param \Dotenv\Repository\Adapter\WriterInterface|string $writer
*
* @throws \InvalidArgumentException
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function addWriter($writer)
{
if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) {
throw new InvalidArgumentException(
\sprintf(
'Expected either an instance of %s or a class-string implementing %s',
WriterInterface::class,
AdapterInterface::class
)
);
}
$optional = Some::create($writer)->flatMap(static function ($writer) {
return \is_string($writer) ? $writer::create() : Some::create($writer);
});
$writers = \array_merge($this->writers, \iterator_to_array($optional));
return new self($this->readers, $writers, $this->immutable, $this->allowList);
}
/**
* Creates a repository builder with the given adapter added.
*
* Accepts either an adapter instance, or a class-string for an adapter. If
* the adapter is not supported, then we silently skip adding it. We will
* add the adapter as both a reader and a writer.
*
* @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter
*
* @throws \InvalidArgumentException
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function addAdapter($adapter)
{
if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) {
throw new InvalidArgumentException(
\sprintf(
'Expected either an instance of %s or a class-string implementing %s',
WriterInterface::class,
AdapterInterface::class
)
);
}
$optional = Some::create($adapter)->flatMap(static function ($adapter) {
return \is_string($adapter) ? $adapter::create() : Some::create($adapter);
});
$readers = \array_merge($this->readers, \iterator_to_array($optional));
$writers = \array_merge($this->writers, \iterator_to_array($optional));
return new self($readers, $writers, $this->immutable, $this->allowList);
}
/**
* Creates a repository builder with mutability enabled.
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function immutable()
{
return new self($this->readers, $this->writers, true, $this->allowList);
}
/**
* Creates a repository builder with the given allow list.
*
* @param string[]|null $allowList
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function allowList(?array $allowList = null)
{
return new self($this->readers, $this->writers, $this->immutable, $allowList);
}
/**
* Creates a new repository instance.
*
* @return \Dotenv\Repository\RepositoryInterface
*/
public function make()
{
$reader = new MultiReader($this->readers);
$writer = new MultiWriter($this->writers);
if ($this->immutable) {
$writer = new ImmutableWriter($writer, $reader);
}
if ($this->allowList !== null) {
$writer = new GuardedWriter($writer, $this->allowList);
}
return new AdapterRepository($reader, $writer);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository;
interface RepositoryInterface
{
/**
* Determine if the given environment variable is defined.
*
* @param string $name
*
* @return bool
*/
public function has(string $name);
/**
* Get an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return string|null
*/
public function get(string $name);
/**
* Set an environment variable.
*
* @param string $name
* @param string $value
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function set(string $name, string $value);
/**
* Clear an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function clear(string $name);
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store\File;
/**
* @internal
*/
final class Paths
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Returns the full paths to the files.
*
* @param string[] $paths
* @param string[] $names
*
* @return string[]
*/
public static function filePaths(array $paths, array $names)
{
$files = [];
foreach ($paths as $path) {
foreach ($names as $name) {
$files[] = \rtrim($path, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.$name;
}
}
return $files;
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store\File;
use Dotenv\Exception\InvalidEncodingException;
use Dotenv\Util\Str;
use PhpOption\Option;
/**
* @internal
*/
final class Reader
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Read the file(s), and return their raw content.
*
* We provide the file path as the key, and its content as the value. If
* short circuit mode is enabled, then the returned array with have length
* at most one. File paths that couldn't be read are omitted entirely.
*
* @param string[] $filePaths
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @throws \Dotenv\Exception\InvalidEncodingException
*
* @return array<string, string>
*/
public static function read(array $filePaths, bool $shortCircuit = true, ?string $fileEncoding = null)
{
$output = [];
foreach ($filePaths as $filePath) {
$content = self::readFromFile($filePath, $fileEncoding);
if ($content->isDefined()) {
$output[$filePath] = $content->get();
if ($shortCircuit) {
break;
}
}
}
return $output;
}
/**
* Read the given file.
*
* @param string $path
* @param string|null $encoding
*
* @throws \Dotenv\Exception\InvalidEncodingException
*
* @return \PhpOption\Option<string>
*/
private static function readFromFile(string $path, ?string $encoding = null)
{
/** @var Option<string> */
$content = Option::fromValue(@\file_get_contents($path), false);
return $content->flatMap(static function (string $content) use ($encoding) {
return Str::utf8($content, $encoding)->mapError(static function (string $error) {
throw new InvalidEncodingException($error);
})->success();
});
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
use Dotenv\Exception\InvalidPathException;
use Dotenv\Store\File\Reader;
final class FileStore implements StoreInterface
{
/**
* The file paths.
*
* @var string[]
*/
private $filePaths;
/**
* Should file loading short circuit?
*
* @var bool
*/
private $shortCircuit;
/**
* The file encoding.
*
* @var string|null
*/
private $fileEncoding;
/**
* Create a new file store instance.
*
* @param string[] $filePaths
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return void
*/
public function __construct(array $filePaths, bool $shortCircuit, ?string $fileEncoding = null)
{
$this->filePaths = $filePaths;
$this->shortCircuit = $shortCircuit;
$this->fileEncoding = $fileEncoding;
}
/**
* Read the content of the environment file(s).
*
* @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException
*
* @return string
*/
public function read()
{
if ($this->filePaths === []) {
throw new InvalidPathException('At least one environment file path must be provided.');
}
$contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding);
if (\count($contents) > 0) {
return \implode("\n", $contents);
}
throw new InvalidPathException(
\sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths))
);
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
use Dotenv\Store\File\Paths;
final class StoreBuilder
{
/**
* The of default name.
*/
private const DEFAULT_NAME = '.env';
/**
* The paths to search within.
*
* @var string[]
*/
private $paths;
/**
* The file names to search for.
*
* @var string[]
*/
private $names;
/**
* Should file loading short circuit?
*
* @var bool
*/
private $shortCircuit;
/**
* The file encoding.
*
* @var string|null
*/
private $fileEncoding;
/**
* Create a new store builder instance.
*
* @param string[] $paths
* @param string[] $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return void
*/
private function __construct(array $paths = [], array $names = [], bool $shortCircuit = false, ?string $fileEncoding = null)
{
$this->paths = $paths;
$this->names = $names;
$this->shortCircuit = $shortCircuit;
$this->fileEncoding = $fileEncoding;
}
/**
* Create a new store builder instance with no names.
*
* @return \Dotenv\Store\StoreBuilder
*/
public static function createWithNoNames()
{
return new self();
}
/**
* Create a new store builder instance with the default name.
*
* @return \Dotenv\Store\StoreBuilder
*/
public static function createWithDefaultName()
{
return new self([], [self::DEFAULT_NAME]);
}
/**
* Creates a store builder with the given path added.
*
* @param string $path
*
* @return \Dotenv\Store\StoreBuilder
*/
public function addPath(string $path)
{
return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding);
}
/**
* Creates a store builder with the given name added.
*
* @param string $name
*
* @return \Dotenv\Store\StoreBuilder
*/
public function addName(string $name)
{
return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding);
}
/**
* Creates a store builder with short circuit mode enabled.
*
* @return \Dotenv\Store\StoreBuilder
*/
public function shortCircuit()
{
return new self($this->paths, $this->names, true, $this->fileEncoding);
}
/**
* Creates a store builder with the specified file encoding.
*
* @param string|null $fileEncoding
*
* @return \Dotenv\Store\StoreBuilder
*/
public function fileEncoding(?string $fileEncoding = null)
{
return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding);
}
/**
* Creates a new store instance.
*
* @return \Dotenv\Store\StoreInterface
*/
public function make()
{
return new FileStore(
Paths::filePaths($this->paths, $this->names),
$this->shortCircuit,
$this->fileEncoding
);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
interface StoreInterface
{
/**
* Read the content of the environment file(s).
*
* @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException
*
* @return string
*/
public function read();
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
final class StringStore implements StoreInterface
{
/**
* The file content.
*
* @var string
*/
private $content;
/**
* Create a new string store instance.
*
* @param string $content
*
* @return void
*/
public function __construct(string $content)
{
$this->content = $content;
}
/**
* Read the content of the environment file(s).
*
* @return string
*/
public function read()
{
return $this->content;
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Dotenv\Util;
use GrahamCampbell\ResultType\Error;
use GrahamCampbell\ResultType\Success;
/**
* @internal
*/
final class Regex
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Perform a preg match, wrapping up the result.
*
* @param string $pattern
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<bool, string>
*/
public static function matches(string $pattern, string $subject)
{
return self::pregAndWrap(static function (string $subject) use ($pattern) {
return @\preg_match($pattern, $subject) === 1;
}, $subject);
}
/**
* Perform a preg match all, wrapping up the result.
*
* @param string $pattern
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<int, string>
*/
public static function occurrences(string $pattern, string $subject)
{
return self::pregAndWrap(static function (string $subject) use ($pattern) {
return (int) @\preg_match_all($pattern, $subject);
}, $subject);
}
/**
* Perform a preg replace callback, wrapping up the result.
*
* @param string $pattern
* @param callable(string[]): string $callback
* @param string $subject
* @param int|null $limit
*
* @return \GrahamCampbell\ResultType\Result<string, string>
*/
public static function replaceCallback(string $pattern, callable $callback, string $subject, ?int $limit = null)
{
return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) {
return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1);
}, $subject);
}
/**
* Perform a preg split, wrapping up the result.
*
* @param string $pattern
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<string[], string>
*/
public static function split(string $pattern, string $subject)
{
return self::pregAndWrap(static function (string $subject) use ($pattern) {
/** @var string[] */
return (array) @\preg_split($pattern, $subject);
}, $subject);
}
/**
* Perform a preg operation, wrapping up the result.
*
* @template V
*
* @param callable(string): V $operation
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<V, string>
*/
private static function pregAndWrap(callable $operation, string $subject)
{
$result = $operation($subject);
if (\preg_last_error() !== \PREG_NO_ERROR) {
/** @var \GrahamCampbell\ResultType\Result<V,string> */
return Error::create(\preg_last_error_msg());
}
/** @var \GrahamCampbell\ResultType\Result<V,string> */
return Success::create($result);
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Dotenv\Util;
use GrahamCampbell\ResultType\Error;
use GrahamCampbell\ResultType\Success;
use PhpOption\Option;
/**
* @internal
*/
final class Str
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Convert a string to UTF-8 from the given encoding.
*
* @param string $input
* @param string|null $encoding
*
* @return \GrahamCampbell\ResultType\Result<string, string>
*/
public static function utf8(string $input, ?string $encoding = null)
{
if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) {
/** @var \GrahamCampbell\ResultType\Result<string, string> */
return Error::create(
\sprintf('Illegal character encoding [%s] specified.', $encoding)
);
}
$converted = $encoding === null ?
@\mb_convert_encoding($input, 'UTF-8') :
@\mb_convert_encoding($input, 'UTF-8', $encoding);
if (!is_string($converted)) {
/** @var \GrahamCampbell\ResultType\Result<string, string> */
return Error::create(
\sprintf('Conversion from encoding [%s] failed.', $encoding ?? 'NULL')
);
}
/**
* this is for support UTF-8 with BOM encoding
* @see https://en.wikipedia.org/wiki/Byte_order_mark
* @see https://github.com/vlucas/phpdotenv/issues/500
*/
if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") {
$converted = \substr($converted, 3);
}
/** @var \GrahamCampbell\ResultType\Result<string, string> */
return Success::create($converted);
}
/**
* Search for a given substring of the input.
*
* @param string $haystack
* @param string $needle
*
* @return \PhpOption\Option<int>
*/
public static function pos(string $haystack, string $needle)
{
/** @var \PhpOption\Option<int> */
return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false);
}
/**
* Grab the specified substring of the input.
*
* @param string $input
* @param int $start
* @param int|null $length
*
* @return string
*/
public static function substr(string $input, int $start, ?int $length = null)
{
return \mb_substr($input, $start, $length, 'UTF-8');
}
/**
* Compute the length of the given string.
*
* @param string $input
*
* @return int
*/
public static function len(string $input)
{
return \mb_strlen($input, 'UTF-8');
}
}
+207
View File
@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace Dotenv;
use Dotenv\Exception\ValidationException;
use Dotenv\Repository\RepositoryInterface;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
class Validator
{
/**
* The environment repository instance.
*
* @var \Dotenv\Repository\RepositoryInterface
*/
private $repository;
/**
* The variables to validate.
*
* @var string[]
*/
private $variables;
/**
* Create a new validator instance.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param string[] $variables
*
* @return void
*/
public function __construct(RepositoryInterface $repository, array $variables)
{
$this->repository = $repository;
$this->variables = $variables;
}
/**
* Assert that each variable is present.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function required()
{
return $this->assert(
static function (?string $value) {
return $value !== null;
},
'is missing'
);
}
/**
* Assert that each variable is not empty.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function notEmpty()
{
return $this->assertNullable(
static function (string $value) {
return Str::len(\trim($value)) > 0;
},
'is empty'
);
}
/**
* Assert that each specified variable is an integer.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function isInteger()
{
return $this->assertNullable(
static function (string $value) {
return \ctype_digit($value);
},
'is not an integer'
);
}
/**
* Assert that each specified variable is a boolean.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function isBoolean()
{
return $this->assertNullable(
static function (string $value) {
if ($value === '') {
return false;
}
return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null;
},
'is not a boolean'
);
}
/**
* Assert that each variable is amongst the given choices.
*
* @param string[] $choices
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function allowedValues(array $choices)
{
return $this->assertNullable(
static function (string $value) use ($choices) {
return \in_array($value, $choices, true);
},
\sprintf('is not one of [%s]', \implode(', ', $choices))
);
}
/**
* Assert that each variable matches the given regular expression.
*
* @param string $regex
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function allowedRegexValues(string $regex)
{
return $this->assertNullable(
static function (string $value) use ($regex) {
return Regex::matches($regex, $value)->success()->getOrElse(false);
},
\sprintf('does not match "%s"', $regex)
);
}
/**
* Assert that the callback returns true for each variable.
*
* @param callable(?string):bool $callback
* @param string $message
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function assert(callable $callback, string $message)
{
$failing = [];
foreach ($this->variables as $variable) {
if ($callback($this->repository->get($variable)) === false) {
$failing[] = \sprintf('%s %s', $variable, $message);
}
}
if (\count($failing) > 0) {
throw new ValidationException(\sprintf(
'One or more environment variables failed assertions: %s.',
\implode(', ', $failing)
));
}
return $this;
}
/**
* Assert that the callback returns true for each variable.
*
* Skip checking null variable values.
*
* @param callable(string):bool $callback
* @param string $message
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function assertNullable(callable $callback, string $message)
{
return $this->assert(
static function (?string $value) use ($callback) {
if ($value === null) {
return true;
}
return $callback($value);
},
$message
);
}
}
+4
View File
@@ -0,0 +1,4 @@
version = 1
[[analyzers]]
name = "php"
+210
View File
@@ -0,0 +1,210 @@
# Changelog
### 2.0.3 (2024-11-21)
- use modern phpdocs e.g. list<int> or conditional-return annotations
### 2.0.2 (2024-11-21)
- small fix for PHP 8.4 (thanks to @gilbertoalbino)
### 2.0.1 (2022-03-08)
- "To people of Russia": There is a war in Ukraine right now. The forces of the Russian Federation are attacking civilians.
- optimize some phpdocs
### 2.0.0 (2022-01-24)
- prefer "Russian - Passport (2013), ICAO" instead of "Russian - GOST 7.79-2000(B)"
- fix "Ukrainian" char-mapping (thanks to @Andr1yk0)
- fix "Persian" char-mapping (thanks to @frost-cyber)
### 1.6.1 (2022-01-24)
- revert: prefer "Russian - Passport (2013), ICAO" instead of "Russian - GOST 7.79-2000(B)"
- revert: fix "Ukrainian" char-mapping (thanks to @Andr1yk0)
- revert: fix "Persian" char-mapping (thanks to @frost-cyber)
### 1.6.0 (2022-01-24)
- prefer "Russian - Passport (2013), ICAO" instead of "Russian - GOST 7.79-2000(B)"
- fix "Ukrainian" char-mapping (thanks to @Andr1yk0)
- fix "Persian" char-mapping (thanks to @frost-cyber)
- fix "ASCII::normalize_whitespace()" -> "CARRIAGE RETURN" is more like "<br>" and no "\n"
- add "ASCII::to_ascii_remap()" -> this method will return broken characters and is only for special cases
### 1.5.6 (2020-11-12)
- "ASCII::normalize_whitespace()" -> can now also remove "control characters" if needed v2
### 1.5.5 (2020-11-12)
- fix "Greeklish" char-mapping (thanks @sebdesign)
- "ASCII::normalize_whitespace()" -> can now also remove "control characters" if needed
### 1.5.4 (2020-11-08)
- add some missing replacements in U+23xx page (thanks @marcoffee)
- fix "Russian" char-mapping (thanks @ilyahoilik)
- running test with PHP 8.0 rc3
### 1.5.3 (2020-07-23)
- fix "Georgian" char-mapping (thanks @waska14)
### 1.5.2 (2020-06-16)
- add "Bengali" (bn) language support (thanks @eliyas5044)
- fix "Portuguese" char-mapping
- reduce the file size (removed extra comments from "avian2/unidecode")
### 1.5.1 (2020-05-26)
- fix merge ASCII transliterations from "avian2/unidecode" (python)
-> https://github.com/avian2/unidecode/
### 1.5.0 (2020-05-24)
- merge ASCII transliterations from "avian2/unidecode" (python)
-> https://github.com/avian2/unidecode/
### 1.4.11 (2020-05-23)
- "composer.json" -> remove "autoload-dev" stuff from "autoload"
- "voku/php-readme-helper" -> auto-generate the API documentation in the README
### 1.4.10 (2020-03-13)
- ASCII::to_ascii() -> fix extra symbol handling in the regex
- ASCII::to_ascii() -> fix for languages with multi-length-special-char (e.g. Greek -> 'ει' => 'i')
### 1.4.9 (2020-03-06)
- ASCII::to_slugify() -> fix php warning from empty "separator"
### 1.4.8 (2020-02-06)
- small optimization for "ASCII::to_ascii()" performance
### 1.4.7 (2020-01-27)
- fix possible wrong type from "getDataIfExists()" -> e.g. a bug reported where "/data/" was modified
- inline variables
- do not use "=== true" for "bool"-types
### 1.4.6 (2019-12-23)
- optimize "ASCII::to_ascii()" performance
- add "armenian" chars
- add "ASCII:getAllLanguages()"
### 1.4.5 (2019-12-19)
- use "@psalm-pure" v2
### 1.4.4 (2019-12-19)
- use "@psalm-pure"
### 1.4.3 (2019-12-19)
- use "@psalm-immutable"
### 1.4.2 (2019-12-13)
- optimize the performance v2
- more fixes for non-ascii regex
### 1.4.1 (2019-12-13)
- fix regex for non-ascii
### 1.4.0 (2019-12-13)
- optimize the performance, via single char replacements
### 1.3.6 (2019-12-13)
- "ascii_extras" -> convert the static content into ascii
-> e.g.: instead of replacing "+" with "più" we use "piu" (Italian), because we want to use ascii anyway
### 1.3.5 (2019-11-11)
- fix "ASCII::remove_invisible_characters()" -> do not remove invisible encoded url strings by default
### 1.3.4 (2019-10-14)
- fix static cache for "ASCII::charsArrayWithOneLanguage"
### 1.3.3 (2019-10-14)
- fix "Turkish" mapping -> 'ä' -> 'a'
### 1.3.2 (2019-10-14)
- fix language parameter usage with e.g. "de_DE"
- re-add missing "extra"-mapping chars
### 1.3.1 (2019-10-13)
- fix "ASCII::to_slugify" -> remove unicode chars
- add more test for ascii chars in the mapping
- fix non ascii chars in the mapping
### 1.3.0 (2019-10-12)
- add transliteration "fr" (was supported before, but with chars from other languages)
- add transliteration "ru" - Passport (2013), ICAO
- add transliteration "ru" - GOST 7.79-2000(B)
- add transliteration "el" - greeklish
- add transliteration "zh"
- add transliteration "nl"
- add transliteration "it"
- add transliteration "mk"
- add transliteration "pt"
- add constants -> ASCII::*LANGUAGE_CODES
- add more special latin chars / (currency) symbols
- add simple tests for all supported languages
- optimize "Russian" to ASCII (via "translit.ru")
- optimize performance of string replacement
- optimize performance of array merging
- optimize phpdoc comments
- "ASCII::to_transliterate" -> use "transliterator_create" + static cache
- "ASCII::to_ascii" -> fix "remove unsupported chars"
- "ASCII::to_ascii" -> add some more special chars
- run/fix static analyse via "pslam" + "phpstan"
- auto fix code style via "php-cs-fixer"
- fix transliteration for "german"
- fix transliteration for "persian" (thanks @mardep)
- fix transliteration for "polish" (thanks @dariusz.drobisz)
- fix transliteration for "bulgarian" (thanks @mkosturkov)
- fix transliteration for "croatian" (thanks @ludifonovac)
- fix transliteration for "serbian" (thanks @ludifonovac)
- fix transliteration for "swedish" (thanks @nicholasruunu)
- fix transliteration for "france" (thanks @sharptsa)
- fix transliteration for "serbian" (thanks @nikolaposa)
- fix transliteration for "czech" (thanks @slepic)
### 1.2.3 (2019-09-10)
- fix language depending ASCII chars (the order matters)
### 1.2.2 (2019-09-10)
- fix bulgarian ASCII chars | thanks @bgphp
### 1.2.1 (2019-09-07)
- "charsArray()" -> add access to "ASCII::$ASCII_MAPS*""
### 1.2.0 (2019-09-07)
- "to_slugify()" -> use the extra ascii array
### 1.1.0 (2019-09-07)
- add + split extra ascii replacements
### 1.0.0 (2019-09-05)
- initial commit

Some files were not shown because too many files have changed in this diff Show More