N°6934 - Symfony 6.4 - upgrade Symfony bundles to 6.4 (#580)

* Update Symfony lib to version ~6.4.0
* Update code missing return type
* Add an iTop general configuration entry to store application secret (Symfony mandatory parameter)
* Use dependency injection in ExceptionListener & UserProvider classes
This commit is contained in:
bdalsass
2023-12-05 13:56:56 +01:00
committed by GitHub
parent 863ab4560c
commit 27ce51ab07
1392 changed files with 44869 additions and 27799 deletions

View File

@@ -13,10 +13,11 @@ namespace Symfony\Bridge\Twig;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Translation\LocaleSwitcher;
/**
* Exposes some Symfony parameters and services as an "app" global variable.
@@ -25,78 +26,89 @@ use Symfony\Component\Security\Core\User\UserInterface;
*/
class AppVariable
{
private $tokenStorage;
private $requestStack;
private $environment;
private $debug;
private TokenStorageInterface $tokenStorage;
private RequestStack $requestStack;
private string $environment;
private bool $debug;
private LocaleSwitcher $localeSwitcher;
private array $enabledLocales;
/**
* @return void
*/
public function setTokenStorage(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* @return void
*/
public function setRequestStack(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* @return void
*/
public function setEnvironment(string $environment)
{
$this->environment = $environment;
}
/**
* @return void
*/
public function setDebug(bool $debug)
{
$this->debug = $debug;
}
public function setLocaleSwitcher(LocaleSwitcher $localeSwitcher): void
{
$this->localeSwitcher = $localeSwitcher;
}
public function setEnabledLocales(array $enabledLocales): void
{
$this->enabledLocales = $enabledLocales;
}
/**
* Returns the current token.
*
* @return TokenInterface|null
*
* @throws \RuntimeException When the TokenStorage is not available
*/
public function getToken()
public function getToken(): ?TokenInterface
{
if (null === $tokenStorage = $this->tokenStorage) {
if (!isset($this->tokenStorage)) {
throw new \RuntimeException('The "app.token" variable is not available.');
}
return $tokenStorage->getToken();
return $this->tokenStorage->getToken();
}
/**
* Returns the current user.
*
* @return UserInterface|null
*
* @see TokenInterface::getUser()
*/
public function getUser()
public function getUser(): ?UserInterface
{
if (null === $tokenStorage = $this->tokenStorage) {
if (!isset($this->tokenStorage)) {
throw new \RuntimeException('The "app.user" variable is not available.');
}
if (!$token = $tokenStorage->getToken()) {
return null;
}
$user = $token->getUser();
// @deprecated since Symfony 5.4, $user will always be a UserInterface instance
return \is_object($user) ? $user : null;
return $this->tokenStorage->getToken()?->getUser();
}
/**
* Returns the current request.
*
* @return Request|null
*/
public function getRequest()
public function getRequest(): ?Request
{
if (null === $this->requestStack) {
if (!isset($this->requestStack)) {
throw new \RuntimeException('The "app.request" variable is not available.');
}
@@ -105,27 +117,23 @@ class AppVariable
/**
* Returns the current session.
*
* @return Session|null
*/
public function getSession()
public function getSession(): ?SessionInterface
{
if (null === $this->requestStack) {
if (!isset($this->requestStack)) {
throw new \RuntimeException('The "app.session" variable is not available.');
}
$request = $this->getRequest();
return $request && $request->hasSession() ? $request->getSession() : null;
return $request?->hasSession() ? $request->getSession() : null;
}
/**
* Returns the current app environment.
*
* @return string
*/
public function getEnvironment()
public function getEnvironment(): string
{
if (null === $this->environment) {
if (!isset($this->environment)) {
throw new \RuntimeException('The "app.environment" variable is not available.');
}
@@ -134,33 +142,53 @@ class AppVariable
/**
* Returns the current app debug mode.
*
* @return bool
*/
public function getDebug()
public function getDebug(): bool
{
if (null === $this->debug) {
if (!isset($this->debug)) {
throw new \RuntimeException('The "app.debug" variable is not available.');
}
return $this->debug;
}
public function getLocale(): string
{
if (!isset($this->localeSwitcher)) {
throw new \RuntimeException('The "app.locale" variable is not available.');
}
return $this->localeSwitcher->getLocale();
}
public function getEnabled_locales(): array
{
if (!isset($this->enabledLocales)) {
throw new \RuntimeException('The "app.enabled_locales" variable is not available.');
}
return $this->enabledLocales;
}
/**
* Returns some or all the existing flash messages:
* * getFlashes() returns all the flash messages
* * getFlashes('notice') returns a simple array with flash messages of that type
* * getFlashes(['notice', 'error']) returns a nested array of type => messages.
*
* @return array
*/
public function getFlashes($types = null)
public function getFlashes(string|array $types = null): array
{
try {
if (null === $session = $this->getSession()) {
return [];
}
} catch (\RuntimeException $e) {
} catch (\RuntimeException) {
return [];
}
// In 7.0 (when symfony/http-foundation: 6.4 is required) this can be updated to
// check if the session is an instance of FlashBagAwareSessionInterface
if (!method_exists($session, 'getFlashBag')) {
return [];
}
@@ -179,4 +207,25 @@ class AppVariable
return $result;
}
public function getCurrent_route(): ?string
{
if (!isset($this->requestStack)) {
throw new \RuntimeException('The "app.current_route" variable is not available.');
}
return $this->getRequest()?->attributes->get('_route');
}
/**
* @return array<string, mixed>
*/
public function getCurrent_route_parameters(): array
{
if (!isset($this->requestStack)) {
throw new \RuntimeException('The "app.current_route_parameters" variable is not available.');
}
return $this->getRequest()?->attributes->get('_route_params') ?? [];
}
}

View File

@@ -0,0 +1,34 @@
<?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\Attribute;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
class Template
{
public function __construct(
/**
* The name of the template to render.
*/
public string $template,
/**
* The controller method arguments to pass to the template.
*/
public ?array $vars = null,
/**
* Enables streaming the template.
*/
public bool $stream = false,
) {
}
}

View File

@@ -1,6 +1,32 @@
CHANGELOG
=========
6.4
---
* Allow an array to be passed as the first argument to the `importmap()` Twig function
* Add `TemplatedEmail::locale()` to set the locale for the email rendering
* Add `AppVariable::getEnabledLocales()` to retrieve the enabled locales
* Add `impersonation_path()` and `impersonation_url()` Twig functions
6.3
---
* Add `AppVariable::getLocale()` to retrieve the current locale when using the `LocaleSwitcher`
6.2
---
* Add `form_label_content` and `form_help_content` block to form themes
* Add `#[Template()]` to describe how to render arrays returned by controllers
* Add support for toggle buttons in Bootstrap 5 form theme
* Add `app.current_route` and `app.current_route_parameters` variables
6.1
---
* Wrap help messages on form elements in `div` instead of `p`
5.4
---

View File

@@ -11,6 +11,7 @@
namespace Symfony\Bridge\Twig\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
@@ -21,8 +22,8 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Twig\Environment;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
@@ -32,17 +33,20 @@ use Twig\Loader\FilesystemLoader;
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
#[AsCommand(name: 'debug:twig', description: 'Show a list of twig functions, filters, globals and tests')]
class DebugCommand extends Command
{
protected static $defaultName = 'debug:twig';
protected static $defaultDescription = 'Show a list of twig functions, filters, globals and tests';
private Environment $twig;
private ?string $projectDir;
private array $bundlesMetadata;
private ?string $twigDefaultPath;
private $twig;
private $projectDir;
private $bundlesMetadata;
private $twigDefaultPath;
private $filesystemLoaders;
private $fileLinkFormatter;
/**
* @var FilesystemLoader[]
*/
private array $filesystemLoaders;
private ?FileLinkFormatter $fileLinkFormatter;
public function __construct(Environment $twig, string $projectDir = null, array $bundlesMetadata = [], string $twigDefaultPath = null, FileLinkFormatter $fileLinkFormatter = null)
{
@@ -55,15 +59,17 @@ class DebugCommand extends Command
$this->fileLinkFormatter = $fileLinkFormatter;
}
/**
* @return void
*/
protected function configure()
{
$this
->setDefinition([
new InputArgument('name', InputArgument::OPTIONAL, 'The template name'),
new InputOption('filter', null, InputOption::VALUE_REQUIRED, 'Show details for all entries matching this filter'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (text or json)', 'text'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'text'),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> command outputs a list of twig functions,
filters, globals and tests.
@@ -88,7 +94,7 @@ EOF
;
}
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
@@ -98,16 +104,11 @@ EOF
throw new InvalidArgumentException(sprintf('Argument "name" not supported, it requires the Twig loader "%s".', FilesystemLoader::class));
}
switch ($input->getOption('format')) {
case 'text':
$name ? $this->displayPathsText($io, $name) : $this->displayGeneralText($io, $filter);
break;
case 'json':
$name ? $this->displayPathsJson($io, $name) : $this->displayGeneralJson($io, $filter);
break;
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format')));
}
match ($input->getOption('format')) {
'text' => $name ? $this->displayPathsText($io, $name) : $this->displayGeneralText($io, $filter),
'json' => $name ? $this->displayPathsJson($io, $name) : $this->displayGeneralJson($io, $filter),
default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
};
return 0;
}
@@ -119,11 +120,11 @@ EOF
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['text', 'json']);
$suggestions->suggestValues($this->getAvailableFormatOptions());
}
}
private function displayPathsText(SymfonyStyle $io, string $name)
private function displayPathsText(SymfonyStyle $io, string $name): void
{
$file = new \ArrayIterator($this->findTemplateFiles($name));
$paths = $this->getLoaderPaths($name);
@@ -164,9 +165,7 @@ EOF
[$namespace, $shortname] = $this->parseTemplateName($name);
$alternatives = $this->findAlternatives($shortname, $shortnames);
if (FilesystemLoader::MAIN_NAMESPACE !== $namespace) {
$alternatives = array_map(function ($shortname) use ($namespace) {
return '@'.$namespace.'/'.$shortname;
}, $alternatives);
$alternatives = array_map(fn ($shortname) => '@'.$namespace.'/'.$shortname, $alternatives);
}
}
@@ -200,7 +199,7 @@ EOF
}
}
private function displayPathsJson(SymfonyStyle $io, string $name)
private function displayPathsJson(SymfonyStyle $io, string $name): void
{
$files = $this->findTemplateFiles($name);
$paths = $this->getLoaderPaths($name);
@@ -218,7 +217,7 @@ EOF
$io->writeln(json_encode($data));
}
private function displayGeneralText(SymfonyStyle $io, string $filter = null)
private function displayGeneralText(SymfonyStyle $io, string $filter = null): void
{
$decorated = $io->isDecorated();
$types = ['functions', 'filters', 'tests', 'globals'];
@@ -252,7 +251,7 @@ EOF
}
}
private function displayGeneralJson(SymfonyStyle $io, ?string $filter)
private function displayGeneralJson(SymfonyStyle $io, ?string $filter): void
{
$decorated = $io->isDecorated();
$types = ['functions', 'filters', 'tests', 'globals'];
@@ -291,7 +290,7 @@ EOF
}
foreach ($namespaces as $namespace) {
$paths = array_map([$this, 'getRelativePath'], $loader->getPaths($namespace));
$paths = array_map($this->getRelativePath(...), $loader->getPaths($namespace));
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$namespace = '(None)';
@@ -306,7 +305,7 @@ EOF
return $loaderPaths;
}
private function getMetadata(string $type, $entity)
private function getMetadata(string $type, mixed $entity): mixed
{
if ('globals' === $type) {
return $entity;
@@ -364,7 +363,7 @@ EOF
return null;
}
private function getPrettyMetadata(string $type, $entity, bool $decorated): ?string
private function getPrettyMetadata(string $type, mixed $entity, bool $decorated): ?string
{
if ('tests' === $type) {
return '';
@@ -381,7 +380,7 @@ EOF
if ('globals' === $type) {
if (\is_object($meta)) {
return ' = object('.\get_class($meta).')';
return ' = object('.$meta::class.')';
}
$description = substr(@json_encode($meta), 0, 50);
@@ -545,7 +544,7 @@ EOF
}
$threshold = 1e3;
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
$alternatives = array_filter($alternatives, fn ($lev) => $lev < 2 * $threshold);
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
return array_keys($alternatives);
@@ -570,7 +569,7 @@ EOF
*/
private function getFilesystemLoaders(): array
{
if (null !== $this->filesystemLoaders) {
if (isset($this->filesystemLoaders)) {
return $this->filesystemLoaders;
}
$this->filesystemLoaders = [];
@@ -597,4 +596,9 @@ EOF
return (string) $this->fileLinkFormatter->format($absolutePath, 1);
}
private function getAvailableFormatOptions(): array
{
return ['text', 'json'];
}
}

View File

@@ -11,6 +11,7 @@
namespace Symfony\Bridge\Twig\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
@@ -35,30 +36,25 @@ use Twig\Source;
* @author Marc Weistroff <marc.weistroff@sensiolabs.com>
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
#[AsCommand(name: 'lint:twig', description: 'Lint a Twig template and outputs encountered errors')]
class LintCommand extends Command
{
protected static $defaultName = 'lint:twig';
protected static $defaultDescription = 'Lint a Twig template and outputs encountered errors';
private string $format;
private $twig;
/**
* @var string|null
*/
private $format;
public function __construct(Environment $twig)
{
public function __construct(
private Environment $twig,
private array $namePatterns = ['*.twig'],
) {
parent::__construct();
$this->twig = $twig;
}
/**
* @return void
*/
protected function configure()
{
$this
->setDescription(self::$defaultDescription)
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors')
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->setHelp(<<<'EOF'
@@ -83,16 +79,12 @@ EOF
;
}
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$filenames = $input->getArgument('filename');
$showDeprecations = $input->getOption('show-deprecations');
$this->format = $input->getOption('format');
if (null === $this->format) {
$this->format = GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt';
}
$this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt');
if (['-'] === $filenames) {
return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]);
@@ -151,12 +143,12 @@ EOF
return $filesInfo;
}
protected function findFiles(string $filename)
protected function findFiles(string $filename): iterable
{
if (is_file($filename)) {
return [$filename];
} elseif (is_dir($filename)) {
return Finder::create()->files()->in($filename)->name('*.twig');
return Finder::create()->files()->in($filename)->name($this->namePatterns);
}
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
@@ -180,21 +172,17 @@ EOF
return ['template' => $template, 'file' => $file, 'valid' => true];
}
private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files)
private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files): int
{
switch ($this->format) {
case 'txt':
return $this->displayTxt($output, $io, $files);
case 'json':
return $this->displayJson($output, $files);
case 'github':
return $this->displayTxt($output, $io, $files, true);
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format')));
}
return match ($this->format) {
'txt' => $this->displayTxt($output, $io, $files),
'json' => $this->displayJson($output, $files),
'github' => $this->displayTxt($output, $io, $files, true),
default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
};
}
private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false)
private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int
{
$errors = 0;
$githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($output) : null;
@@ -217,7 +205,7 @@ EOF
return min($errors, 1);
}
private function displayJson(OutputInterface $output, array $filesInfo)
private function displayJson(OutputInterface $output, array $filesInfo): int
{
$errors = 0;
@@ -236,13 +224,11 @@ EOF
return min($errors, 1);
}
private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null, GithubActionReporter $githubReporter = null)
private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null, GithubActionReporter $githubReporter = null): void
{
$line = $exception->getTemplateLine();
if ($githubReporter) {
$githubReporter->error($exception->getRawMessage(), $file, $line <= 0 ? null : $line);
}
$githubReporter?->error($exception->getRawMessage(), $file, $line <= 0 ? null : $line);
if ($file) {
$output->text(sprintf('<error> ERROR </error> in %s (line %s)', $file, $line));
@@ -271,7 +257,7 @@ EOF
}
}
private function getContext(string $template, int $line, int $context = 3)
private function getContext(string $template, int $line, int $context = 3): array
{
$lines = explode("\n", $template);
@@ -290,7 +276,12 @@ EOF
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['txt', 'json', 'github']);
$suggestions->suggestValues($this->getAvailableFormatOptions());
}
}
private function getAvailableFormatOptions(): array
{
return ['txt', 'json', 'github'];
}
}

View File

@@ -28,9 +28,9 @@ use Twig\Profiler\Profile;
*/
class TwigDataCollector extends DataCollector implements LateDataCollectorInterface
{
private $profile;
private $twig;
private $computed;
private Profile $profile;
private ?Environment $twig;
private array $computed;
public function __construct(Profile $profile, Environment $twig = null)
{
@@ -38,27 +38,18 @@ class TwigDataCollector extends DataCollector implements LateDataCollectorInterf
$this->twig = $twig;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Throwable $exception = null)
public function collect(Request $request, Response $response, \Throwable $exception = null): void
{
}
/**
* {@inheritdoc}
*/
public function reset()
public function reset(): void
{
$this->profile->reset();
$this->computed = null;
unset($this->computed);
$this->data = [];
}
/**
* {@inheritdoc}
*/
public function lateCollect()
public function lateCollect(): void
{
$this->data['profile'] = serialize($this->profile);
$this->data['template_paths'] = [];
@@ -71,7 +62,7 @@ class TwigDataCollector extends DataCollector implements LateDataCollectorInterf
if ($profile->isTemplate()) {
try {
$template = $this->twig->load($name = $profile->getName());
} catch (LoaderError $e) {
} catch (LoaderError) {
$template = null;
}
@@ -87,37 +78,37 @@ class TwigDataCollector extends DataCollector implements LateDataCollectorInterf
$templateFinder($this->profile);
}
public function getTime()
public function getTime(): float
{
return $this->getProfile()->getDuration() * 1000;
}
public function getTemplateCount()
public function getTemplateCount(): int
{
return $this->getComputedData('template_count');
}
public function getTemplatePaths()
public function getTemplatePaths(): array
{
return $this->data['template_paths'];
}
public function getTemplates()
public function getTemplates(): array
{
return $this->getComputedData('templates');
}
public function getBlockCount()
public function getBlockCount(): int
{
return $this->getComputedData('block_count');
}
public function getMacroCount()
public function getMacroCount(): int
{
return $this->getComputedData('macro_count');
}
public function getHtmlCallGraph()
public function getHtmlCallGraph(): Markup
{
$dumper = new HtmlDumper();
$dump = $dumper->dump($this->getProfile());
@@ -138,25 +129,19 @@ class TwigDataCollector extends DataCollector implements LateDataCollectorInterf
return new Markup($dump, 'UTF-8');
}
public function getProfile()
public function getProfile(): Profile
{
if (null === $this->profile) {
$this->profile = unserialize($this->data['profile'], ['allowed_classes' => ['Twig_Profiler_Profile', 'Twig\Profiler\Profile']]);
}
return $this->profile;
return $this->profile ??= unserialize($this->data['profile'], ['allowed_classes' => ['Twig_Profiler_Profile', Profile::class]]);
}
private function getComputedData(string $index)
private function getComputedData(string $index): mixed
{
if (null === $this->computed) {
$this->computed = $this->computeData($this->getProfile());
}
$this->computed ??= $this->computeData($this->getProfile());
return $this->computed[$index];
}
private function computeData(Profile $profile)
private function computeData(Profile $profile): array
{
$data = [
'template_count' => 0,
@@ -193,9 +178,6 @@ class TwigDataCollector extends DataCollector implements LateDataCollectorInterf
return $data;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'twig';

View File

@@ -25,40 +25,33 @@ use Twig\Environment;
*/
class TwigErrorRenderer implements ErrorRendererInterface
{
private $twig;
private $fallbackErrorRenderer;
private $debug;
private Environment $twig;
private HtmlErrorRenderer $fallbackErrorRenderer;
private \Closure|bool $debug;
/**
* @param bool|callable $debug The debugging mode as a boolean or a callable that should return it
*/
public function __construct(Environment $twig, HtmlErrorRenderer $fallbackErrorRenderer = null, $debug = false)
public function __construct(Environment $twig, HtmlErrorRenderer $fallbackErrorRenderer = null, bool|callable $debug = false)
{
if (!\is_bool($debug) && !\is_callable($debug)) {
throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, get_debug_type($debug)));
}
$this->twig = $twig;
$this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer();
$this->debug = $debug;
$this->debug = \is_bool($debug) ? $debug : $debug(...);
}
/**
* {@inheritdoc}
*/
public function render(\Throwable $exception): FlattenException
{
$exception = $this->fallbackErrorRenderer->render($exception);
$debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception);
$flattenException = FlattenException::createFromThrowable($exception);
$debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($flattenException);
if ($debug || !$template = $this->findTemplate($exception->getStatusCode())) {
return $exception;
if ($debug || !$template = $this->findTemplate($flattenException->getStatusCode())) {
return $this->fallbackErrorRenderer->render($exception);
}
return $exception->setAsString($this->twig->render($template, [
'exception' => $exception,
'status_code' => $exception->getStatusCode(),
'status_text' => $exception->getStatusText(),
return $flattenException->setAsString($this->twig->render($template, [
'exception' => $flattenException,
'status_code' => $flattenException->getStatusCode(),
'status_text' => $flattenException->getStatusText(),
]));
}

View File

@@ -0,0 +1,87 @@
<?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\EventListener;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Twig\Environment;
class TemplateAttributeListener implements EventSubscriberInterface
{
public function __construct(
private Environment $twig,
) {
}
/**
* @return void
*/
public function onKernelView(ViewEvent $event)
{
$parameters = $event->getControllerResult();
if (!\is_array($parameters ?? [])) {
return;
}
$attribute = $event->getRequest()->attributes->get('_template');
if (!$attribute instanceof Template && !$attribute = $event->controllerArgumentsEvent?->getAttributes()[Template::class][0] ?? null) {
return;
}
$parameters ??= $this->resolveParameters($event->controllerArgumentsEvent, $attribute->vars);
$status = 200;
foreach ($parameters as $k => $v) {
if (!$v instanceof FormInterface) {
continue;
}
if ($v->isSubmitted() && !$v->isValid()) {
$status = 422;
}
$parameters[$k] = $v->createView();
}
$event->setResponse($attribute->stream
? new StreamedResponse(fn () => $this->twig->display($attribute->template, $parameters), $status)
: new Response($this->twig->render($attribute->template, $parameters), $status)
);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onKernelView', -128],
];
}
private function resolveParameters(ControllerArgumentsEvent $event, ?array $vars): array
{
if ([] === $vars) {
return [];
}
$parameters = $event->getNamedArguments();
if (null !== $vars) {
$parameters = array_intersect_key($parameters, array_flip($vars));
}
return $parameters;
}
}

View File

@@ -22,21 +22,18 @@ use Twig\TwigFunction;
*/
final class AssetExtension extends AbstractExtension
{
private $packages;
private Packages $packages;
public function __construct(Packages $packages)
{
$this->packages = $packages;
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('asset', [$this, 'getAssetUrl']),
new TwigFunction('asset_version', [$this, 'getAssetVersion']),
new TwigFunction('asset', $this->getAssetUrl(...)),
new TwigFunction('asset_version', $this->getAssetVersion(...)),
];
}

View File

@@ -11,47 +11,46 @@
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* Twig extension relate to PHP code and used by the profiler and the default exception templates.
*
* This extension should only be used for debugging tools code
* that is never executed in a production environment.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal since Symfony 6.4
*/
final class CodeExtension extends AbstractExtension
{
private $fileLinkFormat;
private $charset;
private $projectDir;
private string|FileLinkFormatter|array|false $fileLinkFormat;
private string $charset;
private string $projectDir;
/**
* @param string|FileLinkFormatter $fileLinkFormat The format for links to source files
*/
public function __construct($fileLinkFormat, string $projectDir, string $charset)
public function __construct(string|FileLinkFormatter $fileLinkFormat, string $projectDir, string $charset)
{
$this->fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
$this->projectDir = str_replace('\\', '/', $projectDir).'/';
$this->charset = $charset;
}
/**
* {@inheritdoc}
*/
public function getFilters(): array
{
return [
new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
new TwigFilter('format_args', [$this, 'formatArgs'], ['is_safe' => ['html']]),
new TwigFilter('format_args_as_text', [$this, 'formatArgsAsText']),
new TwigFilter('file_excerpt', [$this, 'fileExcerpt'], ['is_safe' => ['html']]),
new TwigFilter('format_file', [$this, 'formatFile'], ['is_safe' => ['html']]),
new TwigFilter('format_file_from_text', [$this, 'formatFileFromText'], ['is_safe' => ['html']]),
new TwigFilter('format_log_message', [$this, 'formatLogMessage'], ['is_safe' => ['html']]),
new TwigFilter('file_link', [$this, 'getFileLink']),
new TwigFilter('file_relative', [$this, 'getFileRelative']),
new TwigFilter('abbr_class', $this->abbrClass(...), ['is_safe' => ['html'], 'pre_escape' => 'html']),
new TwigFilter('abbr_method', $this->abbrMethod(...), ['is_safe' => ['html'], 'pre_escape' => 'html']),
new TwigFilter('format_args', $this->formatArgs(...), ['is_safe' => ['html']]),
new TwigFilter('format_args_as_text', $this->formatArgsAsText(...)),
new TwigFilter('file_excerpt', $this->fileExcerpt(...), ['is_safe' => ['html']]),
new TwigFilter('format_file', $this->formatFile(...), ['is_safe' => ['html']]),
new TwigFilter('format_file_from_text', $this->formatFileFromText(...), ['is_safe' => ['html']]),
new TwigFilter('format_log_message', $this->formatLogMessage(...), ['is_safe' => ['html']]),
new TwigFilter('file_link', $this->getFileLink(...)),
new TwigFilter('file_relative', $this->getFileRelative(...)),
];
}
@@ -128,9 +127,7 @@ final class CodeExtension extends AbstractExtension
// remove main pre/code tags
$code = preg_replace('#^<pre.*?>\s*<code.*?>(.*)</code>\s*</pre>#s', '\\1', $code);
// split multiline code tags
$code = preg_replace_callback('#<code ([^>]++)>((?:[^<]*+\\n)++[^<]*+)</code>#', function ($m) {
return "<code $m[1]>".str_replace("\n", "</code>\n<code $m[1]>", $m[2]).'</code>';
}, $code);
$code = preg_replace_callback('#<code ([^>]++)>((?:[^<]*+\\n)++[^<]*+)</code>#', fn ($m) => "<code $m[1]>".str_replace("\n", "</code>\n<code $m[1]>", $m[2]).'</code>', $code);
// Convert spaces to html entities to preserve indentation when rendered
$code = str_replace(' ', '&nbsp;', $code);
$content = explode("\n", $code);
@@ -138,9 +135,7 @@ final class CodeExtension extends AbstractExtension
// remove main code/span tags
$code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code);
// split multiline spans
$code = preg_replace_callback('#<span ([^>]++)>((?:[^<]*+<br \/>)++[^<]*+)</span>#', function ($m) {
return "<span $m[1]>".str_replace('<br />', "</span><br /><span $m[1]>", $m[2]).'</span>';
}, $code);
$code = preg_replace_callback('#<span ([^>]++)>((?:[^<]*+<br \/>)++[^<]*+)</span>#', fn ($m) => "<span $m[1]>".str_replace('<br />', "</span><br /><span $m[1]>", $m[2]).'</span>', $code);
$content = explode('<br />', $code);
}
@@ -188,12 +183,7 @@ final class CodeExtension extends AbstractExtension
return $text;
}
/**
* Returns the link for a given file/line pair.
*
* @return string|false
*/
public function getFileLink(string $file, int $line)
public function getFileLink(string $file, int $line): string|false
{
if ($fmt = $this->fileLinkFormat) {
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
@@ -215,9 +205,7 @@ final class CodeExtension extends AbstractExtension
public function formatFileFromText(string $text): string
{
return preg_replace_callback('/in ("|&quot;)?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) {
return 'in '.$this->formatFile($match[2], $match[3]);
}, $text);
return preg_replace_callback('/in ("|&quot;)?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', fn ($match) => 'in '.$this->formatFile($match[2], $match[3]), $text);
}
/**

View File

@@ -20,9 +20,6 @@ use Twig\TwigFunction;
*/
final class CsrfExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [

View File

@@ -19,7 +19,7 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
*/
final class CsrfRuntime
{
private $csrfTokenManager;
private CsrfTokenManagerInterface $csrfTokenManager;
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
{

View File

@@ -26,8 +26,8 @@ use Twig\TwigFunction;
*/
final class DumpExtension extends AbstractExtension
{
private $cloner;
private $dumper;
private ClonerInterface $cloner;
private ?HtmlDumper $dumper;
public function __construct(ClonerInterface $cloner, HtmlDumper $dumper = null)
{
@@ -35,19 +35,13 @@ final class DumpExtension extends AbstractExtension
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('dump', [$this, 'dump'], ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true]),
new TwigFunction('dump', $this->dump(...), ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true]),
];
}
/**
* {@inheritdoc}
*/
public function getTokenParsers(): array
{
return [new DumpTokenParser()];
@@ -74,7 +68,7 @@ final class DumpExtension extends AbstractExtension
}
$dump = fopen('php://memory', 'r+');
$this->dumper = $this->dumper ?? new HtmlDumper();
$this->dumper ??= new HtmlDumper();
$this->dumper->setCharset($env->getCharset());
foreach ($vars as $value) {

View File

@@ -22,13 +22,10 @@ use Twig\TwigFunction;
*/
final class ExpressionExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('expression', [$this, 'createExpression']),
new TwigFunction('expression', $this->createExpression(...)),
];
}

View File

@@ -11,10 +11,13 @@
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\TranslatorInterface;
use Twig\Extension\AbstractExtension;
@@ -30,16 +33,13 @@ use Twig\TwigTest;
*/
final class FormExtension extends AbstractExtension
{
private $translator;
private ?TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator = null)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function getTokenParsers(): array
{
return [
@@ -48,46 +48,37 @@ final class FormExtension extends AbstractExtension
];
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('form_widget', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_errors', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_label', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_help', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_row', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_rest', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_start', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('form_end', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']]),
new TwigFunction('csrf_token', ['Symfony\Component\Form\FormRenderer', 'renderCsrfToken']),
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_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']),
new TwigFunction('field_name', $this->getFieldName(...)),
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(...)),
];
}
/**
* {@inheritdoc}
*/
public function getFilters(): array
{
return [
new TwigFilter('humanize', ['Symfony\Component\Form\FormRenderer', 'humanize']),
new TwigFilter('form_encode_currency', ['Symfony\Component\Form\FormRenderer', 'encodeCurrency'], ['is_safe' => ['html'], 'needs_environment' => true]),
new TwigFilter('humanize', [FormRenderer::class, 'humanize']),
new TwigFilter('form_encode_currency', [FormRenderer::class, 'encodeCurrency'], ['is_safe' => ['html'], 'needs_environment' => true]),
];
}
/**
* {@inheritdoc}
*/
public function getTests(): array
{
return [
@@ -103,10 +94,7 @@ final class FormExtension extends AbstractExtension
return $view->vars['full_name'];
}
/**
* @return string|array
*/
public function getFieldValue(FormView $view)
public function getFieldValue(FormView $view): string|array
{
return $view->vars['value'];
}
@@ -158,7 +146,7 @@ final class FormExtension extends AbstractExtension
yield from $this->createFieldChoicesList($view->vars['choices'], $view->vars['choice_translation_domain']);
}
private function createFieldChoicesList(iterable $choices, $translationDomain): iterable
private function createFieldChoicesList(iterable $choices, string|false|null $translationDomain): iterable
{
foreach ($choices as $choice) {
$translatableLabel = $this->createFieldTranslation($choice->label, [], $translationDomain);
@@ -174,7 +162,7 @@ final class FormExtension extends AbstractExtension
}
}
private function createFieldTranslation(?string $value, array $parameters, $domain): ?string
private function createFieldTranslation(?string $value, array $parameters, string|false|null $domain): ?string
{
if (!$this->translator || !$value || false === $domain) {
return $value;
@@ -189,11 +177,9 @@ final class FormExtension extends AbstractExtension
*
* This is a function and not callable due to performance reasons.
*
* @param string|array $selectedValue The selected value to compare
*
* @see ChoiceView::isSelected()
*/
function twig_is_selected_choice(ChoiceView $choice, $selectedValue): bool
function twig_is_selected_choice(ChoiceView $choice, string|array|null $selectedValue): bool
{
if (\is_array($selectedValue)) {
return \in_array($choice->value, $selectedValue, true);

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

@@ -23,21 +23,18 @@ use Twig\TwigFunction;
*/
final class HttpFoundationExtension extends AbstractExtension
{
private $urlHelper;
private UrlHelper $urlHelper;
public function __construct(UrlHelper $urlHelper)
{
$this->urlHelper = $urlHelper;
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('absolute_url', [$this, 'generateAbsoluteUrl']),
new TwigFunction('relative_path', [$this, 'generateRelativePath']),
new TwigFunction('absolute_url', $this->generateAbsoluteUrl(...)),
new TwigFunction('relative_path', $this->generateRelativePath(...)),
];
}

View File

@@ -22,16 +22,13 @@ use Twig\TwigFunction;
*/
final class HttpKernelExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
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', static::class.'::controller'),
new TwigFunction('controller', [self::class, 'controller']),
];
}

View File

@@ -22,8 +22,8 @@ use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface;
*/
final class HttpKernelRuntime
{
private $handler;
private $fragmentUriGenerator;
private FragmentHandler $handler;
private ?FragmentUriGeneratorInterface $fragmentUriGenerator;
public function __construct(FragmentHandler $handler, FragmentUriGeneratorInterface $fragmentUriGenerator = null)
{
@@ -34,11 +34,9 @@ final class HttpKernelRuntime
/**
* Renders a fragment.
*
* @param string|ControllerReference $uri A URI as a string or a ControllerReference instance
*
* @see FragmentHandler::render()
*/
public function renderFragment($uri, array $options = []): string
public function renderFragment(string|ControllerReference $uri, array $options = []): string
{
$strategy = $options['strategy'] ?? 'inline';
unset($options['strategy']);
@@ -49,11 +47,9 @@ final class HttpKernelRuntime
/**
* Renders a fragment.
*
* @param string|ControllerReference $uri A URI as a string or a ControllerReference instance
*
* @see FragmentHandler::render()
*/
public function renderFragmentStrategy(string $strategy, $uri, array $options = []): string
public function renderFragmentStrategy(string $strategy, string|ControllerReference $uri, array $options = []): string
{
return $this->handler->render($uri, $strategy, $options);
}

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,33 @@
<?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|null $entryPoint = 'app', array $attributes = []): string
{
if (null === $entryPoint) {
trigger_deprecation('symfony/twig-bridge', '6.4', 'Passing null as the first argument of the "importmap" Twig function is deprecated, pass an empty array if no entrypoints are desired.');
}
return $this->importMapRenderer->render($entryPoint, $attributes);
}
}

View File

@@ -22,21 +22,18 @@ use Twig\TwigFunction;
*/
final class LogoutUrlExtension extends AbstractExtension
{
private $generator;
private LogoutUrlGenerator $generator;
public function __construct(LogoutUrlGenerator $generator)
{
$this->generator = $generator;
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('logout_url', [$this, 'getLogoutUrl']),
new TwigFunction('logout_path', [$this, 'getLogoutPath']),
new TwigFunction('logout_url', $this->getLogoutUrl(...)),
new TwigFunction('logout_path', $this->getLogoutPath(...)),
];
}

View File

@@ -21,12 +21,12 @@ use Twig\Profiler\Profile;
*/
final class ProfilerExtension extends BaseProfilerExtension
{
private $stopwatch;
private ?Stopwatch $stopwatch;
/**
* @var \SplObjectStorage<Profile, StopwatchEvent>
*/
private $events;
private \SplObjectStorage $events;
public function __construct(Profile $profile, Stopwatch $stopwatch = null)
{

View File

@@ -25,21 +25,18 @@ use Twig\TwigFunction;
*/
final class RoutingExtension extends AbstractExtension
{
private $generator;
private UrlGeneratorInterface $generator;
public function __construct(UrlGeneratorInterface $generator)
{
$this->generator = $generator;
}
/**
* {@inheritdoc}
*/
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']]),
new TwigFunction('url', $this->getUrl(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
new TwigFunction('path', $this->getPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
];
}
@@ -58,7 +55,7 @@ final class RoutingExtension extends AbstractExtension
* 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 an URL that
* 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.
@@ -82,8 +79,8 @@ final class RoutingExtension extends AbstractExtension
$argsNode->hasNode(1) ? $argsNode->getNode(1) : null
);
if (null === $paramsNode || $paramsNode instanceof ArrayExpression && \count($paramsNode) <= 2 &&
(!$paramsNode->hasNode(1) || $paramsNode->getNode(1) instanceof ConstantExpression)
if (null === $paramsNode || $paramsNode instanceof ArrayExpression && \count($paramsNode) <= 2
&& (!$paramsNode->hasNode(1) || $paramsNode->getNode(1) instanceof ConstantExpression)
) {
return ['html'];
}

View File

@@ -25,9 +25,8 @@ use Twig\TwigFunction;
*/
final class SecurityExtension extends AbstractExtension
{
private $securityChecker;
private $impersonateUrlGenerator;
private ?AuthorizationCheckerInterface $securityChecker;
private ?ImpersonateUrlGenerator $impersonateUrlGenerator;
public function __construct(AuthorizationCheckerInterface $securityChecker = null, ImpersonateUrlGenerator $impersonateUrlGenerator = null)
{
@@ -35,10 +34,7 @@ final class SecurityExtension extends AbstractExtension
$this->impersonateUrlGenerator = $impersonateUrlGenerator;
}
/**
* @param mixed $object
*/
public function isGranted($role, $object = null, string $field = null): bool
public function isGranted(mixed $role, mixed $object = null, string $field = null): bool
{
if (null === $this->securityChecker) {
return false;
@@ -50,7 +46,7 @@ final class SecurityExtension extends AbstractExtension
try {
return $this->securityChecker->isGranted($role, $object);
} catch (AuthenticationCredentialsNotFoundException $e) {
} catch (AuthenticationCredentialsNotFoundException) {
return false;
}
}
@@ -73,15 +69,32 @@ final class SecurityExtension extends AbstractExtension
return $this->impersonateUrlGenerator->generateExitPath($exitTo);
}
/**
* {@inheritdoc}
*/
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
{
return [
new TwigFunction('is_granted', [$this, 'isGranted']),
new TwigFunction('impersonation_exit_url', [$this, 'getImpersonateExitUrl']),
new TwigFunction('impersonation_exit_path', [$this, 'getImpersonateExitPath']),
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(...)),
];
}
}

View File

@@ -19,14 +19,14 @@ use Twig\Extension\RuntimeExtensionInterface;
*/
final class SerializerRuntime implements RuntimeExtensionInterface
{
private $serializer;
private SerializerInterface $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
public function serialize($data, string $format = 'json', array $context = []): string
public function serialize(mixed $data, string $format = 'json', array $context = []): string
{
return $this->serializer->serialize($data, $format, $context);
}

View File

@@ -23,8 +23,8 @@ use Twig\TokenParser\TokenParserInterface;
*/
final class StopwatchExtension extends AbstractExtension
{
private $stopwatch;
private $enabled;
private ?Stopwatch $stopwatch;
private bool $enabled;
public function __construct(Stopwatch $stopwatch = null, bool $enabled = true)
{

View File

@@ -34,8 +34,8 @@ class_exists(TranslatorTrait::class);
*/
final class TranslationExtension extends AbstractExtension
{
private $translator;
private $translationNodeVisitor;
private ?TranslatorInterface $translator;
private ?TranslationNodeVisitor $translationNodeVisitor;
public function __construct(TranslatorInterface $translator = null, TranslationNodeVisitor $translationNodeVisitor = null)
{
@@ -58,29 +58,20 @@ final class TranslationExtension extends AbstractExtension
return $this->translator;
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('t', [$this, 'createTranslatable']),
new TwigFunction('t', $this->createTranslatable(...)),
];
}
/**
* {@inheritdoc}
*/
public function getFilters(): array
{
return [
new TwigFilter('trans', [$this, 'trans']),
new TwigFilter('trans', $this->trans(...)),
];
}
/**
* {@inheritdoc}
*/
public function getTokenParsers(): array
{
return [
@@ -92,9 +83,6 @@ final class TranslationExtension extends AbstractExtension
];
}
/**
* {@inheritdoc}
*/
public function getNodeVisitors(): array
{
return [$this->getTranslationNodeVisitor(), new TranslationDefaultDomainNodeVisitor()];
@@ -106,10 +94,9 @@ final class TranslationExtension extends AbstractExtension
}
/**
* @param string|\Stringable|TranslatableInterface|null $message
* @param array|string $arguments Can be the locale as a string when $message is a TranslatableInterface
* @param array|string $arguments Can be the locale as a string when $message is a TranslatableInterface
*/
public function trans($message, $arguments = [], string $domain = null, string $locale = null, int $count = null): string
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)) {

View File

@@ -24,25 +24,22 @@ use Twig\TwigFunction;
*/
final class WebLinkExtension extends AbstractExtension
{
private $requestStack;
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* {@inheritdoc}
*/
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']),
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(...)),
];
}

View File

@@ -25,26 +25,23 @@ use Twig\TwigFunction;
*/
final class WorkflowExtension extends AbstractExtension
{
private $workflowRegistry;
private Registry $workflowRegistry;
public function __construct(Registry $workflowRegistry)
{
$this->workflowRegistry = $workflowRegistry;
}
/**
* {@inheritdoc}
*/
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']),
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(...)),
];
}
@@ -102,7 +99,7 @@ final class WorkflowExtension extends AbstractExtension
* 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, $metadataSubject = null, string $name = null)
public function getMetadata(object $subject, string $key, string|Transition $metadataSubject = null, string $name = null): mixed
{
return $this
->workflowRegistry

View File

@@ -22,24 +22,19 @@ use Twig\TwigFilter;
*/
final class YamlExtension extends AbstractExtension
{
/**
* {@inheritdoc}
*/
public function getFilters(): array
{
return [
new TwigFilter('yaml_encode', [$this, 'encode']),
new TwigFilter('yaml_dump', [$this, 'dump']),
new TwigFilter('yaml_encode', $this->encode(...)),
new TwigFilter('yaml_dump', $this->dump(...)),
];
}
public function encode($input, int $inline = 0, int $dumpObjects = 0): string
public function encode(mixed $input, int $inline = 0, int $dumpObjects = 0): string
{
static $dumper;
if (null === $dumper) {
$dumper = new YamlDumper();
}
$dumper ??= new YamlDumper();
if (\defined('Symfony\Component\Yaml\Yaml::DUMP_OBJECT')) {
return $dumper->dump($input, $inline, 0, $dumpObjects);
@@ -48,7 +43,7 @@ final class YamlExtension extends AbstractExtension
return $dumper->dump($input, $inline, 0, false, $dumpObjects);
}
public function dump($value, int $inline = 0, int $dumpObjects = 0): string
public function dump(mixed $value, int $inline = 0, int $dumpObjects = 0): string
{
if (\is_resource($value)) {
return '%Resource%';

View File

@@ -21,15 +21,8 @@ use Twig\Template;
*/
class TwigRendererEngine extends AbstractRendererEngine
{
/**
* @var Environment
*/
private $environment;
/**
* @var Template
*/
private $template;
private Environment $environment;
private Template $template;
public function __construct(array $defaultThemes, Environment $environment)
{
@@ -37,10 +30,7 @@ class TwigRendererEngine extends AbstractRendererEngine
$this->environment = $environment;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormView $view, $resource, string $blockName, array $variables = [])
public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
@@ -69,10 +59,8 @@ class TwigRendererEngine extends AbstractRendererEngine
* case that the function "block()" is used in the Twig template.
*
* @see getResourceForBlock()
*
* @return bool
*/
protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName)
protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName): bool
{
// The caller guarantees that $this->resources[$cacheKey][$block] is
// not set, but it doesn't have to check whether $this->resources[$cacheKey]
@@ -144,21 +132,20 @@ class TwigRendererEngine extends AbstractRendererEngine
* to initialize the theme first. Any changes made to
* this variable will be kept and be available upon
* further calls to this method using the same theme.
*
* @return void
*/
protected function loadResourcesFromTheme(string $cacheKey, &$theme)
protected function loadResourcesFromTheme(string $cacheKey, mixed &$theme)
{
if (!$theme instanceof Template) {
/* @var Template $theme */
$theme = $this->environment->load($theme)->unwrap();
}
if (null === $this->template) {
// Store the first Template instance that we find so that
// we can call displayBlock() later on. It doesn't matter *which*
// template we use for that, since we pass the used blocks manually
// anyway.
$this->template = $theme;
}
// Store the first Template instance that we find so that
// we can call displayBlock() later on. It doesn't matter *which*
// template we use for that, since we pass the used blocks manually
// anyway.
$this->template ??= $theme;
// Use a separate variable for the inheritance traversal, because
// theme is a reference and we don't want to change it.

View File

@@ -11,10 +11,14 @@
namespace Symfony\Bridge\Twig\Mime;
use League\HTMLToMarkdown\HtmlConverter;
use League\HTMLToMarkdown\HtmlConverterInterface;
use Symfony\Component\Mime\BodyRendererInterface;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter;
use Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface;
use Symfony\Component\Mime\HtmlToTextConverter\LeagueHtmlToMarkdownConverter;
use Symfony\Component\Mime\Message;
use Symfony\Component\Translation\LocaleSwitcher;
use Twig\Environment;
/**
@@ -22,21 +26,17 @@ use Twig\Environment;
*/
final class BodyRenderer implements BodyRendererInterface
{
private $twig;
private $context;
private $converter;
private Environment $twig;
private array $context;
private HtmlToTextConverterInterface $converter;
private ?LocaleSwitcher $localeSwitcher = null;
public function __construct(Environment $twig, array $context = [])
public function __construct(Environment $twig, array $context = [], HtmlToTextConverterInterface $converter = null, LocaleSwitcher $localeSwitcher = null)
{
$this->twig = $twig;
$this->context = $context;
if (class_exists(HtmlConverter::class)) {
$this->converter = new HtmlConverter([
'hard_break' => true,
'strip_tags' => true,
'remove_nodes' => 'head style',
]);
}
$this->converter = $converter ?: (interface_exists(HtmlConverterInterface::class) ? new LeagueHtmlToMarkdownConverter() : new DefaultHtmlToTextConverter());
$this->localeSwitcher = $localeSwitcher;
}
public function render(Message $message): void
@@ -45,61 +45,47 @@ final class BodyRenderer implements BodyRendererInterface
return;
}
$messageContext = $message->getContext();
$previousRenderingKey = $messageContext[__CLASS__] ?? null;
unset($messageContext[__CLASS__]);
$currentRenderingKey = $this->getFingerPrint($message);
if ($previousRenderingKey === $currentRenderingKey) {
if (null === $message->getTextTemplate() && null === $message->getHtmlTemplate()) {
// email has already been rendered
return;
}
if (isset($messageContext['email'])) {
throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', get_debug_type($message)));
$callback = function () use ($message) {
$messageContext = $message->getContext();
if (isset($messageContext['email'])) {
throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', get_debug_type($message)));
}
$vars = array_merge($this->context, $messageContext, [
'email' => new WrappedTemplatedEmail($this->twig, $message),
]);
if ($template = $message->getTextTemplate()) {
$message->text($this->twig->render($template, $vars));
}
if ($template = $message->getHtmlTemplate()) {
$message->html($this->twig->render($template, $vars));
}
$message->markAsRendered();
// if text body is empty, compute one from the HTML body
if (!$message->getTextBody() && null !== $html = $message->getHtmlBody()) {
$text = $this->converter->convert(\is_resource($html) ? stream_get_contents($html) : $html, $message->getHtmlCharset());
$message->text($text, $message->getHtmlCharset());
}
};
$locale = $message->getLocale();
if ($locale && $this->localeSwitcher) {
$this->localeSwitcher->runWithLocale($locale, $callback);
return;
}
$vars = array_merge($this->context, $messageContext, [
'email' => new WrappedTemplatedEmail($this->twig, $message),
]);
if ($template = $message->getTextTemplate()) {
$message->text($this->twig->render($template, $vars));
}
if ($template = $message->getHtmlTemplate()) {
$message->html($this->twig->render($template, $vars));
}
// if text body is empty, compute one from the HTML body
if (!$message->getTextBody() && null !== $html = $message->getHtmlBody()) {
$message->text($this->convertHtmlToText(\is_resource($html) ? stream_get_contents($html) : $html));
}
$message->context($message->getContext() + [__CLASS__ => $currentRenderingKey]);
}
private function getFingerPrint(TemplatedEmail $message): string
{
$messageContext = $message->getContext();
unset($messageContext[__CLASS__]);
$payload = [$messageContext, $message->getTextTemplate(), $message->getHtmlTemplate()];
try {
$serialized = serialize($payload);
} catch (\Exception $e) {
// Serialization of 'Closure' is not allowed
// Happens when context contain a closure, in that case, we assume that context always change.
$serialized = random_bytes(8);
}
return md5($serialized);
}
private function convertHtmlToText(string $html): string
{
if (null !== $this->converter) {
return $this->converter->convert($html);
}
return strip_tags(preg_replace('{<(head|style)\b.*?</\1>}is', '', $html));
$callback();
}
}

View File

@@ -14,6 +14,7 @@ namespace Symfony\Bridge\Twig\Mime;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\Part\DataPart;
use Twig\Extra\CssInliner\CssInlinerExtension;
use Twig\Extra\Inky\InkyExtension;
use Twig\Extra\Markdown\MarkdownExtension;
@@ -28,8 +29,8 @@ class NotificationEmail extends TemplatedEmail
public const IMPORTANCE_MEDIUM = 'medium';
public const IMPORTANCE_LOW = 'low';
private $theme = 'default';
private $context = [
private string $theme = 'default';
private array $context = [
'importance' => self::IMPORTANCE_LOW,
'content' => '',
'exception' => false,
@@ -37,8 +38,9 @@ class NotificationEmail extends TemplatedEmail
'action_url' => null,
'markdown' => false,
'raw' => false,
'footer_text' => 'Notification e-mail sent by Symfony',
'footer_text' => 'Notification email sent by Symfony',
];
private bool $rendered = false;
public function __construct(Headers $headers = null, AbstractPart $body = null)
{
@@ -52,7 +54,7 @@ class NotificationEmail extends TemplatedEmail
}
if ($missingPackages) {
throw new \LogicException(sprintf('You cannot use "%s" if the "%s" Twig extension%s not available; try running "%s".', static::class, implode('" and "', $missingPackages), \count($missingPackages) > 1 ? 's are' : ' is', 'composer require '.implode(' ', array_keys($missingPackages))));
throw new \LogicException(sprintf('You cannot use "%s" if the "%s" Twig extension%s not available. Try running "%s".', static::class, implode('" and "', $missingPackages), \count($missingPackages) > 1 ? 's are' : ' is', 'composer require '.implode(' ', array_keys($missingPackages))));
}
parent::__construct($headers, $body);
@@ -72,7 +74,7 @@ class NotificationEmail extends TemplatedEmail
/**
* @return $this
*/
public function markAsPublic(): self
public function markAsPublic(): static
{
$this->context['importance'] = null;
$this->context['footer_text'] = null;
@@ -83,10 +85,10 @@ class NotificationEmail extends TemplatedEmail
/**
* @return $this
*/
public function markdown(string $content)
public function markdown(string $content): static
{
if (!class_exists(MarkdownExtension::class)) {
throw new \LogicException(sprintf('You cannot use "%s" if the Markdown Twig extension is not available; try running "composer require twig/markdown-extra".', __METHOD__));
throw new \LogicException(sprintf('You cannot use "%s" if the Markdown Twig extension is not available. Try running "composer require twig/markdown-extra".', __METHOD__));
}
$this->context['markdown'] = true;
@@ -97,7 +99,7 @@ class NotificationEmail extends TemplatedEmail
/**
* @return $this
*/
public function content(string $content, bool $raw = false)
public function content(string $content, bool $raw = false): static
{
$this->context['content'] = $content;
$this->context['raw'] = $raw;
@@ -108,7 +110,7 @@ class NotificationEmail extends TemplatedEmail
/**
* @return $this
*/
public function action(string $text, string $url)
public function action(string $text, string $url): static
{
$this->context['action_text'] = $text;
$this->context['action_url'] = $url;
@@ -119,7 +121,7 @@ class NotificationEmail extends TemplatedEmail
/**
* @return $this
*/
public function importance(string $importance)
public function importance(string $importance): static
{
$this->context['importance'] = $importance;
@@ -127,20 +129,14 @@ class NotificationEmail extends TemplatedEmail
}
/**
* @param \Throwable|FlattenException $exception
*
* @return $this
*/
public function exception($exception)
public function exception(\Throwable|FlattenException $exception): static
{
if (!$exception instanceof \Throwable && !$exception instanceof FlattenException) {
throw new \LogicException(sprintf('"%s" accepts "%s" or "%s" instances.', __METHOD__, \Throwable::class, FlattenException::class));
}
$exceptionAsString = $this->getExceptionAsString($exception);
$this->context['exception'] = true;
$this->attach($exceptionAsString, 'exception.txt', 'text/plain');
$this->addPart(new DataPart($exceptionAsString, 'exception.txt', 'text/plain'));
$this->importance(self::IMPORTANCE_URGENT);
if (!$this->getSubject()) {
@@ -153,7 +149,7 @@ class NotificationEmail extends TemplatedEmail
/**
* @return $this
*/
public function theme(string $theme)
public function theme(string $theme): static
{
$this->theme = $theme;
@@ -183,6 +179,18 @@ class NotificationEmail extends TemplatedEmail
return array_merge($this->context, parent::getContext());
}
public function isRendered(): bool
{
return $this->rendered;
}
public function markAsRendered(): void
{
parent::markAsRendered();
$this->rendered = true;
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
@@ -198,20 +206,15 @@ class NotificationEmail extends TemplatedEmail
private function determinePriority(string $importance): int
{
switch ($importance) {
case self::IMPORTANCE_URGENT:
return self::PRIORITY_HIGHEST;
case self::IMPORTANCE_HIGH:
return self::PRIORITY_HIGH;
case self::IMPORTANCE_MEDIUM:
return self::PRIORITY_NORMAL;
case self::IMPORTANCE_LOW:
default:
return self::PRIORITY_LOW;
}
return match ($importance) {
self::IMPORTANCE_URGENT => self::PRIORITY_HIGHEST,
self::IMPORTANCE_HIGH => self::PRIORITY_HIGH,
self::IMPORTANCE_MEDIUM => self::PRIORITY_NORMAL,
default => self::PRIORITY_LOW,
};
}
private function getExceptionAsString($exception): string
private function getExceptionAsString(\Throwable|FlattenException $exception): string
{
if (class_exists(FlattenException::class)) {
$exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception);
@@ -219,7 +222,7 @@ class NotificationEmail extends TemplatedEmail
return $exception->getAsString();
}
$message = \get_class($exception);
$message = $exception::class;
if ('' !== $exception->getMessage()) {
$message .= ': '.$exception->getMessage();
}
@@ -235,7 +238,7 @@ class NotificationEmail extends TemplatedEmail
*/
public function __serialize(): array
{
return [$this->context, $this->theme, parent::__serialize()];
return [$this->context, $this->theme, $this->rendered, parent::__serialize()];
}
/**
@@ -243,7 +246,9 @@ class NotificationEmail extends TemplatedEmail
*/
public function __unserialize(array $data): void
{
if (3 === \count($data)) {
if (4 === \count($data)) {
[$this->context, $this->theme, $this->rendered, $parentData] = $data;
} elseif (3 === \count($data)) {
[$this->context, $this->theme, $parentData] = $data;
} else {
// Backwards compatibility for deserializing data structures that were serialized without the theme

View File

@@ -18,14 +18,15 @@ use Symfony\Component\Mime\Email;
*/
class TemplatedEmail extends Email
{
private $htmlTemplate;
private $textTemplate;
private $context = [];
private ?string $htmlTemplate = null;
private ?string $textTemplate = null;
private ?string $locale = null;
private array $context = [];
/**
* @return $this
*/
public function textTemplate(?string $template)
public function textTemplate(?string $template): static
{
$this->textTemplate = $template;
@@ -35,13 +36,23 @@ class TemplatedEmail extends Email
/**
* @return $this
*/
public function htmlTemplate(?string $template)
public function htmlTemplate(?string $template): static
{
$this->htmlTemplate = $template;
return $this;
}
/**
* @return $this
*/
public function locale(?string $locale): static
{
$this->locale = $locale;
return $this;
}
public function getTextTemplate(): ?string
{
return $this->textTemplate;
@@ -52,10 +63,15 @@ class TemplatedEmail extends Email
return $this->htmlTemplate;
}
public function getLocale(): ?string
{
return $this->locale;
}
/**
* @return $this
*/
public function context(array $context)
public function context(array $context): static
{
$this->context = $context;
@@ -67,12 +83,24 @@ class TemplatedEmail extends Email
return $this->context;
}
public function isRendered(): bool
{
return null === $this->htmlTemplate && null === $this->textTemplate;
}
public function markAsRendered(): void
{
$this->textTemplate = null;
$this->htmlTemplate = null;
$this->context = [];
}
/**
* @internal
*/
public function __serialize(): array
{
return [$this->htmlTemplate, $this->textTemplate, $this->context, parent::__serialize()];
return [$this->htmlTemplate, $this->textTemplate, $this->context, parent::__serialize(), $this->locale];
}
/**
@@ -81,6 +109,7 @@ class TemplatedEmail extends Email
public function __unserialize(array $data): void
{
[$this->htmlTemplate, $this->textTemplate, $this->context, $parentData] = $data;
$this->locale = $data[4] ?? null;
parent::__unserialize($parentData);
}

View File

@@ -12,6 +12,8 @@
namespace Symfony\Bridge\Twig\Mime;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File;
use Twig\Environment;
/**
@@ -21,8 +23,8 @@ use Twig\Environment;
*/
final class WrappedTemplatedEmail
{
private $twig;
private $message;
private Environment $twig;
private TemplatedEmail $message;
public function __construct(Environment $twig, TemplatedEmail $message)
{
@@ -44,11 +46,8 @@ final class WrappedTemplatedEmail
public function image(string $image, string $contentType = null): string
{
$file = $this->twig->getLoader()->getSourceContext($image);
if ($path = $file->getPath()) {
$this->message->embedFromPath($path, $image, $contentType);
} else {
$this->message->embed($file->getCode(), $image, $contentType);
}
$body = $file->getPath() ? new File($file->getPath()) : $file->getCode();
$this->message->addPart((new DataPart($body, $image, $contentType))->asInline());
return 'cid:'.$image;
}
@@ -63,17 +62,14 @@ final class WrappedTemplatedEmail
public function attach(string $file, string $name = null, string $contentType = null): void
{
$file = $this->twig->getLoader()->getSourceContext($file);
if ($path = $file->getPath()) {
$this->message->attachFromPath($path, $name, $contentType);
} else {
$this->message->attach($file->getCode(), $name, $contentType);
}
$body = $file->getPath() ? new File($file->getPath()) : $file->getCode();
$this->message->addPart(new DataPart($body, $name, $contentType));
}
/**
* @return $this
*/
public function setSubject(string $subject): self
public function setSubject(string $subject): static
{
$this->message->subject($subject);
@@ -88,7 +84,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function setReturnPath(string $address): self
public function setReturnPath(string $address): static
{
$this->message->returnPath($address);
@@ -103,7 +99,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function addFrom(string $address, string $name = ''): self
public function addFrom(string $address, string $name = ''): static
{
$this->message->addFrom(new Address($address, $name));
@@ -121,7 +117,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function addReplyTo(string $address): self
public function addReplyTo(string $address): static
{
$this->message->addReplyTo($address);
@@ -139,7 +135,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function addTo(string $address, string $name = ''): self
public function addTo(string $address, string $name = ''): static
{
$this->message->addTo(new Address($address, $name));
@@ -157,7 +153,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function addCc(string $address, string $name = ''): self
public function addCc(string $address, string $name = ''): static
{
$this->message->addCc(new Address($address, $name));
@@ -175,7 +171,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function addBcc(string $address, string $name = ''): self
public function addBcc(string $address, string $name = ''): static
{
$this->message->addBcc(new Address($address, $name));
@@ -193,7 +189,7 @@ final class WrappedTemplatedEmail
/**
* @return $this
*/
public function setPriority(int $priority): self
public function setPriority(int $priority): static
{
$this->message->priority($priority);

View File

@@ -19,7 +19,7 @@ use Twig\Node\Node;
*/
final class DumpNode extends Node
{
private $varPrefix;
private string $varPrefix;
public function __construct(string $varPrefix, ?Node $values, int $lineno, string $tag = null)
{

View File

@@ -16,9 +16,9 @@ namespace Symfony\Bridge\Twig\NodeVisitor;
*/
class Scope
{
private $parent;
private $data = [];
private $left = false;
private ?self $parent;
private array $data = [];
private bool $left = false;
public function __construct(self $parent = null)
{
@@ -27,20 +27,16 @@ class Scope
/**
* Opens a new child scope.
*
* @return self
*/
public function enter()
public function enter(): self
{
return new self($this);
}
/**
* Closes current scope and returns parent one.
*
* @return self|null
*/
public function leave()
public function leave(): ?self
{
$this->left = true;
@@ -54,7 +50,7 @@ class Scope
*
* @throws \LogicException
*/
public function set(string $key, $value)
public function set(string $key, mixed $value): static
{
if ($this->left) {
throw new \LogicException('Left scope is not mutable.');
@@ -67,10 +63,8 @@ class Scope
/**
* Tests if a data is visible from current scope.
*
* @return bool
*/
public function has(string $key)
public function has(string $key): bool
{
if (\array_key_exists($key, $this->data)) {
return true;
@@ -85,10 +79,8 @@ class Scope
/**
* Returns data visible from current scope.
*
* @return mixed
*/
public function get(string $key, $default = null)
public function get(string $key, mixed $default = null): mixed
{
if (\array_key_exists($key, $this->data)) {
return $this->data[$key];

View File

@@ -30,16 +30,13 @@ use Twig\NodeVisitor\AbstractNodeVisitor;
*/
final class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor
{
private $scope;
private Scope $scope;
public function __construct()
{
$this->scope = new Scope();
}
/**
* {@inheritdoc}
*/
protected function doEnterNode(Node $node, Environment $env): Node
{
if ($node instanceof BlockNode || $node instanceof ModuleNode) {
@@ -86,9 +83,6 @@ final class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor
return $node;
}
/**
* {@inheritdoc}
*/
protected function doLeaveNode(Node $node, Environment $env): ?Node
{
if ($node instanceof TransDefaultDomainNode) {
@@ -102,9 +96,6 @@ final class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
return -10;

View File

@@ -29,8 +29,8 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
{
public const UNDEFINED_DOMAIN = '_undefined';
private $enabled = false;
private $messages = [];
private bool $enabled = false;
private array $messages = [];
public function enable(): void
{
@@ -49,9 +49,6 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
return $this->messages;
}
/**
* {@inheritdoc}
*/
protected function doEnterNode(Node $node, Environment $env): Node
{
if (!$this->enabled) {
@@ -59,9 +56,9 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
}
if (
$node instanceof FilterExpression &&
'trans' === $node->getNode('filter')->getAttribute('value') &&
$node->getNode('node') instanceof ConstantExpression
$node instanceof FilterExpression
&& 'trans' === $node->getNode('filter')->getAttribute('value')
&& $node->getNode('node') instanceof ConstantExpression
) {
// extract constant nodes with a trans filter
$this->messages[] = [
@@ -69,8 +66,8 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
$this->getReadDomainFromArguments($node->getNode('arguments'), 1),
];
} elseif (
$node instanceof FunctionExpression &&
't' === $node->getAttribute('name')
$node instanceof FunctionExpression
&& 't' === $node->getAttribute('name')
) {
$nodeArguments = $node->getNode('arguments');
@@ -87,10 +84,10 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
$node->hasNode('domain') ? $this->getReadDomainFromNode($node->getNode('domain')) : null,
];
} elseif (
$node instanceof FilterExpression &&
'trans' === $node->getNode('filter')->getAttribute('value') &&
$node->getNode('node') instanceof ConcatBinary &&
$message = $this->getConcatValueFromNode($node->getNode('node'), null)
$node instanceof FilterExpression
&& 'trans' === $node->getNode('filter')->getAttribute('value')
&& $node->getNode('node') instanceof ConcatBinary
&& $message = $this->getConcatValueFromNode($node->getNode('node'), null)
) {
$this->messages[] = [
$message,
@@ -101,17 +98,11 @@ final class TranslationNodeVisitor extends AbstractNodeVisitor
return $node;
}
/**
* {@inheritdoc}
*/
protected function doLeaveNode(Node $node, Environment $env): ?Node
{
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
return 0;

View File

@@ -233,30 +233,8 @@
{% if required -%}
{% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) %}
{%- endif -%}
{% if label is empty -%}
{%- if label_format is not empty -%}
{% set label = label_format|replace({
'%name%': name,
'%id%': id,
}) %}
{%- else -%}
{% set label = name|humanize %}
{%- endif -%}
{%- endif -%}
<{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>
{%- if translation_domain is same as(false) -%}
{%- if label_html is same as(false) -%}
{{- label -}}
{%- else -%}
{{- label|raw -}}
{%- endif -%}
{%- else -%}
{%- if label_html is same as(false) -%}
{{- label|trans(label_translation_parameters, translation_domain) -}}
{%- else -%}
{{- label|trans(label_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{{- block('form_label_content') -}}
{% block form_label_errors %}{{- form_errors(form) -}}{% endblock form_label_errors %}</{{ element|default('label') }}>
{%- else -%}
{%- if errors|length > 0 -%}
@@ -287,33 +265,11 @@
{% set embed_label_classes = parent_label_class|split(' ')|filter(class => class in ['checkbox-inline', 'radio-inline']) %}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ embed_label_classes|join(' '))|trim}) -%}
{% endif %}
{%- if label is not same as(false) and label is empty -%}
{%- if label_format is not empty -%}
{%- set label = label_format|replace({
'%name%': name,
'%id%': id,
}) -%}
{%- else -%}
{%- set label = name|humanize -%}
{%- endif -%}
{%- endif -%}
{{ widget|raw }}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if label is not same as(false) -%}
{%- if translation_domain is same as(false) -%}
{%- if label_html is same as(false) -%}
{{- label -}}
{%- else -%}
{{- label|raw -}}
{%- endif -%}
{%- else -%}
{%- if label_html is same as(false) -%}
{{- label|trans(label_translation_parameters, translation_domain) -}}
{%- else -%}
{{- label|trans(label_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{{- block('form_label_content') -}}
{%- endif -%}
{{- form_errors(form) -}}
</label>
@@ -357,19 +313,7 @@
{%- if help is not empty -%}
{%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ ' form-text text-muted')|trim}) -%}
<small id="{{ id }}_help"{% with { attr: help_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if translation_domain is same as(false) -%}
{%- if help_html is same as(false) -%}
{{- help -}}
{%- else -%}
{{- help|raw -}}
{%- endif -%}
{%- else -%}
{%- if help_html is same as(false) -%}
{{- help|trans(help_translation_parameters, translation_domain) -}}
{%- else -%}
{{- help|trans(help_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{{- block('form_help_content') -}}
</small>
{%- endif -%}
{%- endblock form_help %}

View File

@@ -209,30 +209,48 @@
{%- endblock submit_widget %}
{%- block checkbox_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
{%- set attr_class = attr_class|default(attr.class|default('')) -%}
{%- set row_class = '' -%}
{%- if 'btn-check' not in attr_class -%}
{%- set attr_class = attr_class ~ ' form-check-input' -%}
{%- set row_class = 'form-check' -%}
{%- endif -%}
{%- set attr = attr|merge({class: attr_class|trim}) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- set row_class = 'form-check' -%}
{%- if 'checkbox-inline' in parent_label_class %}
{%- set row_class = row_class ~ ' form-check-inline' -%}
{% endif -%}
{%- if 'checkbox-switch' in parent_label_class %}
{%- set row_class = row_class ~ ' form-switch' -%}
{% endif -%}
<div class="{{ row_class }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- if row_class is not empty -%}
<div class="{{ row_class }}">
{%- endif -%}
{{- form_label(form, null, { widget: parent() }) -}}
{%- if row_class is not empty -%}
</div>
{%- endif -%}
{%- endblock checkbox_widget %}
{%- block radio_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
{%- set attr_class = attr_class|default(attr.class|default('')) -%}
{%- set row_class = '' -%}
{%- if 'btn-check' not in attr_class -%}
{%- set attr_class = attr_class ~ ' form-check-input' -%}
{%- set row_class = 'form-check' -%}
{%- endif -%}
{%- set attr = attr|merge({class: attr_class|trim}) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- set row_class = 'form-check' -%}
{%- if 'radio-inline' in parent_label_class -%}
{%- set row_class = row_class ~ ' form-check-inline' -%}
{%- endif -%}
<div class="{{ row_class }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- if row_class is not empty -%}
<div class="{{ row_class }}">
{%- endif -%}
{{- form_label(form, null, { widget: parent() }) -}}
{%- if row_class is not empty -%}
</div>
{%- endif -%}
{%- endblock radio_widget %}
{%- block choice_widget_collapsed -%}
@@ -276,7 +294,11 @@
{%- block checkbox_radio_label -%}
{#- Do not display the label if widget is not defined in order to prevent double label rendering -#}
{%- if widget is defined -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-check-label')|trim}) -%}
{%- set label_attr_class = label_attr_class|default(label_attr.class|default('')) -%}
{%- if 'btn' not in label_attr_class -%}
{%- set label_attr_class = label_attr_class ~ ' form-check-label' -%}
{%- endif -%}
{%- set label_attr = label_attr|merge({class: label_attr_class|trim}) -%}
{%- if not compound -%}
{% set label_attr = label_attr|merge({'for': id}) %}
{%- endif -%}
@@ -286,33 +308,11 @@
{%- if parent_label_class is defined -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) -%}
{%- endif -%}
{%- if label is not same as(false) and label is empty -%}
{%- if label_format is not empty -%}
{%- set label = label_format|replace({
'%name%': name,
'%id%': id,
}) -%}
{%- else -%}
{%- set label = name|humanize -%}
{%- endif -%}
{%- endif -%}
{{ widget|raw }}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if label is not same as(false) -%}
{%- if translation_domain is same as(false) -%}
{%- if label_html is same as(false) -%}
{{- label -}}
{%- else -%}
{{- label|raw -}}
{%- endif -%}
{%- else -%}
{%- if label_html is same as(false) -%}
{{- label|trans(label_translation_parameters, translation_domain) -}}
{%- else -%}
{{- label|trans(label_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{{- block('form_label_content') -}}
{%- endif -%}
</label>
{%- endif -%}
@@ -353,7 +353,7 @@
{%- block form_errors -%}
{%- if errors|length > 0 -%}
{%- for error in errors -%}
<div class="invalid-feedback d-block">{{ error.message }}</div>
<div class="{% if form is not rootform %}invalid-feedback{% else %}alert alert-danger{% endif %} d-block">{{ error.message }}</div>
{%- endfor -%}
{%- endif %}
{%- endblock form_errors %}

View File

@@ -14,7 +14,7 @@
{# Attribute "required" is not supported #}
{%- set required = false -%}
{%- endif -%}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
<input type="{{ type }}" {{ block('widget_attributes') }}{% if value is not empty %} value="{{ value }}"{% endif %}>
{%- endblock form_widget_simple -%}
{%- block form_widget_compound -%}
@@ -61,7 +61,7 @@
{%- endif -%}
<select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
{%- if placeholder is not none -%}
<option value=""{% if required and value is empty %} selected="selected"{% endif %}>{{ placeholder != '' ? (translation_domain is same as(false) ? placeholder : placeholder|trans({}, translation_domain)) }}</option>
<option value=""{% if placeholder_attr|default({}) %}{% with { attr: placeholder_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if required and value is empty %} selected="selected"{% endif %}>{{ placeholder != '' ? (translation_domain is same as(false) ? placeholder : placeholder|trans({}, translation_domain)) }}</option>
{%- endif -%}
{%- if preferred_choices|length > 0 -%}
{% set options = preferred_choices %}
@@ -91,11 +91,11 @@
{%- endblock choice_widget_options -%}
{%- block checkbox_widget -%}
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %}>
{%- endblock checkbox_widget -%}
{%- block radio_widget -%}
<input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
<input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %}>
{%- endblock radio_widget -%}
{%- block datetime_widget -%}
@@ -290,34 +290,38 @@
{% if required -%}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
{%- endif -%}
{% if label is empty -%}
{%- if label_format is not empty -%}
{% set label = label_format|replace({
'%name%': name,
'%id%': id,
}) %}
{%- else -%}
{% set label = name|humanize %}
{%- endif -%}
{%- endif -%}
<{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>
{%- if translation_domain is same as(false) -%}
{%- if label_html is same as(false) -%}
{{- label -}}
{%- else -%}
{{- label|raw -}}
{%- endif -%}
{%- else -%}
{%- if label_html is same as(false) -%}
{{- label|trans(label_translation_parameters, translation_domain) -}}
{%- else -%}
{{- label|trans(label_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{{- block('form_label_content') -}}
</{{ element|default('label') }}>
{%- endif -%}
{%- endblock form_label -%}
{%- block form_label_content -%}
{%- if label is empty -%}
{%- if label_format is not empty -%}
{% set label = label_format|replace({
'%name%': name,
'%id%': id,
}) %}
{%- else -%}
{% set label = name|humanize %}
{%- endif -%}
{%- endif -%}
{%- if translation_domain is same as(false) -%}
{%- if label_html is same as(false) -%}
{{- label -}}
{%- else -%}
{{- label|raw -}}
{%- endif -%}
{%- else -%}
{%- if label_html is same as(false) -%}
{{- label|trans(label_translation_parameters, translation_domain) -}}
{%- else -%}
{{- label|trans(label_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{%- endblock form_label_content -%}
{%- block button_label -%}{%- endblock -%}
{# Help #}
@@ -325,24 +329,28 @@
{% block form_help -%}
{%- if help is not empty -%}
{%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ ' help-text')|trim}) -%}
<p id="{{ id }}_help"{% with { attr: help_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if translation_domain is same as(false) -%}
{%- if help_html is same as(false) -%}
{{- help -}}
{%- else -%}
{{- help|raw -}}
{%- endif -%}
{%- else -%}
{%- if help_html is same as(false) -%}
{{- help|trans(help_translation_parameters, translation_domain) -}}
{%- else -%}
{{- help|trans(help_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
</p>
<{{ element|default('div') }} id="{{ id }}_help"{% with { attr: help_attr } %}{{ block('attributes') }}{% endwith %}>
{{- block('form_help_content') -}}
</{{ element|default('div') }}>
{%- endif -%}
{%- endblock form_help %}
{% block form_help_content -%}
{%- if translation_domain is same as(false) -%}
{%- if help_html is same as(false) -%}
{{- help -}}
{%- else -%}
{{- help|raw -}}
{%- endif -%}
{%- else -%}
{%- if help_html is same as(false) -%}
{{- help|trans(help_translation_parameters, translation_domain) -}}
{%- else -%}
{{- help|trans(help_translation_parameters, translation_domain)|raw -}}
{%- endif -%}
{%- endif -%}
{%- endblock form_help_content %}
{# Rows #}
{%- block repeated_row -%}
@@ -394,7 +402,7 @@
{%- endif -%}
<form{% if name != '' %} name="{{ name }}"{% endif %} method="{{ form_method|lower }}"{% if action != '' %} action="{{ action }}"{% endif %}{{ block('attributes') }}{% if multipart %} enctype="multipart/form-data"{% endif %}>
{%- if form_method != method -%}
<input type="hidden" name="_method" value="{{ method }}" />
<input type="hidden" name="_method" value="{{ method }}">
{%- endif -%}
{%- endblock form_start -%}
@@ -432,7 +440,7 @@
{%- endif -%}
{%- if form_method != method -%}
<input type="hidden" name="_method" value="{{ method }}" />
<input type="hidden" name="_method" value="{{ method }}">
{%- endif -%}
{% endif -%}
{% endblock form_rest %}

View File

@@ -156,7 +156,7 @@
{%- endif -%}
<select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple" data-customforms="disabled"{% endif %}>
{% if placeholder is not none -%}
<option value=""{% if required and value is empty %} selected="selected"{% endif %}>{{ translation_domain is same as(false) ? placeholder : placeholder|trans({}, translation_domain) }}</option>
<option value=""{% if placeholder_attr|default({}) %}{% with { attr: placeholder_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if required and value is empty %} selected="selected"{% endif %}>{{ translation_domain is same as(false) ? placeholder : placeholder|trans({}, translation_domain) }}</option>
{%- endif %}
{%- if preferred_choices|length > 0 -%}
{% set options = preferred_choices %}

View File

@@ -29,9 +29,6 @@ use Twig\TokenParser\AbstractTokenParser;
*/
final class DumpTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$values = null;
@@ -43,9 +40,6 @@ final class DumpTokenParser extends AbstractTokenParser
return new DumpNode($this->parser->getVarName(), $values, $token->getLine(), $this->getTag());
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'dump';

View File

@@ -24,9 +24,6 @@ use Twig\TokenParser\AbstractTokenParser;
*/
final class FormThemeTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$lineno = $token->getLine();
@@ -54,9 +51,6 @@ final class FormThemeTokenParser extends AbstractTokenParser
return new FormThemeNode($form, $resources, $lineno, $this->getTag(), $only);
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'form_theme';

View File

@@ -24,7 +24,7 @@ use Twig\TokenParser\AbstractTokenParser;
*/
final class StopwatchTokenParser extends AbstractTokenParser
{
protected $stopwatchIsAvailable;
private bool $stopwatchIsAvailable;
public function __construct(bool $stopwatchIsAvailable)
{
@@ -42,7 +42,7 @@ final class StopwatchTokenParser extends AbstractTokenParser
$stream->expect(Token::BLOCK_END_TYPE);
// {% endstopwatch %}
$body = $this->parser->subparse([$this, 'decideStopwatchEnd'], true);
$body = $this->parser->subparse($this->decideStopwatchEnd(...), true);
$stream->expect(Token::BLOCK_END_TYPE);
if ($this->stopwatchIsAvailable) {

View File

@@ -23,9 +23,6 @@ use Twig\TokenParser\AbstractTokenParser;
*/
final class TransDefaultDomainTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$expr = $this->parser->getExpressionParser()->parseExpression();
@@ -35,9 +32,6 @@ final class TransDefaultDomainTokenParser extends AbstractTokenParser
return new TransDefaultDomainNode($expr, $token->getLine(), $this->getTag());
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'trans_default_domain';

View File

@@ -27,9 +27,6 @@ use Twig\TokenParser\AbstractTokenParser;
*/
final class TransTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$lineno = $token->getLine();
@@ -69,7 +66,7 @@ final class TransTokenParser extends AbstractTokenParser
// {% trans %}message{% endtrans %}
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideTransFork'], true);
$body = $this->parser->subparse($this->decideTransFork(...), true);
if (!$body instanceof TextNode && !$body instanceof AbstractExpression) {
throw new SyntaxError('A message inside a trans tag must be a simple text.', $body->getTemplateLine(), $stream->getSourceContext());
@@ -85,9 +82,6 @@ final class TransTokenParser extends AbstractTokenParser
return $token->test(['endtrans']);
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'trans';

View File

@@ -11,6 +11,7 @@
namespace Symfony\Bridge\Twig\Translation;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
@@ -29,19 +30,15 @@ class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface
{
/**
* Default domain for found messages.
*
* @var string
*/
private $defaultDomain = 'messages';
private string $defaultDomain = 'messages';
/**
* Prefix for found message.
*
* @var string
*/
private $prefix = '';
private string $prefix = '';
private $twig;
private Environment $twig;
public function __construct(Environment $twig)
{
@@ -49,30 +46,33 @@ class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface
}
/**
* {@inheritdoc}
* @return void
*/
public function extract($resource, MessageCatalogue $catalogue)
{
foreach ($this->extractFiles($resource) as $file) {
try {
$this->extractTemplate(file_get_contents($file->getPathname()), $catalogue);
} catch (Error $e) {
} catch (Error) {
// ignore errors, these should be fixed by using the linter
}
}
}
/**
* {@inheritdoc}
* @return void
*/
public function setPrefix(string $prefix)
{
$this->prefix = $prefix;
}
/**
* @return void
*/
protected function extractTemplate(string $template, MessageCatalogue $catalogue)
{
$visitor = $this->twig->getExtension('Symfony\Bridge\Twig\Extension\TranslationExtension')->getTranslationNodeVisitor();
$visitor = $this->twig->getExtension(TranslationExtension::class)->getTranslationNodeVisitor();
$visitor->enable();
$this->twig->parse($this->twig->tokenize(new Source($template, '')));
@@ -84,18 +84,12 @@ class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface
$visitor->disable();
}
/**
* @return bool
*/
protected function canBeExtracted(string $file)
protected function canBeExtracted(string $file): bool
{
return $this->isFile($file) && 'twig' === pathinfo($file, \PATHINFO_EXTENSION);
}
/**
* {@inheritdoc}
*/
protected function extractFromDirectory($directory)
protected function extractFromDirectory($directory): iterable
{
$finder = new Finder();

View File

@@ -24,7 +24,9 @@ class UndefinedCallableHandler
{
private const FILTER_COMPONENTS = [
'humanize' => 'form',
'form_encode_currency' => 'form',
'trans' => 'translation',
'sanitize_html' => 'html-sanitizer',
'yaml_encode' => 'yaml',
'yaml_dump' => 'yaml',
];
@@ -32,6 +34,7 @@ class UndefinedCallableHandler
private const FUNCTION_COMPONENTS = [
'asset' => 'asset',
'asset_version' => 'asset',
'importmap' => 'asset-mapper',
'dump' => 'debug-bundle',
'encore_entry_link_tags' => 'webpack-encore-bundle',
'encore_entry_script_tags' => 'webpack-encore-bundle',
@@ -46,6 +49,13 @@ class UndefinedCallableHandler
'form_start' => 'form',
'form_end' => 'form',
'csrf_token' => 'form',
'form_parent' => 'form',
'field_name' => 'form',
'field_value' => 'form',
'field_label' => 'form',
'field_help' => 'form',
'field_errors' => 'form',
'field_choices' => 'form',
'logout_url' => 'security-http',
'logout_path' => 'security-http',
'is_granted' => 'security-core',
@@ -57,11 +67,15 @@ class UndefinedCallableHandler
'prerender' => 'web-link',
'workflow_can' => 'workflow',
'workflow_transitions' => 'workflow',
'workflow_transition' => 'workflow',
'workflow_has_marked_place' => 'workflow',
'workflow_marked_places' => 'workflow',
'workflow_metadata' => 'workflow',
'workflow_transition_blockers' => 'workflow',
];
private const FULL_STACK_ENABLE = [
'html-sanitizer' => 'enable "framework.html_sanitizer"',
'form' => 'enable "framework.form"',
'security-core' => 'add the "SecurityBundle"',
'security-http' => 'add the "SecurityBundle"',
@@ -69,10 +83,7 @@ class UndefinedCallableHandler
'workflow' => 'enable "framework.workflows"',
];
/**
* @return TwigFilter|false
*/
public static function onUndefinedFilter(string $name)
public static function onUndefinedFilter(string $name): TwigFilter|false
{
if (!isset(self::FILTER_COMPONENTS[$name])) {
return false;
@@ -81,17 +92,14 @@ class UndefinedCallableHandler
throw new SyntaxError(self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name]));
}
/**
* @return TwigFunction|false
*/
public static function onUndefinedFunction(string $name)
public static function onUndefinedFunction(string $name): TwigFunction|false
{
if (!isset(self::FUNCTION_COMPONENTS[$name])) {
return false;
}
if ('webpack-encore-bundle' === self::FUNCTION_COMPONENTS[$name]) {
return new TwigFunction($name, static function () { return ''; });
return new TwigFunction($name, static fn () => '');
}
throw new SyntaxError(self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name]));

View File

@@ -16,38 +16,40 @@
}
],
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16",
"symfony/translation-contracts": "^1.1|^2|^3",
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/translation-contracts": "^2.5|^3",
"twig/twig": "^2.13|^3.0.4"
},
"require-dev": {
"doctrine/annotations": "^1.12|^2",
"egulias/email-validator": "^2.1.10|^3|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/asset": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/finder": "^4.4|^5.0|^6.0",
"symfony/form": "^5.4.21|^6.2.7",
"symfony/http-foundation": "^5.3|^6.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0",
"symfony/intl": "^4.4|^5.0|^6.0",
"symfony/mime": "^5.2|^6.0",
"symfony/asset": "^5.4|^6.0|^7.0",
"symfony/asset-mapper": "^6.3|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/finder": "^5.4|^6.0|^7.0",
"symfony/form": "^6.4|^7.0",
"symfony/html-sanitizer": "^6.1|^7.0",
"symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/intl": "^5.4|^6.0|^7.0",
"symfony/mime": "^6.2|^7.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/property-info": "^4.4|^5.1|^6.0",
"symfony/routing": "^4.4|^5.0|^6.0",
"symfony/translation": "^5.2|^6.0",
"symfony/yaml": "^4.4|^5.0|^6.0",
"symfony/property-info": "^5.4|^6.0|^7.0",
"symfony/routing": "^5.4|^6.0|^7.0",
"symfony/translation": "^6.1|^7.0",
"symfony/yaml": "^5.4|^6.0|^7.0",
"symfony/security-acl": "^2.8|^3.0",
"symfony/security-core": "^4.4|^5.0|^6.0",
"symfony/security-csrf": "^4.4|^5.0|^6.0",
"symfony/security-http": "^4.4|^5.0|^6.0",
"symfony/serializer": "^5.2|^6.0",
"symfony/stopwatch": "^4.4|^5.0|^6.0",
"symfony/console": "^5.3|^6.0",
"symfony/expression-language": "^4.4|^5.0|^6.0",
"symfony/web-link": "^4.4|^5.0|^6.0",
"symfony/workflow": "^5.2|^6.0",
"symfony/security-core": "^5.4|^6.0|^7.0",
"symfony/security-csrf": "^5.4|^6.0|^7.0",
"symfony/security-http": "^5.4|^6.0|^7.0",
"symfony/serializer": "^6.4|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/web-link": "^5.4|^6.0|^7.0",
"symfony/workflow": "^5.4|^6.0|^7.0",
"twig/cssinliner-extra": "^2.12|^3",
"twig/inky-extra": "^2.12|^3",
"twig/markdown-extra": "^2.12|^3"
@@ -55,28 +57,14 @@
"conflict": {
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/console": "<5.3",
"symfony/form": "<5.4.21|>=6,<6.2.7",
"symfony/http-foundation": "<5.3",
"symfony/http-kernel": "<4.4",
"symfony/translation": "<5.2",
"symfony/workflow": "<5.2"
},
"suggest": {
"symfony/finder": "",
"symfony/asset": "For using the AssetExtension",
"symfony/form": "For using the FormExtension",
"symfony/http-kernel": "For using the HttpKernelExtension",
"symfony/routing": "For using the RoutingExtension",
"symfony/translation": "For using the TranslationExtension",
"symfony/yaml": "For using the YamlExtension",
"symfony/security-core": "For using the SecurityExtension",
"symfony/security-csrf": "For using the CsrfExtension",
"symfony/security-http": "For using the LogoutUrlExtension",
"symfony/stopwatch": "For using the StopwatchExtension",
"symfony/var-dumper": "For using the DumpExtension",
"symfony/expression-language": "For using the ExpressionExtension",
"symfony/web-link": "For using the WebLinkExtension"
"symfony/console": "<5.4",
"symfony/form": "<6.3",
"symfony/http-foundation": "<5.4",
"symfony/http-kernel": "<6.4",
"symfony/mime": "<6.2",
"symfony/serializer": "<6.4",
"symfony/translation": "<5.4",
"symfony/workflow": "<5.4"
},
"autoload": {
"psr-4": { "Symfony\\Bridge\\Twig\\": "" },