rerer
This commit is contained in:
+194
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 '�';
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 => '"', /* quotation mark */
|
||||
38 => '&', /* ampersand */
|
||||
60 => '<', /* less-than sign */
|
||||
62 => '>', /* 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dotenv\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionInterface extends Throwable
|
||||
{
|
||||
//
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dotenv\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidEncodingException extends InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
//
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dotenv\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidFileException extends InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
//
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dotenv\Exception;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidPathException extends InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
//
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dotenv\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ValidationException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
//
|
||||
}
|
||||
+48
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "php"
|
||||
+210
@@ -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
Reference in New Issue
Block a user