initial commit

This commit is contained in:
boris
2025-09-30 09:24:25 +01:00
committed by boris
parent a783a12c97
commit c7770ea03b
4695 changed files with 525784 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Asset\Packages;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Twig extension for the Symfony Asset component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class AssetExtension extends AbstractExtension
{
public function __construct(
private Packages $packages,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('asset', $this->getAssetUrl(...)),
new TwigFunction('asset_version', $this->getAssetVersion(...)),
];
}
/**
* Returns the public url/path of an asset.
*
* If the package used to generate the path is an instance of
* UrlPackage, you will always get a URL and not a path.
*/
public function getAssetUrl(string $path, ?string $packageName = null): string
{
return $this->packages->getUrl($path, $packageName);
}
/**
* Returns the version of an asset.
*/
public function getAssetVersion(string $path, ?string $packageName = null): string
{
return $this->packages->getVersion($path, $packageName);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
final class CsrfExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('csrf_token', [CsrfRuntime::class, 'getCsrfToken']),
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
final class CsrfRuntime
{
public function __construct(
private CsrfTokenManagerInterface $csrfTokenManager,
) {
}
public function getCsrfToken(string $tokenId): string
{
return $this->csrfTokenManager->getToken($tokenId)->getValue();
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\DumpTokenParser;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\Template;
use Twig\TwigFunction;
/**
* Provides integration of the dump() function with Twig.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class DumpExtension extends AbstractExtension
{
public function __construct(
private ClonerInterface $cloner,
private ?HtmlDumper $dumper = null,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('dump', $this->dump(...), ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true]),
];
}
public function getTokenParsers(): array
{
return [new DumpTokenParser()];
}
public function dump(Environment $env, array $context): ?string
{
if (!$env->isDebug()) {
return null;
}
if (2 === \func_num_args()) {
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof Template) {
$vars[$key] = $value;
}
}
$vars = [$vars];
} else {
$vars = \func_get_args();
unset($vars[0], $vars[1]);
}
$dump = fopen('php://memory', 'r+');
$this->dumper ??= new HtmlDumper();
$this->dumper->setCharset($env->getCharset());
foreach ($vars as $value) {
$this->dumper->dump($this->cloner->cloneVar($value), $dump);
}
return stream_get_contents($dump, -1, 0);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Emoji\EmojiTransliterator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
final class EmojiExtension extends AbstractExtension
{
private static array $transliterators = [];
public function __construct(
private readonly string $defaultCatalog = 'text',
) {
if (!class_exists(EmojiTransliterator::class)) {
throw new \LogicException('You cannot use the "emojify" filter as the "Emoji" component is not installed. Try running "composer require symfony/emoji".');
}
}
public function getFilters(): array
{
return [
new TwigFilter('emojify', $this->emojify(...)),
];
}
/**
* Converts emoji short code (:wave:) to real emoji (👋).
*/
public function emojify(string $string, ?string $catalog = null): string
{
$catalog ??= $this->defaultCatalog;
try {
$tr = self::$transliterators[$catalog] ??= EmojiTransliterator::create($catalog, EmojiTransliterator::REVERSE);
} catch (\IntlException $e) {
throw new \LogicException(\sprintf('The emoji catalog "%s" is not available.', $catalog), previous: $e);
}
return (string) $tr->transliterate($string);
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\ExpressionLanguage\Expression;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* ExpressionExtension gives a way to create Expressions from a template.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class ExpressionExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('expression', $this->createExpression(...)),
];
}
public function createExpression(string $expression): Expression
{
return new Expression($expression);
}
}

View File

@@ -0,0 +1,213 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\Node\RenderBlockNode;
use Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode;
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\FormView;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/**
* FormExtension extends Twig with form capabilities.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
final class FormExtension extends AbstractExtension
{
public function __construct(
private ?TranslatorInterface $translator = null,
) {
}
public function getTokenParsers(): array
{
return [
// {% form_theme form "SomeBundle::widgets.twig" %}
new FormThemeTokenParser(),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('form_widget', null, ['node_class' => SearchAndRenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_errors', null, ['node_class' => SearchAndRenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_label', null, ['node_class' => SearchAndRenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_help', null, ['node_class' => SearchAndRenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_row', null, ['node_class' => SearchAndRenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_rest', null, ['node_class' => SearchAndRenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form', null, ['node_class' => RenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_start', null, ['node_class' => RenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('form_end', null, ['node_class' => RenderBlockNode::class, 'is_safe' => ['html']]),
new TwigFunction('csrf_token', [FormRenderer::class, 'renderCsrfToken']),
new TwigFunction('form_parent', 'Symfony\Bridge\Twig\Extension\twig_get_form_parent'),
new TwigFunction('field_name', $this->getFieldName(...)),
new TwigFunction('field_id', $this->getFieldId(...)),
new TwigFunction('field_value', $this->getFieldValue(...)),
new TwigFunction('field_label', $this->getFieldLabel(...)),
new TwigFunction('field_help', $this->getFieldHelp(...)),
new TwigFunction('field_errors', $this->getFieldErrors(...)),
new TwigFunction('field_choices', $this->getFieldChoices(...)),
];
}
public function getFilters(): array
{
return [
new TwigFilter('humanize', [FormRenderer::class, 'humanize']),
new TwigFilter('form_encode_currency', [FormRenderer::class, 'encodeCurrency'], ['is_safe' => ['html'], 'needs_environment' => true]),
];
}
public function getTests(): array
{
return [
new TwigTest('selectedchoice', 'Symfony\Bridge\Twig\Extension\twig_is_selected_choice'),
new TwigTest('rootform', 'Symfony\Bridge\Twig\Extension\twig_is_root_form'),
];
}
public function getFieldName(FormView $view): string
{
$view->setRendered();
return $view->vars['full_name'];
}
public function getFieldId(FormView $view): string
{
return $view->vars['id'];
}
public function getFieldValue(FormView $view): string|array
{
return $view->vars['value'];
}
public function getFieldLabel(FormView $view): ?string
{
if (false === $label = $view->vars['label']) {
return null;
}
if (!$label && $labelFormat = $view->vars['label_format']) {
$label = str_replace(['%id%', '%name%'], [$view->vars['id'], $view->vars['name']], $labelFormat);
} elseif (!$label) {
$label = ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $view->vars['name']))));
}
return $this->createFieldTranslation(
$label,
$view->vars['label_translation_parameters'] ?: [],
$view->vars['translation_domain']
);
}
public function getFieldHelp(FormView $view): ?string
{
return $this->createFieldTranslation(
$view->vars['help'],
$view->vars['help_translation_parameters'] ?: [],
$view->vars['translation_domain']
);
}
/**
* @return string[]
*/
public function getFieldErrors(FormView $view): iterable
{
/** @var FormError $error */
foreach ($view->vars['errors'] as $error) {
yield $error->getMessage();
}
}
/**
* @return string[]|string[][]
*/
public function getFieldChoices(FormView $view): iterable
{
yield from $this->createFieldChoicesList($view->vars['choices'], $view->vars['choice_translation_domain']);
}
private function createFieldChoicesList(iterable $choices, string|false|null $translationDomain): iterable
{
foreach ($choices as $choice) {
if ($choice instanceof ChoiceGroupView) {
$translatableLabel = $this->createFieldTranslation($choice->label, [], $translationDomain);
yield $translatableLabel => $this->createFieldChoicesList($choice, $translationDomain);
continue;
}
/** @var ChoiceView $choice */
$translatableLabel = $this->createFieldTranslation($choice->label, $choice->labelTranslationParameters, $translationDomain);
yield $translatableLabel => $choice->value;
}
}
private function createFieldTranslation(TranslatableInterface|string|null $value, array $parameters, string|false|null $domain): ?string
{
if (!$this->translator || !$value || false === $domain) {
return null !== $value ? (string) $value : null;
}
if ($value instanceof TranslatableInterface) {
return $value->trans($this->translator);
}
return $this->translator->trans($value, $parameters, $domain);
}
}
/**
* Returns whether a choice is selected for a given form value.
*
* This is a function and not callable due to performance reasons.
*
* @see ChoiceView::isSelected()
*/
function twig_is_selected_choice(ChoiceView $choice, string|array|null $selectedValue): bool
{
if (\is_array($selectedValue)) {
return \in_array($choice->value, $selectedValue, true);
}
return $choice->value === $selectedValue;
}
/**
* @internal
*/
function twig_is_root_form(FormView $formView): bool
{
return null === $formView->parent;
}
/**
* @internal
*/
function twig_get_form_parent(FormView $formView): ?FormView
{
return $formView->parent;
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Psr\Container\ContainerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
final class HtmlSanitizerExtension extends AbstractExtension
{
public function __construct(
private ContainerInterface $sanitizers,
private string $defaultSanitizer = 'default',
) {
}
public function getFilters(): array
{
return [
new TwigFilter('sanitize_html', $this->sanitize(...), ['is_safe' => ['html']]),
];
}
public function sanitize(string $html, ?string $sanitizer = null): string
{
return $this->sanitizers->get($sanitizer ?? $this->defaultSanitizer)->sanitize($html);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\UrlHelper;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Twig extension for the Symfony HttpFoundation component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class HttpFoundationExtension extends AbstractExtension
{
public function __construct(
private UrlHelper $urlHelper,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('absolute_url', $this->generateAbsoluteUrl(...)),
new TwigFunction('relative_path', $this->generateRelativePath(...)),
];
}
/**
* Returns the absolute URL for the given absolute or relative path.
*
* This method returns the path unchanged if no request is available.
*
* @see Request::getUriForPath()
*/
public function generateAbsoluteUrl(string $path): string
{
return $this->urlHelper->getAbsoluteUrl($path);
}
/**
* Returns a relative path based on the current Request.
*
* This method returns the path unchanged if no request is available.
*
* @see Request::getRelativeUriForPath()
*/
public function generateRelativePath(string $path): string
{
return $this->urlHelper->getRelativePath($path);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Provides integration with the HttpKernel component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class HttpKernelExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('render', [HttpKernelRuntime::class, 'renderFragment'], ['is_safe' => ['html']]),
new TwigFunction('render_*', [HttpKernelRuntime::class, 'renderFragmentStrategy'], ['is_safe' => ['html']]),
new TwigFunction('fragment_uri', [HttpKernelRuntime::class, 'generateFragmentUri']),
new TwigFunction('controller', [self::class, 'controller']),
];
}
public static function controller(string $controller, array $attributes = [], array $query = []): ControllerReference
{
return new ControllerReference($controller, $attributes, $query);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
/**
* Provides integration with the HttpKernel component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class HttpKernelRuntime
{
public function __construct(
private FragmentHandler $handler,
private ?FragmentUriGeneratorInterface $fragmentUriGenerator = null,
) {
}
/**
* Renders a fragment.
*
* @see FragmentHandler::render()
*/
public function renderFragment(string|ControllerReference $uri, array $options = []): string
{
$strategy = $options['strategy'] ?? 'inline';
unset($options['strategy']);
return $this->handler->render($uri, $strategy, $options);
}
/**
* Renders a fragment.
*
* @see FragmentHandler::render()
*/
public function renderFragmentStrategy(string $strategy, string|ControllerReference $uri, array $options = []): string
{
return $this->handler->render($uri, $strategy, $options);
}
public function generateFragmentUri(ControllerReference $controller, bool $absolute = false, bool $strict = true, bool $sign = true): string
{
if (null === $this->fragmentUriGenerator) {
throw new \LogicException(\sprintf('An instance of "%s" must be provided to use "%s()".', FragmentUriGeneratorInterface::class, __METHOD__));
}
return $this->fragmentUriGenerator->generate($controller, null, $absolute, $strict, $sign);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* @author Kévin Dunglas <kevin@dunglas.dev>
*/
final class ImportMapExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('importmap', [ImportMapRuntime::class, 'importmap'], ['is_safe' => ['html']]),
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
/**
* @author Kévin Dunglas <kevin@dunglas.dev>
*/
class ImportMapRuntime
{
public function __construct(
private readonly ImportMapRenderer $importMapRenderer,
) {
}
public function importmap(string|array $entryPoint = 'app', array $attributes = []): string
{
return $this->importMapRenderer->render($entryPoint, $attributes);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* LogoutUrlHelper provides generator functions for the logout URL to Twig.
*
* @author Jeremy Mikola <jmikola@gmail.com>
*/
final class LogoutUrlExtension extends AbstractExtension
{
public function __construct(
private LogoutUrlGenerator $generator,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('logout_url', $this->getLogoutUrl(...)),
new TwigFunction('logout_path', $this->getLogoutPath(...)),
];
}
/**
* Generates the relative logout URL for the firewall.
*
* @param string|null $key The firewall key or null to use the current firewall key
*/
public function getLogoutPath(?string $key = null): string
{
return $this->generator->getLogoutPath($key);
}
/**
* Generates the absolute logout URL for the firewall.
*
* @param string|null $key The firewall key or null to use the current firewall key
*/
public function getLogoutUrl(?string $key = null): string
{
return $this->generator->getLogoutUrl($key);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Stopwatch\StopwatchEvent;
use Twig\Extension\ProfilerExtension as BaseProfilerExtension;
use Twig\Profiler\Profile;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class ProfilerExtension extends BaseProfilerExtension
{
/**
* @var \SplObjectStorage<Profile, StopwatchEvent>
*/
private \SplObjectStorage $events;
public function __construct(
Profile $profile,
private ?Stopwatch $stopwatch = null,
) {
parent::__construct($profile);
$this->events = new \SplObjectStorage();
}
public function enter(Profile $profile): void
{
if ($this->stopwatch && $profile->isTemplate()) {
$this->events[$profile] = $this->stopwatch->start($profile->getName(), 'template');
}
parent::enter($profile);
}
public function leave(Profile $profile): void
{
parent::leave($profile);
if ($this->stopwatch && $profile->isTemplate()) {
$this->events[$profile]->stop();
unset($this->events[$profile]);
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\AbstractExtension;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
use Twig\TwigFunction;
/**
* Provides integration of the Routing component with Twig.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class RoutingExtension extends AbstractExtension
{
public function __construct(
private UrlGeneratorInterface $generator,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('url', $this->getUrl(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
new TwigFunction('path', $this->getPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
];
}
public function getPath(string $name, array $parameters = [], bool $relative = false): string
{
return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
}
public function getUrl(string $name, array $parameters = [], bool $schemeRelative = false): string
{
return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
}
/**
* Determines at compile time whether the generated URL will be safe and thus
* saving the unneeded automatic escaping for performance reasons.
*
* The URL generation process percent encodes non-alphanumeric characters. So there is no risk
* that malicious/invalid characters are part of the URL. The only character within a URL that
* must be escaped in html is the ampersand ("&") which separates query params. So we cannot mark
* the URL generation as always safe, but only when we are sure there won't be multiple query
* params. This is the case when there are none or only one constant parameter given.
* E.g. we know beforehand this will be safe:
* - path('route')
* - path('route', {'param': 'value'})
* But the following may not:
* - path('route', var)
* - path('route', {'param': ['val1', 'val2'] }) // a sub-array
* - path('route', {'param1': 'value1', 'param2': 'value2'})
* If param1 and param2 reference placeholder in the route, it would still be safe. But we don't know.
*
* @param Node $argsNode The arguments of the path/url function
*
* @return array An array with the contexts the URL is safe
*/
public function isUrlGenerationSafe(Node $argsNode): array
{
// support named arguments
$paramsNode = $argsNode->hasNode('parameters') ? $argsNode->getNode('parameters') : (
$argsNode->hasNode(1) ? $argsNode->getNode(1) : null
);
if (null === $paramsNode || $paramsNode instanceof ArrayExpression && \count($paramsNode) <= 2
&& (!$paramsNode->hasNode(1) || $paramsNode->getNode(1) instanceof ConstantExpression)
) {
return ['html'];
}
return [];
}
}

View File

@@ -0,0 +1,135 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* SecurityExtension exposes security context features.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SecurityExtension extends AbstractExtension
{
public function __construct(
private ?AuthorizationCheckerInterface $securityChecker = null,
private ?ImpersonateUrlGenerator $impersonateUrlGenerator = null,
) {
}
public function isGranted(mixed $role, mixed $object = null, ?string $field = null, ?AccessDecision $accessDecision = null): bool
{
if (null === $this->securityChecker) {
return false;
}
if (null !== $field) {
if (!class_exists(FieldVote::class)) {
throw new \LogicException('Passing a $field to the "is_granted()" function requires symfony/acl. Try running "composer require symfony/acl-bundle" if you need field-level access control.');
}
$object = new FieldVote($object, $field);
}
try {
return $this->securityChecker->isGranted($role, $object, $accessDecision);
} catch (AuthenticationCredentialsNotFoundException) {
return false;
}
}
public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?string $field = null, ?AccessDecision $accessDecision = null): bool
{
if (null === $this->securityChecker) {
return false;
}
if (!$this->securityChecker instanceof UserAuthorizationCheckerInterface) {
throw new \LogicException(\sprintf('You cannot use "%s()" if the authorization checker doesn\'t implement "%s".%s', __METHOD__, UserAuthorizationCheckerInterface::class, interface_exists(UserAuthorizationCheckerInterface::class) ? ' Try upgrading the "symfony/security-core" package to v7.3 minimum.' : ''));
}
if (null !== $field) {
if (!class_exists(FieldVote::class)) {
throw new \LogicException('Passing a $field to the "is_granted_for_user()" function requires symfony/acl. Try running "composer require symfony/acl-bundle" if you need field-level access control.');
}
$subject = new FieldVote($subject, $field);
}
try {
return $this->securityChecker->isGrantedForUser($user, $attribute, $subject, $accessDecision);
} catch (AuthenticationCredentialsNotFoundException) {
return false;
}
}
public function getImpersonateExitUrl(?string $exitTo = null): string
{
if (null === $this->impersonateUrlGenerator) {
return '';
}
return $this->impersonateUrlGenerator->generateExitUrl($exitTo);
}
public function getImpersonateExitPath(?string $exitTo = null): string
{
if (null === $this->impersonateUrlGenerator) {
return '';
}
return $this->impersonateUrlGenerator->generateExitPath($exitTo);
}
public function getImpersonateUrl(string $identifier): string
{
if (null === $this->impersonateUrlGenerator) {
return '';
}
return $this->impersonateUrlGenerator->generateImpersonationUrl($identifier);
}
public function getImpersonatePath(string $identifier): string
{
if (null === $this->impersonateUrlGenerator) {
return '';
}
return $this->impersonateUrlGenerator->generateImpersonationPath($identifier);
}
public function getFunctions(): array
{
$functions = [
new TwigFunction('is_granted', $this->isGranted(...)),
new TwigFunction('impersonation_exit_url', $this->getImpersonateExitUrl(...)),
new TwigFunction('impersonation_exit_path', $this->getImpersonateExitPath(...)),
new TwigFunction('impersonation_url', $this->getImpersonateUrl(...)),
new TwigFunction('impersonation_path', $this->getImpersonatePath(...)),
];
if ($this->securityChecker instanceof UserAuthorizationCheckerInterface) {
$functions[] = new TwigFunction('is_granted_for_user', $this->isGrantedForUser(...));
}
return $functions;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* @author Jesse Rushlow <jr@rushlow.dev>
*/
final class SerializerExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('serialize', [SerializerRuntime::class, 'serialize']),
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Serializer\SerializerInterface;
use Twig\Extension\RuntimeExtensionInterface;
/**
* @author Jesse Rushlow <jr@rushlow.dev>
*/
final class SerializerRuntime implements RuntimeExtensionInterface
{
public function __construct(
private SerializerInterface $serializer,
) {
}
public function serialize(mixed $data, string $format = 'json', array $context = []): string
{
return $this->serializer->serialize($data, $format, $context);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\StopwatchTokenParser;
use Symfony\Component\Stopwatch\Stopwatch;
use Twig\Extension\AbstractExtension;
use Twig\TokenParser\TokenParserInterface;
/**
* Twig extension for the stopwatch helper.
*
* @author Wouter J <wouter@wouterj.nl>
*/
final class StopwatchExtension extends AbstractExtension
{
public function __construct(
private ?Stopwatch $stopwatch = null,
private bool $enabled = true,
) {
}
public function getStopwatch(): Stopwatch
{
return $this->stopwatch;
}
/**
* @return TokenParserInterface[]
*/
public function getTokenParsers(): array
{
return [
/*
* {% stopwatch foo %}
* Some stuff which will be recorded on the timeline
* {% endstopwatch %}
*/
new StopwatchTokenParser(null !== $this->stopwatch && $this->enabled),
];
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor;
use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser;
use Symfony\Bridge\Twig\TokenParser\TransTokenParser;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
// Help opcache.preload discover always-needed symbols
class_exists(TranslatorInterface::class);
class_exists(TranslatorTrait::class);
/**
* Provides integration of the Translation component with Twig.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TranslationExtension extends AbstractExtension
{
public function __construct(
private ?TranslatorInterface $translator = null,
private ?TranslationNodeVisitor $translationNodeVisitor = null,
) {
}
public function getTranslator(): TranslatorInterface
{
if (null === $this->translator) {
if (!interface_exists(TranslatorInterface::class)) {
throw new \LogicException(\sprintf('You cannot use the "%s" if the Translation Contracts are not available. Try running "composer require symfony/translation".', __CLASS__));
}
$this->translator = new class implements TranslatorInterface {
use TranslatorTrait;
};
}
return $this->translator;
}
public function getFunctions(): array
{
return [
new TwigFunction('t', $this->createTranslatable(...)),
];
}
public function getFilters(): array
{
return [
new TwigFilter('trans', $this->trans(...)),
];
}
public function getTokenParsers(): array
{
return [
// {% trans %}Symfony is great!{% endtrans %}
new TransTokenParser(),
// {% trans_default_domain "foobar" %}
new TransDefaultDomainTokenParser(),
];
}
public function getNodeVisitors(): array
{
return [$this->getTranslationNodeVisitor(), new TranslationDefaultDomainNodeVisitor()];
}
public function getTranslationNodeVisitor(): TranslationNodeVisitor
{
return $this->translationNodeVisitor ?: $this->translationNodeVisitor = new TranslationNodeVisitor();
}
/**
* @param array|string $arguments Can be the locale as a string when $message is a TranslatableInterface
*/
public function trans(string|\Stringable|TranslatableInterface|null $message, array|string $arguments = [], ?string $domain = null, ?string $locale = null, ?int $count = null): string
{
if ($message instanceof TranslatableInterface) {
if ([] !== $arguments && !\is_string($arguments)) {
throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be a locale passed as a string when the message is a "%s", "%s" given.', __METHOD__, TranslatableInterface::class, get_debug_type($arguments)));
}
if ($message instanceof TranslatableMessage && '' === $message->getMessage()) {
return '';
}
return $message->trans($this->getTranslator(), $locale ?? (\is_string($arguments) ? $arguments : null));
}
if (!\is_array($arguments)) {
throw new \TypeError(\sprintf('Unless the message is a "%s", argument 2 passed to "%s()" must be an array of parameters, "%s" given.', TranslatableInterface::class, __METHOD__, get_debug_type($arguments)));
}
if ('' === $message = (string) $message) {
return '';
}
if (null !== $count) {
$arguments['%count%'] = $count;
}
return $this->getTranslator()->trans($message, $arguments, $domain, $locale);
}
public function createTranslatable(string $message, array $parameters = [], ?string $domain = null): TranslatableMessage
{
if (!class_exists(TranslatableMessage::class)) {
throw new \LogicException(\sprintf('You cannot use the "%s" as the Translation Component is not installed. Try running "composer require symfony/translation".', __CLASS__));
}
return new TranslatableMessage($message, $parameters, $domain);
}
}

View File

@@ -0,0 +1,132 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\WebLink\GenericLinkProvider;
use Symfony\Component\WebLink\Link;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Twig extension for the Symfony WebLink component.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class WebLinkExtension extends AbstractExtension
{
public function __construct(
private RequestStack $requestStack,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('link', $this->link(...)),
new TwigFunction('preload', $this->preload(...)),
new TwigFunction('dns_prefetch', $this->dnsPrefetch(...)),
new TwigFunction('preconnect', $this->preconnect(...)),
new TwigFunction('prefetch', $this->prefetch(...)),
new TwigFunction('prerender', $this->prerender(...)),
];
}
/**
* Adds a "Link" HTTP header.
*
* @param string $rel The relation type (e.g. "preload", "prefetch", or "dns-prefetch")
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
* @return string The relation URI
*/
public function link(string $uri, string $rel, array $attributes = []): string
{
if (!$request = $this->requestStack->getMainRequest()) {
return $uri;
}
$link = new Link($rel, $uri);
foreach ($attributes as $key => $value) {
$link = $link->withAttribute($key, $value);
}
$linkProvider = $request->attributes->get('_links', new GenericLinkProvider());
$request->attributes->set('_links', $linkProvider->withLink($link));
return $uri;
}
/**
* Preloads a resource.
*
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['crossorigin' => 'use-credentials']")
*
* @return string The path of the asset
*/
public function preload(string $uri, array $attributes = []): string
{
return $this->link($uri, 'preload', $attributes);
}
/**
* Resolves a resource origin as early as possible.
*
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
* @return string The path of the asset
*/
public function dnsPrefetch(string $uri, array $attributes = []): string
{
return $this->link($uri, 'dns-prefetch', $attributes);
}
/**
* Initiates a early connection to a resource (DNS resolution, TCP handshake, TLS negotiation).
*
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
* @return string The path of the asset
*/
public function preconnect(string $uri, array $attributes = []): string
{
return $this->link($uri, 'preconnect', $attributes);
}
/**
* Indicates to the client that it should prefetch this resource.
*
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
* @return string The path of the asset
*/
public function prefetch(string $uri, array $attributes = []): string
{
return $this->link($uri, 'prefetch', $attributes);
}
/**
* Indicates to the client that it should prerender this resource.
*
* This feature is deprecated and superseded by the Speculation Rules API.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prerender
*
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
* @return string The path of the asset
*/
public function prerender(string $uri, array $attributes = []): string
{
return $this->link($uri, 'prerender', $attributes);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\TransitionBlockerList;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* WorkflowExtension.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Carlos Pereira De Amorim <carlos@shauri.fr>
*/
final class WorkflowExtension extends AbstractExtension
{
public function __construct(
private Registry $workflowRegistry,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('workflow_can', $this->canTransition(...)),
new TwigFunction('workflow_transitions', $this->getEnabledTransitions(...)),
new TwigFunction('workflow_transition', $this->getEnabledTransition(...)),
new TwigFunction('workflow_has_marked_place', $this->hasMarkedPlace(...)),
new TwigFunction('workflow_marked_places', $this->getMarkedPlaces(...)),
new TwigFunction('workflow_metadata', $this->getMetadata(...)),
new TwigFunction('workflow_transition_blockers', $this->buildTransitionBlockerList(...)),
];
}
/**
* Returns true if the transition is enabled.
*/
public function canTransition(object $subject, string $transitionName, ?string $name = null): bool
{
return $this->workflowRegistry->get($subject, $name)->can($subject, $transitionName);
}
/**
* Returns all enabled transitions.
*
* @return Transition[]
*/
public function getEnabledTransitions(object $subject, ?string $name = null): array
{
return $this->workflowRegistry->get($subject, $name)->getEnabledTransitions($subject);
}
public function getEnabledTransition(object $subject, string $transition, ?string $name = null): ?Transition
{
return $this->workflowRegistry->get($subject, $name)->getEnabledTransition($subject, $transition);
}
/**
* Returns true if the place is marked.
*/
public function hasMarkedPlace(object $subject, string $placeName, ?string $name = null): bool
{
return $this->workflowRegistry->get($subject, $name)->getMarking($subject)->has($placeName);
}
/**
* Returns marked places.
*
* @return string[]|int[]
*/
public function getMarkedPlaces(object $subject, bool $placesNameOnly = true, ?string $name = null): array
{
$places = $this->workflowRegistry->get($subject, $name)->getMarking($subject)->getPlaces();
if ($placesNameOnly) {
return array_keys($places);
}
return $places;
}
/**
* Returns the metadata for a specific subject.
*
* @param string|Transition|null $metadataSubject Use null to get workflow metadata
* Use a string (the place name) to get place metadata
* Use a Transition instance to get transition metadata
*/
public function getMetadata(object $subject, string $key, string|Transition|null $metadataSubject = null, ?string $name = null): mixed
{
return $this
->workflowRegistry
->get($subject, $name)
->getMetadataStore()
->getMetadata($key, $metadataSubject)
;
}
public function buildTransitionBlockerList(object $subject, string $transitionName, ?string $name = null): TransitionBlockerList
{
$workflow = $this->workflowRegistry->get($subject, $name);
return $workflow->buildTransitionBlockerList($subject, $transitionName);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Yaml\Dumper as YamlDumper;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* Provides integration of the Yaml component with Twig.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class YamlExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('yaml_encode', $this->encode(...)),
new TwigFilter('yaml_dump', $this->dump(...)),
];
}
public function encode(mixed $input, int $inline = 0, int $dumpObjects = 0): string
{
static $dumper;
$dumper ??= new YamlDumper();
if (\defined('Symfony\Component\Yaml\Yaml::DUMP_OBJECT')) {
return $dumper->dump($input, $inline, 0, $dumpObjects);
}
return $dumper->dump($input, $inline, 0, false, $dumpObjects);
}
public function dump(mixed $value, int $inline = 0, int $dumpObjects = 0): string
{
if (\is_resource($value)) {
return '%Resource%';
}
if (\is_array($value) || \is_object($value)) {
return '%'.\gettype($value).'% '.$this->encode($value, $inline, $dumpObjects);
}
return $this->encode($value, $inline, $dumpObjects);
}
}