login consent app sql

This commit is contained in:
2022-05-03 08:54:45 +02:00
parent e7253acfd8
commit f9a6535906
1652 changed files with 187600 additions and 45 deletions

View File

@ -0,0 +1,182 @@
<?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;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Exposes some Symfony parameters and services as an "app" global variable.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class AppVariable
{
private $tokenStorage;
private $requestStack;
private $environment;
private $debug;
public function setTokenStorage(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function setRequestStack(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function setEnvironment(string $environment)
{
$this->environment = $environment;
}
public function setDebug(bool $debug)
{
$this->debug = $debug;
}
/**
* Returns the current token.
*
* @return TokenInterface|null
*
* @throws \RuntimeException When the TokenStorage is not available
*/
public function getToken()
{
if (null === $tokenStorage = $this->tokenStorage) {
throw new \RuntimeException('The "app.token" variable is not available.');
}
return $tokenStorage->getToken();
}
/**
* Returns the current user.
*
* @return UserInterface|null
*
* @see TokenInterface::getUser()
*/
public function getUser()
{
if (null === $tokenStorage = $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;
}
/**
* Returns the current request.
*
* @return Request|null
*/
public function getRequest()
{
if (null === $this->requestStack) {
throw new \RuntimeException('The "app.request" variable is not available.');
}
return $this->requestStack->getCurrentRequest();
}
/**
* Returns the current session.
*
* @return Session|null
*/
public function getSession()
{
if (null === $this->requestStack) {
throw new \RuntimeException('The "app.session" variable is not available.');
}
$request = $this->getRequest();
return $request && $request->hasSession() ? $request->getSession() : null;
}
/**
* Returns the current app environment.
*
* @return string
*/
public function getEnvironment()
{
if (null === $this->environment) {
throw new \RuntimeException('The "app.environment" variable is not available.');
}
return $this->environment;
}
/**
* Returns the current app debug mode.
*
* @return bool
*/
public function getDebug()
{
if (null === $this->debug) {
throw new \RuntimeException('The "app.debug" variable is not available.');
}
return $this->debug;
}
/**
* 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)
{
try {
if (null === $session = $this->getSession()) {
return [];
}
} catch (\RuntimeException $e) {
return [];
}
if (null === $types || '' === $types || [] === $types) {
return $session->getFlashBag()->all();
}
if (\is_string($types)) {
return $session->getFlashBag()->get($types);
}
$result = [];
foreach ($types as $type) {
$result[$type] = $session->getFlashBag()->get($type);
}
return $result;
}
}

169
vendor/symfony/twig-bridge/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,169 @@
CHANGELOG
=========
5.4
---
* Add `github` format & autodetection to render errors as annotations when
running the Twig linter command in a Github Actions environment.
5.3
---
* Add a new `markAsPublic` method on `NotificationEmail` to change the `importance` context option to null after creation
* Add a new `fragment_uri()` helper to generate the URI of a fragment
* Add support of Bootstrap 5 for form theming
* Add a new `serialize` filter to serialize objects using the Serializer component
5.2.0
-----
* added the `impersonation_exit_url()` and `impersonation_exit_path()` functions. They return a URL that allows to switch back to the original user.
* added the `workflow_transition()` function to easily retrieve a specific transition object
* added support for translating `TranslatableInterface` objects
* added the `t()` function to easily create `TranslatableMessage` objects
* Added support for extracting messages from the `t()` function
* Added `field_*` Twig functions to access string values from Form fields
* changed the `importance` context option of `NotificationEmail` to allow `null`
5.0.0
-----
* removed `TwigEngine` class, use `\Twig\Environment` instead.
* removed `transChoice` filter and token
* `HttpFoundationExtension` requires a `UrlHelper` on instantiation
* removed support for implicit STDIN usage in the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit.
* added form theme for Foundation 6
* added support for Foundation 6 switches: add the `switch-input` class to the attributes of a `CheckboxType`
4.4.0
-----
* added a new `TwigErrorRenderer` for `html` format, integrated with the `ErrorHandler` component
* marked all classes extending twig as `@final`
* deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the
`DebugCommand::__construct()` method, swap the variables position.
* the `LintCommand` lints all the templates stored in all configured Twig paths if none argument is provided
* deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit.
* added `--show-deprecations` option to the `lint:twig` command
* added support for Bootstrap4 switches: add the `switch-custom` class to the label attributes of a `CheckboxType`
* Marked the `TwigDataCollector` class as `@final`.
4.3.0
-----
* added the `form_parent()` function that allows to reliably retrieve the parent form in Twig templates
* added the `workflow_transition_blockers()` function
* deprecated the `$requestStack` and `$requestContext` arguments of the
`HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper`
instance as the only argument instead
4.2.0
-----
* add bundle name suggestion on wrongly overridden templates paths
* added `name` argument in `debug:twig` command and changed `filter` argument as `--filter` option
* deprecated the `transchoice` tag and filter, use the `trans` ones instead with a `%count%` parameter
4.1.0
-----
* add a `workflow_metadata` function
3.4.0
-----
* added an `only` keyword to `form_theme` tag to disable usage of default themes when rendering a form
* deprecated `Symfony\Bridge\Twig\Form\TwigRenderer`
* deprecated `DebugCommand::set/getTwigEnvironment`. Pass an instance of
`Twig\Environment` as first argument of the constructor instead
* deprecated `LintCommand::set/getTwigEnvironment`. Pass an instance of
`Twig\Environment` as first argument of the constructor instead
3.3.0
-----
* added a `workflow_has_marked_place` function
* added a `workflow_marked_places` function
3.2.0
-----
* added `AppVariable::getToken()`
* Deprecated the possibility to inject the Form `TwigRenderer` into the `FormExtension`.
* [BC BREAK] Registering the `FormExtension` without configuring a runtime loader for the `TwigRenderer`
doesn't work anymore.
Before:
```php
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
// ...
$rendererEngine = new TwigRendererEngine(['form_div_layout.html.twig']);
$rendererEngine->setEnvironment($twig);
$twig->addExtension(new FormExtension(new TwigRenderer($rendererEngine, $csrfTokenManager)));
```
After:
```php
// ...
$rendererEngine = new TwigRendererEngine(['form_div_layout.html.twig'], $twig);
// require Twig 1.30+
$twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryRuntimeLoader([
TwigRenderer::class => function () use ($rendererEngine, $csrfTokenManager) {
return new TwigRenderer($rendererEngine, $csrfTokenManager);
},
]));
$twig->addExtension(new FormExtension());
```
* Deprecated the `TwigRendererEngineInterface` interface.
* added WorkflowExtension (provides `workflow_can` and `workflow_transitions`)
2.7.0
-----
* added LogoutUrlExtension (provides `logout_url` and `logout_path`)
* added an HttpFoundation extension (provides the `absolute_url` and the `relative_path` functions)
* added AssetExtension (provides the `asset` and `asset_version` functions)
* Added possibility to extract translation messages from a file or files besides extracting from a directory
2.5.0
-----
* moved command `twig:lint` from `TwigBundle`
2.4.0
-----
* added stopwatch tag to time templates with the WebProfilerBundle
2.3.0
-----
* added helpers form(), form_start() and form_end()
* deprecated form_enctype() in favor of form_start()
2.2.0
-----
* added a `controller` function to help generating controller references
* added a `render_esi` and a `render_hinclude` function
* [BC BREAK] restricted the `render` tag to only accept URIs or ControllerReference instances (the signature changed)
* added a `render` function to render a request
* The `app` global variable is now injected even when using the twig service directly.
* Added an optional parameter to the `path` and `url` function which allows to generate
relative paths (e.g. "../parent-file") and scheme-relative URLs (e.g. "//example.com/dir/file").
2.1.0
-----
* added global variables access in a form theme
* added TwigEngine
* added TwigExtractor
* added a csrf_token function
* added a way to specify a default domain for a Twig template (via the
'trans_default_domain' tag)

View File

@ -0,0 +1,600 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
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\Finder\Finder;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Twig\Environment;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
/**
* Lists twig functions, filters, globals and tests present in the current project.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class DebugCommand extends Command
{
protected static $defaultName = 'debug:twig';
protected static $defaultDescription = 'Show a list of twig functions, filters, globals and tests';
private $twig;
private $projectDir;
private $bundlesMetadata;
private $twigDefaultPath;
private $filesystemLoaders;
private $fileLinkFormatter;
public function __construct(Environment $twig, string $projectDir = null, array $bundlesMetadata = [], string $twigDefaultPath = null, FileLinkFormatter $fileLinkFormatter = null)
{
parent::__construct();
$this->twig = $twig;
$this->projectDir = $projectDir;
$this->bundlesMetadata = $bundlesMetadata;
$this->twigDefaultPath = $twigDefaultPath;
$this->fileLinkFormatter = $fileLinkFormatter;
}
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'),
])
->setDescription(self::$defaultDescription)
->setHelp(<<<'EOF'
The <info>%command.name%</info> command outputs a list of twig functions,
filters, globals and tests.
<info>php %command.full_name%</info>
The command lists all functions, filters, etc.
<info>php %command.full_name% @Twig/Exception/error.html.twig</info>
The command lists all paths that match the given template name.
<info>php %command.full_name% --filter=date</info>
The command lists everything that contains the word date.
<info>php %command.full_name% --format=json</info>
The command lists everything in a machine readable json format.
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
$filter = $input->getOption('filter');
if (null !== $name && [] === $this->getFilesystemLoaders()) {
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')));
}
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('name')) {
$suggestions->suggestValues(array_keys($this->getLoaderPaths()));
}
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['text', 'json']);
}
}
private function displayPathsText(SymfonyStyle $io, string $name)
{
$file = new \ArrayIterator($this->findTemplateFiles($name));
$paths = $this->getLoaderPaths($name);
$io->section('Matched File');
if ($file->valid()) {
if ($fileLink = $this->getFileLink($file->key())) {
$io->block($file->current(), 'OK', sprintf('fg=black;bg=green;href=%s', $fileLink), ' ', true);
} else {
$io->success($file->current());
}
$file->next();
if ($file->valid()) {
$io->section('Overridden Files');
do {
if ($fileLink = $this->getFileLink($file->key())) {
$io->text(sprintf('* <href=%s>%s</>', $fileLink, $file->current()));
} else {
$io->text(sprintf('* %s', $file->current()));
}
$file->next();
} while ($file->valid());
}
} else {
$alternatives = [];
if ($paths) {
$shortnames = [];
$dirs = [];
foreach (current($paths) as $path) {
$dirs[] = $this->isAbsolutePath($path) ? $path : $this->projectDir.'/'.$path;
}
foreach (Finder::create()->files()->followLinks()->in($dirs) as $file) {
$shortnames[] = str_replace('\\', '/', $file->getRelativePathname());
}
[$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);
}
}
$this->error($io, sprintf('Template name "%s" not found', $name), $alternatives);
}
$io->section('Configured Paths');
if ($paths) {
$io->table(['Namespace', 'Paths'], $this->buildTableRows($paths));
} else {
$alternatives = [];
$namespace = $this->parseTemplateName($name)[0];
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$message = 'No template paths configured for your application';
} else {
$message = sprintf('No template paths configured for "@%s" namespace', $namespace);
foreach ($this->getFilesystemLoaders() as $loader) {
$namespaces = $loader->getNamespaces();
foreach ($this->findAlternatives($namespace, $namespaces) as $namespace) {
$alternatives[] = '@'.$namespace;
}
}
}
$this->error($io, $message, $alternatives);
if (!$alternatives && $paths = $this->getLoaderPaths()) {
$io->table(['Namespace', 'Paths'], $this->buildTableRows($paths));
}
}
}
private function displayPathsJson(SymfonyStyle $io, string $name)
{
$files = $this->findTemplateFiles($name);
$paths = $this->getLoaderPaths($name);
if ($files) {
$data['matched_file'] = array_shift($files);
if ($files) {
$data['overridden_files'] = $files;
}
} else {
$data['matched_file'] = sprintf('Template name "%s" not found', $name);
}
$data['loader_paths'] = $paths;
$io->writeln(json_encode($data));
}
private function displayGeneralText(SymfonyStyle $io, string $filter = null)
{
$decorated = $io->isDecorated();
$types = ['functions', 'filters', 'tests', 'globals'];
foreach ($types as $index => $type) {
$items = [];
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
if (!$filter || str_contains($name, $filter)) {
$items[$name] = $name.$this->getPrettyMetadata($type, $entity, $decorated);
}
}
if (!$items) {
continue;
}
$io->section(ucfirst($type));
ksort($items);
$io->listing($items);
}
if (!$filter && $paths = $this->getLoaderPaths()) {
$io->section('Loader Paths');
$io->table(['Namespace', 'Paths'], $this->buildTableRows($paths));
}
if ($wrongBundles = $this->findWrongBundleOverrides()) {
foreach ($this->buildWarningMessages($wrongBundles) as $message) {
$io->warning($message);
}
}
}
private function displayGeneralJson(SymfonyStyle $io, ?string $filter)
{
$decorated = $io->isDecorated();
$types = ['functions', 'filters', 'tests', 'globals'];
$data = [];
foreach ($types as $type) {
foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) {
if (!$filter || str_contains($name, $filter)) {
$data[$type][$name] = $this->getMetadata($type, $entity);
}
}
}
if (isset($data['tests'])) {
$data['tests'] = array_keys($data['tests']);
}
if (!$filter && $paths = $this->getLoaderPaths($filter)) {
$data['loader_paths'] = $paths;
}
if ($wrongBundles = $this->findWrongBundleOverrides()) {
$data['warnings'] = $this->buildWarningMessages($wrongBundles);
}
$data = json_encode($data, \JSON_PRETTY_PRINT);
$io->writeln($decorated ? OutputFormatter::escape($data) : $data);
}
private function getLoaderPaths(string $name = null): array
{
$loaderPaths = [];
foreach ($this->getFilesystemLoaders() as $loader) {
$namespaces = $loader->getNamespaces();
if (null !== $name) {
$namespace = $this->parseTemplateName($name)[0];
$namespaces = array_intersect([$namespace], $namespaces);
}
foreach ($namespaces as $namespace) {
$paths = array_map([$this, 'getRelativePath'], $loader->getPaths($namespace));
if (FilesystemLoader::MAIN_NAMESPACE === $namespace) {
$namespace = '(None)';
} else {
$namespace = '@'.$namespace;
}
$loaderPaths[$namespace] = array_merge($loaderPaths[$namespace] ?? [], $paths);
}
}
return $loaderPaths;
}
private function getMetadata(string $type, $entity)
{
if ('globals' === $type) {
return $entity;
}
if ('tests' === $type) {
return null;
}
if ('functions' === $type || 'filters' === $type) {
$cb = $entity->getCallable();
if (null === $cb) {
return null;
}
if (\is_array($cb)) {
if (!method_exists($cb[0], $cb[1])) {
return null;
}
$refl = new \ReflectionMethod($cb[0], $cb[1]);
} elseif (\is_object($cb) && method_exists($cb, '__invoke')) {
$refl = new \ReflectionMethod($cb, '__invoke');
} elseif (\function_exists($cb)) {
$refl = new \ReflectionFunction($cb);
} elseif (\is_string($cb) && preg_match('{^(.+)::(.+)$}', $cb, $m) && method_exists($m[1], $m[2])) {
$refl = new \ReflectionMethod($m[1], $m[2]);
} else {
throw new \UnexpectedValueException('Unsupported callback type.');
}
$args = $refl->getParameters();
// filter out context/environment args
if ($entity->needsEnvironment()) {
array_shift($args);
}
if ($entity->needsContext()) {
array_shift($args);
}
if ('filters' === $type) {
// remove the value the filter is applied on
array_shift($args);
}
// format args
$args = array_map(function (\ReflectionParameter $param) {
if ($param->isDefaultValueAvailable()) {
return $param->getName().' = '.json_encode($param->getDefaultValue());
}
return $param->getName();
}, $args);
return $args;
}
return null;
}
private function getPrettyMetadata(string $type, $entity, bool $decorated): ?string
{
if ('tests' === $type) {
return '';
}
try {
$meta = $this->getMetadata($type, $entity);
if (null === $meta) {
return '(unknown?)';
}
} catch (\UnexpectedValueException $e) {
return sprintf(' <error>%s</error>', $decorated ? OutputFormatter::escape($e->getMessage()) : $e->getMessage());
}
if ('globals' === $type) {
if (\is_object($meta)) {
return ' = object('.\get_class($meta).')';
}
$description = substr(@json_encode($meta), 0, 50);
return sprintf(' = %s', $decorated ? OutputFormatter::escape($description) : $description);
}
if ('functions' === $type) {
return '('.implode(', ', $meta).')';
}
if ('filters' === $type) {
return $meta ? '('.implode(', ', $meta).')' : '';
}
return null;
}
private function findWrongBundleOverrides(): array
{
$alternatives = [];
$bundleNames = [];
if ($this->twigDefaultPath && $this->projectDir) {
$folders = glob($this->twigDefaultPath.'/bundles/*', \GLOB_ONLYDIR);
$relativePath = ltrim(substr($this->twigDefaultPath.'/bundles/', \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
$bundleNames = array_reduce($folders, function ($carry, $absolutePath) use ($relativePath) {
if (str_starts_with($absolutePath, $this->projectDir)) {
$name = basename($absolutePath);
$path = ltrim($relativePath.$name, \DIRECTORY_SEPARATOR);
$carry[$name] = $path;
}
return $carry;
}, $bundleNames);
}
if ($notFoundBundles = array_diff_key($bundleNames, $this->bundlesMetadata)) {
$alternatives = [];
foreach ($notFoundBundles as $notFoundBundle => $path) {
$alternatives[$path] = $this->findAlternatives($notFoundBundle, array_keys($this->bundlesMetadata));
}
}
return $alternatives;
}
private function buildWarningMessages(array $wrongBundles): array
{
$messages = [];
foreach ($wrongBundles as $path => $alternatives) {
$message = sprintf('Path "%s" not matching any bundle found', $path);
if ($alternatives) {
if (1 === \count($alternatives)) {
$message .= sprintf(", did you mean \"%s\"?\n", $alternatives[0]);
} else {
$message .= ", did you mean one of these:\n";
foreach ($alternatives as $bundle) {
$message .= sprintf(" - %s\n", $bundle);
}
}
}
$messages[] = trim($message);
}
return $messages;
}
private function error(SymfonyStyle $io, string $message, array $alternatives = []): void
{
if ($alternatives) {
if (1 === \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
$message .= "\n\nDid you mean one of these?\n ";
}
$message .= implode("\n ", $alternatives);
}
$io->block($message, null, 'fg=white;bg=red', ' ', true);
}
private function findTemplateFiles(string $name): array
{
[$namespace, $shortname] = $this->parseTemplateName($name);
$files = [];
foreach ($this->getFilesystemLoaders() as $loader) {
foreach ($loader->getPaths($namespace) as $path) {
if (!$this->isAbsolutePath($path)) {
$path = $this->projectDir.'/'.$path;
}
$filename = $path.'/'.$shortname;
if (is_file($filename)) {
if (false !== $realpath = realpath($filename)) {
$files[$realpath] = $this->getRelativePath($realpath);
} else {
$files[$filename] = $this->getRelativePath($filename);
}
}
}
}
return $files;
}
private function parseTemplateName(string $name, string $default = FilesystemLoader::MAIN_NAMESPACE): array
{
if (isset($name[0]) && '@' === $name[0]) {
if (false === ($pos = strpos($name, '/')) || $pos === \strlen($name) - 1) {
throw new InvalidArgumentException(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
}
$namespace = substr($name, 1, $pos - 1);
$shortname = substr($name, $pos + 1);
return [$namespace, $shortname];
}
return [$default, $name];
}
private function buildTableRows(array $loaderPaths): array
{
$rows = [];
$firstNamespace = true;
$prevHasSeparator = false;
foreach ($loaderPaths as $namespace => $paths) {
if (!$firstNamespace && !$prevHasSeparator && \count($paths) > 1) {
$rows[] = ['', ''];
}
$firstNamespace = false;
foreach ($paths as $path) {
$rows[] = [$namespace, $path.\DIRECTORY_SEPARATOR];
$namespace = '';
}
if (\count($paths) > 1) {
$rows[] = ['', ''];
$prevHasSeparator = true;
} else {
$prevHasSeparator = false;
}
}
if ($prevHasSeparator) {
array_pop($rows);
}
return $rows;
}
private function findAlternatives(string $name, array $collection): array
{
$alternatives = [];
foreach ($collection as $item) {
$lev = levenshtein($name, $item);
if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
}
}
$threshold = 1e3;
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
return array_keys($alternatives);
}
private function getRelativePath(string $path): string
{
if (null !== $this->projectDir && str_starts_with($path, $this->projectDir)) {
return ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR);
}
return $path;
}
private function isAbsolutePath(string $file): bool
{
return strspn($file, '/\\', 0, 1) || (\strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1)) || null !== parse_url($file, \PHP_URL_SCHEME);
}
/**
* @return FilesystemLoader[]
*/
private function getFilesystemLoaders(): array
{
if (null !== $this->filesystemLoaders) {
return $this->filesystemLoaders;
}
$this->filesystemLoaders = [];
$loader = $this->twig->getLoader();
if ($loader instanceof FilesystemLoader) {
$this->filesystemLoaders[] = $loader;
} elseif ($loader instanceof ChainLoader) {
foreach ($loader->getLoaders() as $l) {
if ($l instanceof FilesystemLoader) {
$this->filesystemLoaders[] = $l;
}
}
}
return $this->filesystemLoaders;
}
private function getFileLink(string $absolutePath): string
{
if (null === $this->fileLinkFormatter) {
return '';
}
return (string) $this->fileLinkFormatter->format($absolutePath, 1);
}
}

View File

@ -0,0 +1,296 @@
<?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\Command;
use Symfony\Component\Console\CI\GithubActionReporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
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\Finder\Finder;
use Twig\Environment;
use Twig\Error\Error;
use Twig\Loader\ArrayLoader;
use Twig\Loader\FilesystemLoader;
use Twig\Source;
/**
* Command that will validate your template syntax and output encountered errors.
*
* @author Marc Weistroff <marc.weistroff@sensiolabs.com>
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*/
class LintCommand extends Command
{
protected static $defaultName = 'lint:twig';
protected static $defaultDescription = 'Lint a Twig template and outputs encountered errors';
private $twig;
/**
* @var string|null
*/
private $format;
public function __construct(Environment $twig)
{
parent::__construct();
$this->twig = $twig;
}
protected function configure()
{
$this
->setDescription(self::$defaultDescription)
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
->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'
The <info>%command.name%</info> command lints a template and outputs to STDOUT
the first encountered syntax error.
You can validate the syntax of contents passed from STDIN:
<info>cat filename | php %command.full_name% -</info>
Or the syntax of a file:
<info>php %command.full_name% filename</info>
Or of a whole directory:
<info>php %command.full_name% dirname</info>
<info>php %command.full_name% dirname --format=json</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$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';
}
if (['-'] === $filenames) {
return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]);
}
if (!$filenames) {
$loader = $this->twig->getLoader();
if ($loader instanceof FilesystemLoader) {
$paths = [];
foreach ($loader->getNamespaces() as $namespace) {
$paths[] = $loader->getPaths($namespace);
}
$filenames = array_merge(...$paths);
}
if (!$filenames) {
throw new RuntimeException('Please provide a filename or pipe template content to STDIN.');
}
}
if ($showDeprecations) {
$prevErrorHandler = set_error_handler(static function ($level, $message, $file, $line) use (&$prevErrorHandler) {
if (\E_USER_DEPRECATED === $level) {
$templateLine = 0;
if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) {
$templateLine = $matches[1];
}
throw new Error($message, $templateLine);
}
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
});
}
try {
$filesInfo = $this->getFilesInfo($filenames);
} finally {
if ($showDeprecations) {
restore_error_handler();
}
}
return $this->display($input, $output, $io, $filesInfo);
}
private function getFilesInfo(array $filenames): array
{
$filesInfo = [];
foreach ($filenames as $filename) {
foreach ($this->findFiles($filename) as $file) {
$filesInfo[] = $this->validate(file_get_contents($file), $file);
}
}
return $filesInfo;
}
protected function findFiles(string $filename)
{
if (is_file($filename)) {
return [$filename];
} elseif (is_dir($filename)) {
return Finder::create()->files()->in($filename)->name('*.twig');
}
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
}
private function validate(string $template, string $file): array
{
$realLoader = $this->twig->getLoader();
try {
$temporaryLoader = new ArrayLoader([$file => $template]);
$this->twig->setLoader($temporaryLoader);
$nodeTree = $this->twig->parse($this->twig->tokenize(new Source($template, $file)));
$this->twig->compile($nodeTree);
$this->twig->setLoader($realLoader);
} catch (Error $e) {
$this->twig->setLoader($realLoader);
return ['template' => $template, 'file' => $file, 'line' => $e->getTemplateLine(), 'valid' => false, 'exception' => $e];
}
return ['template' => $template, 'file' => $file, 'valid' => true];
}
private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files)
{
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')));
}
}
private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false)
{
$errors = 0;
$githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($output) : null;
foreach ($filesInfo as $info) {
if ($info['valid'] && $output->isVerbose()) {
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
} elseif (!$info['valid']) {
++$errors;
$this->renderException($io, $info['template'], $info['exception'], $info['file'], $githubReporter);
}
}
if (0 === $errors) {
$io->success(sprintf('All %d Twig files contain valid syntax.', \count($filesInfo)));
} else {
$io->warning(sprintf('%d Twig files have valid syntax and %d contain errors.', \count($filesInfo) - $errors, $errors));
}
return min($errors, 1);
}
private function displayJson(OutputInterface $output, array $filesInfo)
{
$errors = 0;
array_walk($filesInfo, function (&$v) use (&$errors) {
$v['file'] = (string) $v['file'];
unset($v['template']);
if (!$v['valid']) {
$v['message'] = $v['exception']->getMessage();
unset($v['exception']);
++$errors;
}
});
$output->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
return min($errors, 1);
}
private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null, GithubActionReporter $githubReporter = null)
{
$line = $exception->getTemplateLine();
if ($githubReporter) {
$githubReporter->error($exception->getRawMessage(), $file, $line <= 0 ? null : $line);
}
if ($file) {
$output->text(sprintf('<error> ERROR </error> in %s (line %s)', $file, $line));
} else {
$output->text(sprintf('<error> ERROR </error> (line %s)', $line));
}
// If the line is not known (this might happen for deprecations if we fail at detecting the line for instance),
// we render the message without context, to ensure the message is displayed.
if ($line <= 0) {
$output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
return;
}
foreach ($this->getContext($template, $line) as $lineNumber => $code) {
$output->text(sprintf(
'%s %-6s %s',
$lineNumber === $line ? '<error> >> </error>' : ' ',
$lineNumber,
$code
));
if ($lineNumber === $line) {
$output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
}
}
}
private function getContext(string $template, int $line, int $context = 3)
{
$lines = explode("\n", $template);
$position = max(0, $line - $context);
$max = min(\count($lines), $line - 1 + $context);
$result = [];
while ($position < $max) {
$result[$position + 1] = $lines[$position];
++$position;
}
return $result;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestOptionValuesFor('format')) {
$suggestions->suggestValues(['txt', 'json', 'github']);
}
}
}

View File

@ -0,0 +1,203 @@
<?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\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Markup;
use Twig\Profiler\Dumper\HtmlDumper;
use Twig\Profiler\Profile;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class TwigDataCollector extends DataCollector implements LateDataCollectorInterface
{
private $profile;
private $twig;
private $computed;
public function __construct(Profile $profile, Environment $twig = null)
{
$this->profile = $profile;
$this->twig = $twig;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->profile->reset();
$this->computed = null;
$this->data = [];
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$this->data['profile'] = serialize($this->profile);
$this->data['template_paths'] = [];
if (null === $this->twig) {
return;
}
$templateFinder = function (Profile $profile) use (&$templateFinder) {
if ($profile->isTemplate()) {
try {
$template = $this->twig->load($name = $profile->getName());
} catch (LoaderError $e) {
$template = null;
}
if (null !== $template && '' !== $path = $template->getSourceContext()->getPath()) {
$this->data['template_paths'][$name] = $path;
}
}
foreach ($profile as $p) {
$templateFinder($p);
}
};
$templateFinder($this->profile);
}
public function getTime()
{
return $this->getProfile()->getDuration() * 1000;
}
public function getTemplateCount()
{
return $this->getComputedData('template_count');
}
public function getTemplatePaths()
{
return $this->data['template_paths'];
}
public function getTemplates()
{
return $this->getComputedData('templates');
}
public function getBlockCount()
{
return $this->getComputedData('block_count');
}
public function getMacroCount()
{
return $this->getComputedData('macro_count');
}
public function getHtmlCallGraph()
{
$dumper = new HtmlDumper();
$dump = $dumper->dump($this->getProfile());
// needed to remove the hardcoded CSS styles
$dump = str_replace([
'<span style="background-color: #ffd">',
'<span style="color: #d44">',
'<span style="background-color: #dfd">',
'<span style="background-color: #ddf">',
], [
'<span class="status-warning">',
'<span class="status-error">',
'<span class="status-success">',
'<span class="status-info">',
], $dump);
return new Markup($dump, 'UTF-8');
}
public function getProfile()
{
if (null === $this->profile) {
$this->profile = unserialize($this->data['profile'], ['allowed_classes' => ['Twig_Profiler_Profile', 'Twig\Profiler\Profile']]);
}
return $this->profile;
}
private function getComputedData(string $index)
{
if (null === $this->computed) {
$this->computed = $this->computeData($this->getProfile());
}
return $this->computed[$index];
}
private function computeData(Profile $profile)
{
$data = [
'template_count' => 0,
'block_count' => 0,
'macro_count' => 0,
];
$templates = [];
foreach ($profile as $p) {
$d = $this->computeData($p);
$data['template_count'] += ($p->isTemplate() ? 1 : 0) + $d['template_count'];
$data['block_count'] += ($p->isBlock() ? 1 : 0) + $d['block_count'];
$data['macro_count'] += ($p->isMacro() ? 1 : 0) + $d['macro_count'];
if ($p->isTemplate()) {
if (!isset($templates[$p->getTemplate()])) {
$templates[$p->getTemplate()] = 1;
} else {
++$templates[$p->getTemplate()];
}
}
foreach ($d['templates'] as $template => $count) {
if (!isset($templates[$template])) {
$templates[$template] = $count;
} else {
$templates[$template] += $count;
}
}
}
$data['templates'] = $templates;
return $data;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'twig';
}
}

View File

@ -0,0 +1,90 @@
<?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\ErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Environment;
/**
* Provides the ability to render custom Twig-based HTML error pages
* in non-debug mode, otherwise falls back to HtmlErrorRenderer.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TwigErrorRenderer implements ErrorRendererInterface
{
private $twig;
private $fallbackErrorRenderer;
private $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)
{
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;
}
/**
* {@inheritdoc}
*/
public function render(\Throwable $exception): FlattenException
{
$exception = $this->fallbackErrorRenderer->render($exception);
$debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception);
if ($debug || !$template = $this->findTemplate($exception->getStatusCode())) {
return $exception;
}
return $exception->setAsString($this->twig->render($template, [
'exception' => $exception,
'status_code' => $exception->getStatusCode(),
'status_text' => $exception->getStatusText(),
]));
}
public static function isDebug(RequestStack $requestStack, bool $debug): \Closure
{
return static function () use ($requestStack, $debug): bool {
if (!$request = $requestStack->getCurrentRequest()) {
return $debug;
}
return $debug && $request->attributes->getBoolean('showException', true);
};
}
private function findTemplate(int $statusCode): ?string
{
$template = sprintf('@Twig/Exception/error%s.html.twig', $statusCode);
if ($this->twig->getLoader()->exists($template)) {
return $template;
}
$template = '@Twig/Exception/error.html.twig';
if ($this->twig->getLoader()->exists($template)) {
return $template;
}
return null;
}
}

View File

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

View File

@ -0,0 +1,246 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
/**
* Twig extension relate to PHP code and used by the profiler and the default exception templates.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class CodeExtension extends AbstractExtension
{
private $fileLinkFormat;
private $charset;
private $projectDir;
/**
* @param string|FileLinkFormatter $fileLinkFormat The format for links to source files
*/
public function __construct($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']]),
new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['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']),
];
}
public function abbrClass(string $class): string
{
$parts = explode('\\', $class);
$short = array_pop($parts);
return sprintf('<abbr title="%s">%s</abbr>', $class, $short);
}
public function abbrMethod(string $method): string
{
if (str_contains($method, '::')) {
[$class, $method] = explode('::', $method, 2);
$result = sprintf('%s::%s()', $this->abbrClass($class), $method);
} elseif ('Closure' === $method) {
$result = sprintf('<abbr title="%s">%1$s</abbr>', $method);
} else {
$result = sprintf('<abbr title="%s">%1$s</abbr>()', $method);
}
return $result;
}
/**
* Formats an array as a string.
*/
public function formatArgs(array $args): string
{
$result = [];
foreach ($args as $key => $item) {
if ('object' === $item[0]) {
$parts = explode('\\', $item[1]);
$short = array_pop($parts);
$formattedValue = sprintf('<em>object</em>(<abbr title="%s">%s</abbr>)', $item[1], $short);
} elseif ('array' === $item[0]) {
$formattedValue = sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
} elseif ('null' === $item[0]) {
$formattedValue = '<em>null</em>';
} elseif ('boolean' === $item[0]) {
$formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>';
} elseif ('resource' === $item[0]) {
$formattedValue = '<em>resource</em>';
} else {
$formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
}
$result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
}
return implode(', ', $result);
}
/**
* Formats an array as a string.
*/
public function formatArgsAsText(array $args): string
{
return strip_tags($this->formatArgs($args));
}
/**
* Returns an excerpt of a code file around the given line number.
*/
public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?string
{
if (is_file($file) && is_readable($file)) {
// highlight_file could throw warnings
// see https://bugs.php.net/25725
$code = @highlight_file($file, true);
// 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);
$content = explode('<br />', $code);
$lines = [];
if (0 > $srcContext) {
$srcContext = \count($content);
}
for ($i = max($line - $srcContext, 1), $max = min($line + $srcContext, \count($content)); $i <= $max; ++$i) {
$lines[] = '<li'.($i == $line ? ' class="selected"' : '').'><a class="anchor" id="line'.$i.'"></a><code>'.self::fixCodeMarkup($content[$i - 1]).'</code></li>';
}
return '<ol start="'.max($line - $srcContext, 1).'">'.implode("\n", $lines).'</ol>';
}
return null;
}
/**
* Formats a file path.
*/
public function formatFile(string $file, int $line, string $text = null): string
{
$file = trim($file);
if (null === $text) {
$text = $file;
if (null !== $rel = $this->getFileRelative($text)) {
$rel = explode('/', $rel, 2);
$text = sprintf('<abbr title="%s%2$s">%s</abbr>%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? ''));
}
}
if (0 < $line) {
$text .= ' at line '.$line;
}
if (false !== $link = $this->getFileLink($file, $line)) {
return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', htmlspecialchars($link, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $text);
}
return $text;
}
/**
* Returns the link for a given file/line pair.
*
* @return string|false
*/
public function getFileLink(string $file, int $line)
{
if ($fmt = $this->fileLinkFormat) {
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
}
return false;
}
public function getFileRelative(string $file): ?string
{
$file = str_replace('\\', '/', $file);
if (null !== $this->projectDir && str_starts_with($file, $this->projectDir)) {
return ltrim(substr($file, \strlen($this->projectDir)), '/');
}
return null;
}
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);
}
/**
* @internal
*/
public function formatLogMessage(string $message, array $context): string
{
if ($context && str_contains($message, '{')) {
$replacements = [];
foreach ($context as $key => $val) {
if (is_scalar($val)) {
$replacements['{'.$key.'}'] = $val;
}
}
if ($replacements) {
$message = strtr($message, $replacements);
}
}
return htmlspecialchars($message, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
}
protected static function fixCodeMarkup(string $line): string
{
// </span> ending tag from previous line
$opening = strpos($line, '<span');
$closing = strpos($line, '</span>');
if (false !== $closing && (false === $opening || $closing < $opening)) {
$line = substr_replace($line, '', $closing, 7);
}
// missing </span> tag at the end of line
$opening = strpos($line, '<span');
$closing = strpos($line, '</span>');
if (false !== $opening && (false === $closing || $closing > $opening)) {
$line .= '</span>';
}
return trim($line);
}
}

View File

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

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\Security\Csrf\CsrfTokenManagerInterface;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
final class CsrfRuntime
{
private $csrfTokenManager;
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
{
$this->csrfTokenManager = $csrfTokenManager;
}
public function getCsrfToken(string $tokenId): string
{
return $this->csrfTokenManager->getToken($tokenId)->getValue();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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\Extension;
use Symfony\Component\Security\Acl\Voter\FieldVote;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* SecurityExtension exposes security context features.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SecurityExtension extends AbstractExtension
{
private $securityChecker;
private $impersonateUrlGenerator;
public function __construct(AuthorizationCheckerInterface $securityChecker = null, ImpersonateUrlGenerator $impersonateUrlGenerator = null)
{
$this->securityChecker = $securityChecker;
$this->impersonateUrlGenerator = $impersonateUrlGenerator;
}
/**
* @param mixed $object
*/
public function isGranted($role, $object = null, string $field = null): bool
{
if (null === $this->securityChecker) {
return false;
}
if (null !== $field) {
$object = new FieldVote($object, $field);
}
try {
return $this->securityChecker->isGranted($role, $object);
} catch (AuthenticationCredentialsNotFoundException $e) {
return false;
}
}
public function getImpersonateExitUrl(string $exitTo = null): string
{
if (null === $this->impersonateUrlGenerator) {
return '';
}
return $this->impersonateUrlGenerator->generateExitUrl($exitTo);
}
public function getImpersonateExitPath(string $exitTo = null): string
{
if (null === $this->impersonateUrlGenerator) {
return '';
}
return $this->impersonateUrlGenerator->generateExitPath($exitTo);
}
/**
* {@inheritdoc}
*/
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']),
];
}
}

View File

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

View File

@ -0,0 +1,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\Serializer\SerializerInterface;
use Twig\Extension\RuntimeExtensionInterface;
/**
* @author Jesse Rushlow <jr@rushlow.dev>
*/
final class SerializerRuntime implements RuntimeExtensionInterface
{
private $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
public function serialize($data, string $format = 'json', array $context = []): string
{
return $this->serializer->serialize($data, $format, $context);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,182 @@
<?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\Form;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\FormView;
use Twig\Environment;
use Twig\Template;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TwigRendererEngine extends AbstractRendererEngine
{
/**
* @var Environment
*/
private $environment;
/**
* @var Template
*/
private $template;
public function __construct(array $defaultThemes, Environment $environment)
{
parent::__construct($defaultThemes);
$this->environment = $environment;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormView $view, $resource, string $blockName, array $variables = [])
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
$context = $this->environment->mergeGlobals($variables);
ob_start();
// By contract,This method can only be called after getting the resource
// (which is passed to the method). Getting a resource for the first time
// (with an empty cache) is guaranteed to invoke loadResourcesFromTheme(),
// where the property $template is initialized.
// We do not call renderBlock here to avoid too many nested level calls
// (XDebug limits the level to 100 by default)
$this->template->displayBlock($blockName, $context, $this->resources[$cacheKey]);
return ob_get_clean();
}
/**
* Loads the cache with the resource for a given block name.
*
* This implementation eagerly loads all blocks of the themes assigned to the given view
* and all of its ancestors views. This is necessary, because Twig receives the
* list of blocks later. At that point, all blocks must already be loaded, for the
* case that the function "block()" is used in the Twig template.
*
* @see getResourceForBlock()
*
* @return bool
*/
protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName)
{
// The caller guarantees that $this->resources[$cacheKey][$block] is
// not set, but it doesn't have to check whether $this->resources[$cacheKey]
// is set. If $this->resources[$cacheKey] is set, all themes for this
// $cacheKey are already loaded (due to the eager population, see doc comment).
if (isset($this->resources[$cacheKey])) {
// As said in the previous, the caller guarantees that
// $this->resources[$cacheKey][$block] is not set. Since the themes are
// already loaded, it can only be a non-existing block.
$this->resources[$cacheKey][$blockName] = false;
return false;
}
// Recursively try to find the block in the themes assigned to $view,
// then of its parent view, then of the parent view of the parent and so on.
// When the root view is reached in this recursion, also the default
// themes are taken into account.
// Check each theme whether it contains the searched block
if (isset($this->themes[$cacheKey])) {
for ($i = \count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
$this->loadResourcesFromTheme($cacheKey, $this->themes[$cacheKey][$i]);
// CONTINUE LOADING (see doc comment)
}
}
// Check the default themes once we reach the root view without success
if (!$view->parent) {
if (!isset($this->useDefaultThemes[$cacheKey]) || $this->useDefaultThemes[$cacheKey]) {
for ($i = \count($this->defaultThemes) - 1; $i >= 0; --$i) {
$this->loadResourcesFromTheme($cacheKey, $this->defaultThemes[$i]);
// CONTINUE LOADING (see doc comment)
}
}
}
// Proceed with the themes of the parent view
if ($view->parent) {
$parentCacheKey = $view->parent->vars[self::CACHE_KEY_VAR];
if (!isset($this->resources[$parentCacheKey])) {
$this->loadResourceForBlockName($parentCacheKey, $view->parent, $blockName);
}
// EAGER CACHE POPULATION (see doc comment)
foreach ($this->resources[$parentCacheKey] as $nestedBlockName => $resource) {
if (!isset($this->resources[$cacheKey][$nestedBlockName])) {
$this->resources[$cacheKey][$nestedBlockName] = $resource;
}
}
}
// Even though we loaded the themes, it can happen that none of them
// contains the searched block
if (!isset($this->resources[$cacheKey][$blockName])) {
// Cache that we didn't find anything to speed up further accesses
$this->resources[$cacheKey][$blockName] = false;
}
return false !== $this->resources[$cacheKey][$blockName];
}
/**
* Loads the resources for all blocks in a theme.
*
* @param mixed $theme The theme to load the block from. This parameter
* is passed by reference, because it might be necessary
* 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.
*/
protected function loadResourcesFromTheme(string $cacheKey, &$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;
}
// Use a separate variable for the inheritance traversal, because
// theme is a reference and we don't want to change it.
$currentTheme = $theme;
$context = $this->environment->mergeGlobals([]);
// The do loop takes care of template inheritance.
// Add blocks from all templates in the inheritance tree, but avoid
// overriding blocks already set.
do {
foreach ($currentTheme->getBlocks() as $block => $blockData) {
if (!isset($this->resources[$cacheKey][$block])) {
// The resource given back is the key to the bucket that
// contains this block.
$this->resources[$cacheKey][$block] = $blockData;
}
}
} while (false !== $currentTheme = $currentTheme->getParent($context));
}
}

19
vendor/symfony/twig-bridge/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,105 @@
<?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\Mime;
use League\HTMLToMarkdown\HtmlConverter;
use Symfony\Component\Mime\BodyRendererInterface;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Message;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class BodyRenderer implements BodyRendererInterface
{
private $twig;
private $context;
private $converter;
public function __construct(Environment $twig, array $context = [])
{
$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',
]);
}
}
public function render(Message $message): void
{
if (!$message instanceof TemplatedEmail) {
return;
}
$messageContext = $message->getContext();
$previousRenderingKey = $messageContext[__CLASS__] ?? null;
unset($messageContext[__CLASS__]);
$currentRenderingKey = $this->getFingerPrint($message);
if ($previousRenderingKey === $currentRenderingKey) {
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)));
}
$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));
}
}

View File

@ -0,0 +1,250 @@
<?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\Mime;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart;
use Twig\Extra\CssInliner\CssInlinerExtension;
use Twig\Extra\Inky\InkyExtension;
use Twig\Extra\Markdown\MarkdownExtension;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class NotificationEmail extends TemplatedEmail
{
public const IMPORTANCE_URGENT = 'urgent';
public const IMPORTANCE_HIGH = 'high';
public const IMPORTANCE_MEDIUM = 'medium';
public const IMPORTANCE_LOW = 'low';
private $theme = 'default';
private $context = [
'importance' => self::IMPORTANCE_LOW,
'content' => '',
'exception' => false,
'action_text' => null,
'action_url' => null,
'markdown' => false,
'raw' => false,
'footer_text' => 'Notification e-mail sent by Symfony',
];
public function __construct(Headers $headers = null, AbstractPart $body = null)
{
$missingPackages = [];
if (!class_exists(CssInlinerExtension::class)) {
$missingPackages['twig/cssinliner-extra'] = 'CSS Inliner';
}
if (!class_exists(InkyExtension::class)) {
$missingPackages['twig/inky-extra'] = 'Inky';
}
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))));
}
parent::__construct($headers, $body);
}
/**
* Creates a NotificationEmail instance that is appropriate to send to normal (non-admin) users.
*/
public static function asPublicEmail(Headers $headers = null, AbstractPart $body = null): self
{
$email = new static($headers, $body);
$email->markAsPublic();
return $email;
}
/**
* @return $this
*/
public function markAsPublic(): self
{
$this->context['importance'] = null;
$this->context['footer_text'] = null;
return $this;
}
/**
* @return $this
*/
public function markdown(string $content)
{
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__));
}
$this->context['markdown'] = true;
return $this->content($content);
}
/**
* @return $this
*/
public function content(string $content, bool $raw = false)
{
$this->context['content'] = $content;
$this->context['raw'] = $raw;
return $this;
}
/**
* @return $this
*/
public function action(string $text, string $url)
{
$this->context['action_text'] = $text;
$this->context['action_url'] = $url;
return $this;
}
/**
* @return $this
*/
public function importance(string $importance)
{
$this->context['importance'] = $importance;
return $this;
}
/**
* @param \Throwable|FlattenException $exception
*
* @return $this
*/
public function exception($exception)
{
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->importance(self::IMPORTANCE_URGENT);
if (!$this->getSubject()) {
$this->subject($exception->getMessage());
}
return $this;
}
/**
* @return $this
*/
public function theme(string $theme)
{
$this->theme = $theme;
return $this;
}
public function getTextTemplate(): ?string
{
if ($template = parent::getTextTemplate()) {
return $template;
}
return '@email/'.$this->theme.'/notification/body.txt.twig';
}
public function getHtmlTemplate(): ?string
{
if ($template = parent::getHtmlTemplate()) {
return $template;
}
return '@email/'.$this->theme.'/notification/body.html.twig';
}
public function getContext(): array
{
return array_merge($this->context, parent::getContext());
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
$importance = $this->context['importance'] ?? self::IMPORTANCE_LOW;
$this->priority($this->determinePriority($importance));
if ($this->context['importance']) {
$headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject()));
}
return $headers;
}
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;
}
}
private function getExceptionAsString($exception): string
{
if (class_exists(FlattenException::class)) {
$exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception);
return $exception->getAsString();
}
$message = \get_class($exception);
if ('' !== $exception->getMessage()) {
$message .= ': '.$exception->getMessage();
}
$message .= ' in '.$exception->getFile().':'.$exception->getLine()."\n";
$message .= "Stack trace:\n".$exception->getTraceAsString()."\n\n";
return rtrim($message);
}
/**
* @internal
*/
public function __serialize(): array
{
return [$this->context, parent::__serialize()];
}
/**
* @internal
*/
public function __unserialize(array $data): void
{
[$this->context, $parentData] = $data;
parent::__unserialize($parentData);
}
}

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\Mime;
use Symfony\Component\Mime\Email;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class TemplatedEmail extends Email
{
private $htmlTemplate;
private $textTemplate;
private $context = [];
/**
* @return $this
*/
public function textTemplate(?string $template)
{
$this->textTemplate = $template;
return $this;
}
/**
* @return $this
*/
public function htmlTemplate(?string $template)
{
$this->htmlTemplate = $template;
return $this;
}
public function getTextTemplate(): ?string
{
return $this->textTemplate;
}
public function getHtmlTemplate(): ?string
{
return $this->htmlTemplate;
}
/**
* @return $this
*/
public function context(array $context)
{
$this->context = $context;
return $this;
}
public function getContext(): array
{
return $this->context;
}
/**
* @internal
*/
public function __serialize(): array
{
return [$this->htmlTemplate, $this->textTemplate, $this->context, parent::__serialize()];
}
/**
* @internal
*/
public function __unserialize(array $data): void
{
[$this->htmlTemplate, $this->textTemplate, $this->context, $parentData] = $data;
parent::__unserialize($parentData);
}
}

View File

@ -0,0 +1,194 @@
<?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\Mime;
use Symfony\Component\Mime\Address;
use Twig\Environment;
/**
* @internal
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class WrappedTemplatedEmail
{
private $twig;
private $message;
public function __construct(Environment $twig, TemplatedEmail $message)
{
$this->twig = $twig;
$this->message = $message;
}
public function toName(): string
{
return $this->message->getTo()[0]->getName();
}
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);
}
return 'cid:'.$image;
}
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);
}
}
/**
* @return $this
*/
public function setSubject(string $subject): self
{
$this->message->subject($subject);
return $this;
}
public function getSubject(): ?string
{
return $this->message->getSubject();
}
/**
* @return $this
*/
public function setReturnPath(string $address): self
{
$this->message->returnPath($address);
return $this;
}
public function getReturnPath(): string
{
return $this->message->getReturnPath();
}
/**
* @return $this
*/
public function addFrom(string $address, string $name = ''): self
{
$this->message->addFrom(new Address($address, $name));
return $this;
}
/**
* @return Address[]
*/
public function getFrom(): array
{
return $this->message->getFrom();
}
/**
* @return $this
*/
public function addReplyTo(string $address): self
{
$this->message->addReplyTo($address);
return $this;
}
/**
* @return Address[]
*/
public function getReplyTo(): array
{
return $this->message->getReplyTo();
}
/**
* @return $this
*/
public function addTo(string $address, string $name = ''): self
{
$this->message->addTo(new Address($address, $name));
return $this;
}
/**
* @return Address[]
*/
public function getTo(): array
{
return $this->message->getTo();
}
/**
* @return $this
*/
public function addCc(string $address, string $name = ''): self
{
$this->message->addCc(new Address($address, $name));
return $this;
}
/**
* @return Address[]
*/
public function getCc(): array
{
return $this->message->getCc();
}
/**
* @return $this
*/
public function addBcc(string $address, string $name = ''): self
{
$this->message->addBcc(new Address($address, $name));
return $this;
}
/**
* @return Address[]
*/
public function getBcc(): array
{
return $this->message->getBcc();
}
/**
* @return $this
*/
public function setPriority(int $priority): self
{
$this->message->priority($priority);
return $this;
}
public function getPriority(): int
{
return $this->message->getPriority();
}
}

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\Node;
use Twig\Compiler;
use Twig\Node\Node;
/**
* @author Julien Galenski <julien.galenski@gmail.com>
*/
final class DumpNode extends Node
{
private $varPrefix;
public function __construct(string $varPrefix, ?Node $values, int $lineno, string $tag = null)
{
$nodes = [];
if (null !== $values) {
$nodes['values'] = $values;
}
parent::__construct($nodes, [], $lineno, $tag);
$this->varPrefix = $varPrefix;
}
public function compile(Compiler $compiler): void
{
$compiler
->write("if (\$this->env->isDebug()) {\n")
->indent();
if (!$this->hasNode('values')) {
// remove embedded templates (macros) from the context
$compiler
->write(sprintf('$%svars = [];'."\n", $this->varPrefix))
->write(sprintf('foreach ($context as $%1$skey => $%1$sval) {'."\n", $this->varPrefix))
->indent()
->write(sprintf('if (!$%sval instanceof \Twig\Template) {'."\n", $this->varPrefix))
->indent()
->write(sprintf('$%1$svars[$%1$skey] = $%1$sval;'."\n", $this->varPrefix))
->outdent()
->write("}\n")
->outdent()
->write("}\n")
->addDebugInfo($this)
->write(sprintf('\Symfony\Component\VarDumper\VarDumper::dump($%svars);'."\n", $this->varPrefix));
} elseif (($values = $this->getNode('values')) && 1 === $values->count()) {
$compiler
->addDebugInfo($this)
->write('\Symfony\Component\VarDumper\VarDumper::dump(')
->subcompile($values->getNode(0))
->raw(");\n");
} else {
$compiler
->addDebugInfo($this)
->write('\Symfony\Component\VarDumper\VarDumper::dump(['."\n")
->indent();
foreach ($values as $node) {
$compiler->write('');
if ($node->hasAttribute('name')) {
$compiler
->string($node->getAttribute('name'))
->raw(' => ');
}
$compiler
->subcompile($node)
->raw(",\n");
}
$compiler
->outdent()
->write("]);\n");
}
$compiler
->outdent()
->write("}\n");
}
}

View File

@ -0,0 +1,42 @@
<?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\Node;
use Symfony\Component\Form\FormRenderer;
use Twig\Compiler;
use Twig\Node\Node;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class FormThemeNode extends Node
{
public function __construct(Node $form, Node $resources, int $lineno, string $tag = null, bool $only = false)
{
parent::__construct(['form' => $form, 'resources' => $resources], ['only' => $only], $lineno, $tag);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write('$this->env->getRuntime(')
->string(FormRenderer::class)
->raw(')->setTheme(')
->subcompile($this->getNode('form'))
->raw(', ')
->subcompile($this->getNode('resources'))
->raw(', ')
->raw(false === $this->getAttribute('only') ? 'true' : 'false')
->raw(");\n");
}
}

View File

@ -0,0 +1,45 @@
<?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\Node;
use Twig\Compiler;
use Twig\Node\Expression\FunctionExpression;
/**
* Compiles a call to {@link \Symfony\Component\Form\FormRendererInterface::renderBlock()}.
*
* The function name is used as block name. For example, if the function name
* is "foo", the block "foo" will be rendered.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
final class RenderBlockNode extends FunctionExpression
{
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$arguments = iterator_to_array($this->getNode('arguments'));
$compiler->write('$this->env->getRuntime(\'Symfony\Component\Form\FormRenderer\')->renderBlock(');
if (isset($arguments[0])) {
$compiler->subcompile($arguments[0]);
$compiler->raw(', \''.$this->getAttribute('name').'\'');
if (isset($arguments[1])) {
$compiler->raw(', ');
$compiler->subcompile($arguments[1]);
}
}
$compiler->raw(')');
}
}

View File

@ -0,0 +1,110 @@
<?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\Node;
use Twig\Compiler;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
final class SearchAndRenderBlockNode extends FunctionExpression
{
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$compiler->raw('$this->env->getRuntime(\'Symfony\Component\Form\FormRenderer\')->searchAndRenderBlock(');
preg_match('/_([^_]+)$/', $this->getAttribute('name'), $matches);
$arguments = iterator_to_array($this->getNode('arguments'));
$blockNameSuffix = $matches[1];
if (isset($arguments[0])) {
$compiler->subcompile($arguments[0]);
$compiler->raw(', \''.$blockNameSuffix.'\'');
if (isset($arguments[1])) {
if ('label' === $blockNameSuffix) {
// The "label" function expects the label in the second and
// the variables in the third argument
$label = $arguments[1];
$variables = $arguments[2] ?? null;
$lineno = $label->getTemplateLine();
if ($label instanceof ConstantExpression) {
// If the label argument is given as a constant, we can either
// strip it away if it is empty, or integrate it into the array
// of variables at compile time.
$labelIsExpression = false;
// Only insert the label into the array if it is not empty
if (!twig_test_empty($label->getAttribute('value'))) {
$originalVariables = $variables;
$variables = new ArrayExpression([], $lineno);
$labelKey = new ConstantExpression('label', $lineno);
if (null !== $originalVariables) {
foreach ($originalVariables->getKeyValuePairs() as $pair) {
// Don't copy the original label attribute over if it exists
if ((string) $labelKey !== (string) $pair['key']) {
$variables->addElement($pair['value'], $pair['key']);
}
}
}
// Insert the label argument into the array
$variables->addElement($label, $labelKey);
}
} else {
// The label argument is not a constant, but some kind of
// expression. This expression needs to be evaluated at runtime.
// Depending on the result (whether it is null or not), the
// label in the arguments should take precedence over the label
// in the attributes or not.
$labelIsExpression = true;
}
} else {
// All other functions than "label" expect the variables
// in the second argument
$label = null;
$variables = $arguments[1];
$labelIsExpression = false;
}
if (null !== $variables || $labelIsExpression) {
$compiler->raw(', ');
if (null !== $variables) {
$compiler->subcompile($variables);
}
if ($labelIsExpression) {
if (null !== $variables) {
$compiler->raw(' + ');
}
// Check at runtime whether the label is empty.
// If not, add it to the array at runtime.
$compiler->raw('(twig_test_empty($_label_ = ');
$compiler->subcompile($label);
$compiler->raw(') ? [] : ["label" => $_label_])');
}
}
}
}
$compiler->raw(')');
}
}

View File

@ -0,0 +1,48 @@
<?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\Node;
use Twig\Compiler;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Node;
/**
* Represents a stopwatch node.
*
* @author Wouter J <wouter@wouterj.nl>
*/
final class StopwatchNode extends Node
{
public function __construct(Node $name, Node $body, AssignNameExpression $var, int $lineno = 0, string $tag = null)
{
parent::__construct(['body' => $body, 'name' => $name, 'var' => $var], [], $lineno, $tag);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write('')
->subcompile($this->getNode('var'))
->raw(' = ')
->subcompile($this->getNode('name'))
->write(";\n")
->write("\$this->env->getExtension('Symfony\Bridge\Twig\Extension\StopwatchExtension')->getStopwatch()->start(")
->subcompile($this->getNode('var'))
->raw(", 'template');\n")
->subcompile($this->getNode('body'))
->write("\$this->env->getExtension('Symfony\Bridge\Twig\Extension\StopwatchExtension')->getStopwatch()->stop(")
->subcompile($this->getNode('var'))
->raw(");\n")
;
}
}

View File

@ -0,0 +1,32 @@
<?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\Node;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TransDefaultDomainNode extends Node
{
public function __construct(AbstractExpression $expr, int $lineno = 0, string $tag = null)
{
parent::__construct(['expr' => $expr], [], $lineno, $tag);
}
public function compile(Compiler $compiler): void
{
// noop as this node is just a marker for TranslationDefaultDomainNodeVisitor
}
}

View File

@ -0,0 +1,130 @@
<?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\Node;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Node;
use Twig\Node\TextNode;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TransNode extends Node
{
public function __construct(Node $body, Node $domain = null, AbstractExpression $count = null, AbstractExpression $vars = null, AbstractExpression $locale = null, int $lineno = 0, string $tag = null)
{
$nodes = ['body' => $body];
if (null !== $domain) {
$nodes['domain'] = $domain;
}
if (null !== $count) {
$nodes['count'] = $count;
}
if (null !== $vars) {
$nodes['vars'] = $vars;
}
if (null !== $locale) {
$nodes['locale'] = $locale;
}
parent::__construct($nodes, [], $lineno, $tag);
}
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$defaults = new ArrayExpression([], -1);
if ($this->hasNode('vars') && ($vars = $this->getNode('vars')) instanceof ArrayExpression) {
$defaults = $this->getNode('vars');
$vars = null;
}
[$msg, $defaults] = $this->compileString($this->getNode('body'), $defaults, (bool) $vars);
$compiler
->write('echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans(')
->subcompile($msg)
;
$compiler->raw(', ');
if (null !== $vars) {
$compiler
->raw('array_merge(')
->subcompile($defaults)
->raw(', ')
->subcompile($this->getNode('vars'))
->raw(')')
;
} else {
$compiler->subcompile($defaults);
}
$compiler->raw(', ');
if (!$this->hasNode('domain')) {
$compiler->repr('messages');
} else {
$compiler->subcompile($this->getNode('domain'));
}
if ($this->hasNode('locale')) {
$compiler
->raw(', ')
->subcompile($this->getNode('locale'))
;
} elseif ($this->hasNode('count')) {
$compiler->raw(', null');
}
if ($this->hasNode('count')) {
$compiler
->raw(', ')
->subcompile($this->getNode('count'))
;
}
$compiler->raw(");\n");
}
private function compileString(Node $body, ArrayExpression $vars, bool $ignoreStrictCheck = false): array
{
if ($body instanceof ConstantExpression) {
$msg = $body->getAttribute('value');
} elseif ($body instanceof TextNode) {
$msg = $body->getAttribute('data');
} else {
return [$body, $vars];
}
preg_match_all('/(?<!%)%([^%]+)%/', $msg, $matches);
foreach ($matches[1] as $var) {
$key = new ConstantExpression('%'.$var.'%', $body->getTemplateLine());
if (!$vars->hasElement($key)) {
if ('count' === $var && $this->hasNode('count')) {
$vars->addElement($this->getNode('count'), $key);
} else {
$varExpr = new NameExpression($var, $body->getTemplateLine());
$varExpr->setAttribute('ignore_strict_check', $ignoreStrictCheck);
$vars->addElement($varExpr, $key);
}
}
}
return [new ConstantExpression(str_replace('%%', '%', trim($msg)), $body->getTemplateLine()), $vars];
}
}

View File

@ -0,0 +1,103 @@
<?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\NodeVisitor;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
class Scope
{
private $parent;
private $data = [];
private $left = false;
public function __construct(self $parent = null)
{
$this->parent = $parent;
}
/**
* Opens a new child scope.
*
* @return self
*/
public function enter()
{
return new self($this);
}
/**
* Closes current scope and returns parent one.
*
* @return self|null
*/
public function leave()
{
$this->left = true;
return $this->parent;
}
/**
* Stores data into current scope.
*
* @return $this
*
* @throws \LogicException
*/
public function set(string $key, $value)
{
if ($this->left) {
throw new \LogicException('Left scope is not mutable.');
}
$this->data[$key] = $value;
return $this;
}
/**
* Tests if a data is visible from current scope.
*
* @return bool
*/
public function has(string $key)
{
if (\array_key_exists($key, $this->data)) {
return true;
}
if (null === $this->parent) {
return false;
}
return $this->parent->has($key);
}
/**
* Returns data visible from current scope.
*
* @return mixed
*/
public function get(string $key, $default = null)
{
if (\array_key_exists($key, $this->data)) {
return $this->data[$key];
}
if (null === $this->parent) {
return $default;
}
return $this->parent->get($key, $default);
}
}

View File

@ -0,0 +1,128 @@
<?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\NodeVisitor;
use Symfony\Bridge\Twig\Node\TransDefaultDomainNode;
use Symfony\Bridge\Twig\Node\TransNode;
use Twig\Environment;
use Twig\Node\BlockNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\SetNode;
use Twig\NodeVisitor\AbstractNodeVisitor;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor
{
private $scope;
public function __construct()
{
$this->scope = new Scope();
}
/**
* {@inheritdoc}
*/
protected function doEnterNode(Node $node, Environment $env): Node
{
if ($node instanceof BlockNode || $node instanceof ModuleNode) {
$this->scope = $this->scope->enter();
}
if ($node instanceof TransDefaultDomainNode) {
if ($node->getNode('expr') instanceof ConstantExpression) {
$this->scope->set('domain', $node->getNode('expr'));
return $node;
} else {
$var = $this->getVarName();
$name = new AssignNameExpression($var, $node->getTemplateLine());
$this->scope->set('domain', new NameExpression($var, $node->getTemplateLine()));
return new SetNode(false, new Node([$name]), new Node([$node->getNode('expr')]), $node->getTemplateLine());
}
}
if (!$this->scope->has('domain')) {
return $node;
}
if ($node instanceof FilterExpression && 'trans' === $node->getNode('filter')->getAttribute('value')) {
$arguments = $node->getNode('arguments');
if ($this->isNamedArguments($arguments)) {
if (!$arguments->hasNode('domain') && !$arguments->hasNode(1)) {
$arguments->setNode('domain', $this->scope->get('domain'));
}
} elseif (!$arguments->hasNode(1)) {
if (!$arguments->hasNode(0)) {
$arguments->setNode(0, new ArrayExpression([], $node->getTemplateLine()));
}
$arguments->setNode(1, $this->scope->get('domain'));
}
} elseif ($node instanceof TransNode) {
if (!$node->hasNode('domain')) {
$node->setNode('domain', $this->scope->get('domain'));
}
}
return $node;
}
/**
* {@inheritdoc}
*/
protected function doLeaveNode(Node $node, Environment $env): ?Node
{
if ($node instanceof TransDefaultDomainNode) {
return null;
}
if ($node instanceof BlockNode || $node instanceof ModuleNode) {
$this->scope = $this->scope->leave();
}
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
return -10;
}
private function isNamedArguments(Node $arguments): bool
{
foreach ($arguments as $name => $node) {
if (!\is_int($name)) {
return true;
}
}
return false;
}
private function getVarName(): string
{
return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false));
}
}

View File

@ -0,0 +1,187 @@
<?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\NodeVisitor;
use Symfony\Bridge\Twig\Node\TransNode;
use Twig\Environment;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Node;
use Twig\NodeVisitor\AbstractNodeVisitor;
/**
* TranslationNodeVisitor extracts translation messages.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TranslationNodeVisitor extends AbstractNodeVisitor
{
public const UNDEFINED_DOMAIN = '_undefined';
private $enabled = false;
private $messages = [];
public function enable(): void
{
$this->enabled = true;
$this->messages = [];
}
public function disable(): void
{
$this->enabled = false;
$this->messages = [];
}
public function getMessages(): array
{
return $this->messages;
}
/**
* {@inheritdoc}
*/
protected function doEnterNode(Node $node, Environment $env): Node
{
if (!$this->enabled) {
return $node;
}
if (
$node instanceof FilterExpression &&
'trans' === $node->getNode('filter')->getAttribute('value') &&
$node->getNode('node') instanceof ConstantExpression
) {
// extract constant nodes with a trans filter
$this->messages[] = [
$node->getNode('node')->getAttribute('value'),
$this->getReadDomainFromArguments($node->getNode('arguments'), 1),
];
} elseif (
$node instanceof FunctionExpression &&
't' === $node->getAttribute('name')
) {
$nodeArguments = $node->getNode('arguments');
if ($nodeArguments->getIterator()->current() instanceof ConstantExpression) {
$this->messages[] = [
$this->getReadMessageFromArguments($nodeArguments, 0),
$this->getReadDomainFromArguments($nodeArguments, 2),
];
}
} elseif ($node instanceof TransNode) {
// extract trans nodes
$this->messages[] = [
$node->getNode('body')->getAttribute('data'),
$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)
) {
$this->messages[] = [
$message,
$this->getReadDomainFromArguments($node->getNode('arguments'), 1),
];
}
return $node;
}
/**
* {@inheritdoc}
*/
protected function doLeaveNode(Node $node, Environment $env): ?Node
{
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
return 0;
}
private function getReadMessageFromArguments(Node $arguments, int $index): ?string
{
if ($arguments->hasNode('message')) {
$argument = $arguments->getNode('message');
} elseif ($arguments->hasNode($index)) {
$argument = $arguments->getNode($index);
} else {
return null;
}
return $this->getReadMessageFromNode($argument);
}
private function getReadMessageFromNode(Node $node): ?string
{
if ($node instanceof ConstantExpression) {
return $node->getAttribute('value');
}
return null;
}
private function getReadDomainFromArguments(Node $arguments, int $index): ?string
{
if ($arguments->hasNode('domain')) {
$argument = $arguments->getNode('domain');
} elseif ($arguments->hasNode($index)) {
$argument = $arguments->getNode($index);
} else {
return null;
}
return $this->getReadDomainFromNode($argument);
}
private function getReadDomainFromNode(Node $node): ?string
{
if ($node instanceof ConstantExpression) {
return $node->getAttribute('value');
}
return self::UNDEFINED_DOMAIN;
}
private function getConcatValueFromNode(Node $node, ?string $value): ?string
{
if ($node instanceof ConcatBinary) {
foreach ($node as $nextNode) {
if ($nextNode instanceof ConcatBinary) {
$nextValue = $this->getConcatValueFromNode($nextNode, $value);
if (null === $nextValue) {
return null;
}
$value .= $nextValue;
} elseif ($nextNode instanceof ConstantExpression) {
$value .= $nextNode->getAttribute('value');
} else {
// this is a node we cannot process (variable, or translation in translation)
return null;
}
}
} elseif ($node instanceof ConstantExpression) {
$value .= $node->getAttribute('value');
}
return $value;
}
}

13
vendor/symfony/twig-bridge/README.md vendored Normal file
View File

@ -0,0 +1,13 @@
Twig Bridge
===========
The Twig bridge provides integration for [Twig](https://twig.symfony.com/) with
various Symfony components.
Resources
---------
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -0,0 +1 @@
{% extends "@email/zurb_2/notification/body.html.twig" %}

View File

@ -0,0 +1 @@
{% extends "@email/zurb_2/notification/body.txt.twig" %}

View File

@ -0,0 +1,1667 @@
/*
* Copyright (c) 2017 ZURB, inc. -- MIT License
*
* https://raw.githubusercontent.com/foundation/foundation-emails/v2.2.1/dist/foundation-emails.css
*/
.wrapper {
width: 100%;
}
#outlook a {
padding: 0;
}
body {
width: 100% !important;
min-width: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
margin: 0;
Margin: 0;
padding: 0;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
#backgroundTable {
margin: 0;
Margin: 0;
padding: 0;
width: 100% !important;
line-height: 100% !important;
}
img {
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
width: auto;
max-width: 100%;
clear: both;
display: block;
}
center {
width: 100%;
min-width: 580px;
}
a img {
border: none;
}
p {
margin: 0 0 0 10px;
Margin: 0 0 0 10px;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
td {
word-wrap: break-word;
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
border-collapse: collapse !important;
}
table,
tr,
td {
padding: 0;
vertical-align: top;
text-align: left;
}
@media only screen {
html {
min-height: 100%;
background: #f3f3f3;
}
}
table.body {
background: #f3f3f3;
height: 100%;
width: 100%;
}
table.container {
background: #fefefe;
width: 580px;
margin: 0 auto;
Margin: 0 auto;
text-align: inherit;
}
table.row {
padding: 0;
width: 100%;
position: relative;
}
table.spacer {
width: 100%;
}
table.spacer td {
mso-line-height-rule: exactly;
}
table.container table.row {
display: table;
}
td.columns,
td.column,
th.columns,
th.column {
margin: 0 auto;
Margin: 0 auto;
padding-left: 16px;
padding-bottom: 16px;
}
td.columns .column,
td.columns .columns,
td.column .column,
td.column .columns,
th.columns .column,
th.columns .columns,
th.column .column,
th.column .columns {
padding-left: 0 !important;
padding-right: 0 !important;
}
td.columns .column center,
td.columns .columns center,
td.column .column center,
td.column .columns center,
th.columns .column center,
th.columns .columns center,
th.column .column center,
th.column .columns center {
min-width: none !important;
}
td.columns.last,
td.column.last,
th.columns.last,
th.column.last {
padding-right: 16px;
}
td.columns table:not(.button),
td.column table:not(.button),
th.columns table:not(.button),
th.column table:not(.button) {
width: 100%;
}
td.large-1,
th.large-1 {
width: 32.33333px;
padding-left: 8px;
padding-right: 8px;
}
td.large-1.first,
th.large-1.first {
padding-left: 16px;
}
td.large-1.last,
th.large-1.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-1,
.collapse>tbody>tr>th.large-1 {
padding-right: 0;
padding-left: 0;
width: 48.33333px;
}
.collapse td.large-1.first,
.collapse th.large-1.first,
.collapse td.large-1.last,
.collapse th.large-1.last {
width: 56.33333px;
}
td.large-1 center,
th.large-1 center {
min-width: 0.33333px;
}
.body .columns td.large-1,
.body .column td.large-1,
.body .columns th.large-1,
.body .column th.large-1 {
width: 8.33333%;
}
td.large-2,
th.large-2 {
width: 80.66667px;
padding-left: 8px;
padding-right: 8px;
}
td.large-2.first,
th.large-2.first {
padding-left: 16px;
}
td.large-2.last,
th.large-2.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-2,
.collapse>tbody>tr>th.large-2 {
padding-right: 0;
padding-left: 0;
width: 96.66667px;
}
.collapse td.large-2.first,
.collapse th.large-2.first,
.collapse td.large-2.last,
.collapse th.large-2.last {
width: 104.66667px;
}
td.large-2 center,
th.large-2 center {
min-width: 48.66667px;
}
.body .columns td.large-2,
.body .column td.large-2,
.body .columns th.large-2,
.body .column th.large-2 {
width: 16.66667%;
}
td.large-3,
th.large-3 {
width: 129px;
padding-left: 8px;
padding-right: 8px;
}
td.large-3.first,
th.large-3.first {
padding-left: 16px;
}
td.large-3.last,
th.large-3.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-3,
.collapse>tbody>tr>th.large-3 {
padding-right: 0;
padding-left: 0;
width: 145px;
}
.collapse td.large-3.first,
.collapse th.large-3.first,
.collapse td.large-3.last,
.collapse th.large-3.last {
width: 153px;
}
td.large-3 center,
th.large-3 center {
min-width: 97px;
}
.body .columns td.large-3,
.body .column td.large-3,
.body .columns th.large-3,
.body .column th.large-3 {
width: 25%;
}
td.large-4,
th.large-4 {
width: 177.33333px;
padding-left: 8px;
padding-right: 8px;
}
td.large-4.first,
th.large-4.first {
padding-left: 16px;
}
td.large-4.last,
th.large-4.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-4,
.collapse>tbody>tr>th.large-4 {
padding-right: 0;
padding-left: 0;
width: 193.33333px;
}
.collapse td.large-4.first,
.collapse th.large-4.first,
.collapse td.large-4.last,
.collapse th.large-4.last {
width: 201.33333px;
}
td.large-4 center,
th.large-4 center {
min-width: 145.33333px;
}
.body .columns td.large-4,
.body .column td.large-4,
.body .columns th.large-4,
.body .column th.large-4 {
width: 33.33333%;
}
td.large-5,
th.large-5 {
width: 225.66667px;
padding-left: 8px;
padding-right: 8px;
}
td.large-5.first,
th.large-5.first {
padding-left: 16px;
}
td.large-5.last,
th.large-5.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-5,
.collapse>tbody>tr>th.large-5 {
padding-right: 0;
padding-left: 0;
width: 241.66667px;
}
.collapse td.large-5.first,
.collapse th.large-5.first,
.collapse td.large-5.last,
.collapse th.large-5.last {
width: 249.66667px;
}
td.large-5 center,
th.large-5 center {
min-width: 193.66667px;
}
.body .columns td.large-5,
.body .column td.large-5,
.body .columns th.large-5,
.body .column th.large-5 {
width: 41.66667%;
}
td.large-6,
th.large-6 {
width: 274px;
padding-left: 8px;
padding-right: 8px;
}
td.large-6.first,
th.large-6.first {
padding-left: 16px;
}
td.large-6.last,
th.large-6.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-6,
.collapse>tbody>tr>th.large-6 {
padding-right: 0;
padding-left: 0;
width: 290px;
}
.collapse td.large-6.first,
.collapse th.large-6.first,
.collapse td.large-6.last,
.collapse th.large-6.last {
width: 298px;
}
td.large-6 center,
th.large-6 center {
min-width: 242px;
}
.body .columns td.large-6,
.body .column td.large-6,
.body .columns th.large-6,
.body .column th.large-6 {
width: 50%;
}
td.large-7,
th.large-7 {
width: 322.33333px;
padding-left: 8px;
padding-right: 8px;
}
td.large-7.first,
th.large-7.first {
padding-left: 16px;
}
td.large-7.last,
th.large-7.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-7,
.collapse>tbody>tr>th.large-7 {
padding-right: 0;
padding-left: 0;
width: 338.33333px;
}
.collapse td.large-7.first,
.collapse th.large-7.first,
.collapse td.large-7.last,
.collapse th.large-7.last {
width: 346.33333px;
}
td.large-7 center,
th.large-7 center {
min-width: 290.33333px;
}
.body .columns td.large-7,
.body .column td.large-7,
.body .columns th.large-7,
.body .column th.large-7 {
width: 58.33333%;
}
td.large-8,
th.large-8 {
width: 370.66667px;
padding-left: 8px;
padding-right: 8px;
}
td.large-8.first,
th.large-8.first {
padding-left: 16px;
}
td.large-8.last,
th.large-8.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-8,
.collapse>tbody>tr>th.large-8 {
padding-right: 0;
padding-left: 0;
width: 386.66667px;
}
.collapse td.large-8.first,
.collapse th.large-8.first,
.collapse td.large-8.last,
.collapse th.large-8.last {
width: 394.66667px;
}
td.large-8 center,
th.large-8 center {
min-width: 338.66667px;
}
.body .columns td.large-8,
.body .column td.large-8,
.body .columns th.large-8,
.body .column th.large-8 {
width: 66.66667%;
}
td.large-9,
th.large-9 {
width: 419px;
padding-left: 8px;
padding-right: 8px;
}
td.large-9.first,
th.large-9.first {
padding-left: 16px;
}
td.large-9.last,
th.large-9.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-9,
.collapse>tbody>tr>th.large-9 {
padding-right: 0;
padding-left: 0;
width: 435px;
}
.collapse td.large-9.first,
.collapse th.large-9.first,
.collapse td.large-9.last,
.collapse th.large-9.last {
width: 443px;
}
td.large-9 center,
th.large-9 center {
min-width: 387px;
}
.body .columns td.large-9,
.body .column td.large-9,
.body .columns th.large-9,
.body .column th.large-9 {
width: 75%;
}
td.large-10,
th.large-10 {
width: 467.33333px;
padding-left: 8px;
padding-right: 8px;
}
td.large-10.first,
th.large-10.first {
padding-left: 16px;
}
td.large-10.last,
th.large-10.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-10,
.collapse>tbody>tr>th.large-10 {
padding-right: 0;
padding-left: 0;
width: 483.33333px;
}
.collapse td.large-10.first,
.collapse th.large-10.first,
.collapse td.large-10.last,
.collapse th.large-10.last {
width: 491.33333px;
}
td.large-10 center,
th.large-10 center {
min-width: 435.33333px;
}
.body .columns td.large-10,
.body .column td.large-10,
.body .columns th.large-10,
.body .column th.large-10 {
width: 83.33333%;
}
td.large-11,
th.large-11 {
width: 515.66667px;
padding-left: 8px;
padding-right: 8px;
}
td.large-11.first,
th.large-11.first {
padding-left: 16px;
}
td.large-11.last,
th.large-11.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-11,
.collapse>tbody>tr>th.large-11 {
padding-right: 0;
padding-left: 0;
width: 531.66667px;
}
.collapse td.large-11.first,
.collapse th.large-11.first,
.collapse td.large-11.last,
.collapse th.large-11.last {
width: 539.66667px;
}
td.large-11 center,
th.large-11 center {
min-width: 483.66667px;
}
.body .columns td.large-11,
.body .column td.large-11,
.body .columns th.large-11,
.body .column th.large-11 {
width: 91.66667%;
}
td.large-12,
th.large-12 {
width: 564px;
padding-left: 8px;
padding-right: 8px;
}
td.large-12.first,
th.large-12.first {
padding-left: 16px;
}
td.large-12.last,
th.large-12.last {
padding-right: 16px;
}
.collapse>tbody>tr>td.large-12,
.collapse>tbody>tr>th.large-12 {
padding-right: 0;
padding-left: 0;
width: 580px;
}
.collapse td.large-12.first,
.collapse th.large-12.first,
.collapse td.large-12.last,
.collapse th.large-12.last {
width: 588px;
}
td.large-12 center,
th.large-12 center {
min-width: 532px;
}
.body .columns td.large-12,
.body .column td.large-12,
.body .columns th.large-12,
.body .column th.large-12 {
width: 100%;
}
td.large-offset-1,
td.large-offset-1.first,
td.large-offset-1.last,
th.large-offset-1,
th.large-offset-1.first,
th.large-offset-1.last {
padding-left: 64.33333px;
}
td.large-offset-2,
td.large-offset-2.first,
td.large-offset-2.last,
th.large-offset-2,
th.large-offset-2.first,
th.large-offset-2.last {
padding-left: 112.66667px;
}
td.large-offset-3,
td.large-offset-3.first,
td.large-offset-3.last,
th.large-offset-3,
th.large-offset-3.first,
th.large-offset-3.last {
padding-left: 161px;
}
td.large-offset-4,
td.large-offset-4.first,
td.large-offset-4.last,
th.large-offset-4,
th.large-offset-4.first,
th.large-offset-4.last {
padding-left: 209.33333px;
}
td.large-offset-5,
td.large-offset-5.first,
td.large-offset-5.last,
th.large-offset-5,
th.large-offset-5.first,
th.large-offset-5.last {
padding-left: 257.66667px;
}
td.large-offset-6,
td.large-offset-6.first,
td.large-offset-6.last,
th.large-offset-6,
th.large-offset-6.first,
th.large-offset-6.last {
padding-left: 306px;
}
td.large-offset-7,
td.large-offset-7.first,
td.large-offset-7.last,
th.large-offset-7,
th.large-offset-7.first,
th.large-offset-7.last {
padding-left: 354.33333px;
}
td.large-offset-8,
td.large-offset-8.first,
td.large-offset-8.last,
th.large-offset-8,
th.large-offset-8.first,
th.large-offset-8.last {
padding-left: 402.66667px;
}
td.large-offset-9,
td.large-offset-9.first,
td.large-offset-9.last,
th.large-offset-9,
th.large-offset-9.first,
th.large-offset-9.last {
padding-left: 451px;
}
td.large-offset-10,
td.large-offset-10.first,
td.large-offset-10.last,
th.large-offset-10,
th.large-offset-10.first,
th.large-offset-10.last {
padding-left: 499.33333px;
}
td.large-offset-11,
td.large-offset-11.first,
td.large-offset-11.last,
th.large-offset-11,
th.large-offset-11.first,
th.large-offset-11.last {
padding-left: 547.66667px;
}
td.expander,
th.expander {
visibility: hidden;
width: 0;
padding: 0 !important;
}
table.container.radius {
border-radius: 0;
border-collapse: separate;
}
.block-grid {
width: 100%;
max-width: 580px;
}
.block-grid td {
display: inline-block;
padding: 8px;
}
.up-2 td {
width: 274px !important;
}
.up-3 td {
width: 177px !important;
}
.up-4 td {
width: 129px !important;
}
.up-5 td {
width: 100px !important;
}
.up-6 td {
width: 80px !important;
}
.up-7 td {
width: 66px !important;
}
.up-8 td {
width: 56px !important;
}
table.text-center,
th.text-center,
td.text-center,
h1.text-center,
h2.text-center,
h3.text-center,
h4.text-center,
h5.text-center,
h6.text-center,
p.text-center,
span.text-center {
text-align: center;
}
table.text-left,
th.text-left,
td.text-left,
h1.text-left,
h2.text-left,
h3.text-left,
h4.text-left,
h5.text-left,
h6.text-left,
p.text-left,
span.text-left {
text-align: left;
}
table.text-right,
th.text-right,
td.text-right,
h1.text-right,
h2.text-right,
h3.text-right,
h4.text-right,
h5.text-right,
h6.text-right,
p.text-right,
span.text-right {
text-align: right;
}
span.text-center {
display: block;
width: 100%;
text-align: center;
}
@media only screen and (max-width: 596px) {
.small-float-center {
margin: 0 auto !important;
float: none !important;
text-align: center !important;
}
.small-text-center {
text-align: center !important;
}
.small-text-left {
text-align: left !important;
}
.small-text-right {
text-align: right !important;
}
}
img.float-left {
float: left;
text-align: left;
}
img.float-right {
float: right;
text-align: right;
}
img.float-center,
img.text-center {
margin: 0 auto;
Margin: 0 auto;
float: none;
text-align: center;
}
table.float-center,
td.float-center,
th.float-center {
margin: 0 auto;
Margin: 0 auto;
float: none;
text-align: center;
}
.hide-for-large {
display: none !important;
mso-hide: all;
overflow: hidden;
max-height: 0;
font-size: 0;
width: 0;
line-height: 0;
}
@media only screen and (max-width: 596px) {
.hide-for-large {
display: block !important;
width: auto !important;
overflow: visible !important;
max-height: none !important;
font-size: inherit !important;
line-height: inherit !important;
}
}
table.body table.container .hide-for-large * {
mso-hide: all;
}
@media only screen and (max-width: 596px) {
table.body table.container .hide-for-large,
table.body table.container .row.hide-for-large {
display: table !important;
width: 100% !important;
}
}
@media only screen and (max-width: 596px) {
table.body table.container .callout-inner.hide-for-large {
display: table-cell !important;
width: 100% !important;
}
}
@media only screen and (max-width: 596px) {
table.body table.container .show-for-large {
display: none !important;
width: 0;
mso-hide: all;
overflow: hidden;
}
}
body,
table.body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
td,
th,
a {
color: #0a0a0a;
font-family: Helvetica, Arial, sans-serif;
font-weight: normal;
padding: 0;
margin: 0;
Margin: 0;
text-align: left;
line-height: 1.3;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: inherit;
word-wrap: normal;
font-family: Helvetica, Arial, sans-serif;
font-weight: normal;
margin-bottom: 10px;
Margin-bottom: 10px;
}
h1 {
font-size: 34px;
}
h2 {
font-size: 30px;
}
h3 {
font-size: 28px;
}
h4 {
font-size: 24px;
}
h5 {
font-size: 20px;
}
h6 {
font-size: 18px;
}
body,
table.body,
p,
td,
th {
font-size: 16px;
line-height: 1.3;
}
p {
margin-bottom: 10px;
Margin-bottom: 10px;
}
p.lead {
font-size: 20px;
line-height: 1.6;
}
p.subheader {
margin-top: 4px;
margin-bottom: 8px;
Margin-top: 4px;
Margin-bottom: 8px;
font-weight: normal;
line-height: 1.4;
color: #8a8a8a;
}
small {
font-size: 80%;
color: #cacaca;
}
a {
color: #2199e8;
text-decoration: none;
}
a:hover {
color: #147dc2;
}
a:active {
color: #147dc2;
}
a:visited {
color: #2199e8;
}
h1 a,
h1 a:visited,
h2 a,
h2 a:visited,
h3 a,
h3 a:visited,
h4 a,
h4 a:visited,
h5 a,
h5 a:visited,
h6 a,
h6 a:visited {
color: #2199e8;
}
pre {
background: #f3f3f3;
margin: 30px 0;
Margin: 30px 0;
}
pre code {
color: #cacaca;
}
pre code span.callout {
color: #8a8a8a;
font-weight: bold;
}
pre code span.callout-strong {
color: #ff6908;
font-weight: bold;
}
table.hr {
width: 100%;
}
table.hr th {
height: 0;
max-width: 580px;
border-top: 0;
border-right: 0;
border-bottom: 1px solid #0a0a0a;
border-left: 0;
margin: 20px auto;
Margin: 20px auto;
clear: both;
}
.stat {
font-size: 40px;
line-height: 1;
}
p+.stat {
margin-top: -16px;
Margin-top: -16px;
}
span.preheader {
display: none !important;
visibility: hidden;
mso-hide: all !important;
font-size: 1px;
color: #f3f3f3;
line-height: 1px;
max-height: 0px;
max-width: 0px;
opacity: 0;
overflow: hidden;
}
table.button {
width: auto;
margin: 0 0 16px 0;
Margin: 0 0 16px 0;
}
table.button table td {
text-align: left;
color: #fefefe;
background: #2199e8;
border: 2px solid #2199e8;
}
table.button table td a {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
font-weight: bold;
color: #fefefe;
text-decoration: none;
display: inline-block;
padding: 8px 16px 8px 16px;
border: 0 solid #2199e8;
border-radius: 3px;
}
table.button.radius table td {
border-radius: 3px;
border: none;
}
table.button.rounded table td {
border-radius: 500px;
border: none;
}
table.button:hover table tr td a,
table.button:active table tr td a,
table.button table tr td a:visited,
table.button.tiny:hover table tr td a,
table.button.tiny:active table tr td a,
table.button.tiny table tr td a:visited,
table.button.small:hover table tr td a,
table.button.small:active table tr td a,
table.button.small table tr td a:visited,
table.button.large:hover table tr td a,
table.button.large:active table tr td a,
table.button.large table tr td a:visited {
color: #fefefe;
}
table.button.tiny table td,
table.button.tiny table a {
padding: 4px 8px 4px 8px;
}
table.button.tiny table a {
font-size: 10px;
font-weight: normal;
}
table.button.small table td,
table.button.small table a {
padding: 5px 10px 5px 10px;
font-size: 12px;
}
table.button.large table a {
padding: 10px 20px 10px 20px;
font-size: 20px;
}
table.button.expand,
table.button.expanded {
width: 100% !important;
}
table.button.expand table,
table.button.expanded table {
width: 100%;
}
table.button.expand table a,
table.button.expanded table a {
text-align: center;
width: 100%;
padding-left: 0;
padding-right: 0;
}
table.button.expand center,
table.button.expanded center {
min-width: 0;
}
table.button:hover table td,
table.button:visited table td,
table.button:active table td {
background: #147dc2;
color: #fefefe;
}
table.button:hover table a,
table.button:visited table a,
table.button:active table a {
border: 0 solid #147dc2;
}
table.button.secondary table td {
background: #777777;
color: #fefefe;
border: 0px solid #777777;
}
table.button.secondary table a {
color: #fefefe;
border: 0 solid #777777;
}
table.button.secondary:hover table td {
background: #919191;
color: #fefefe;
}
table.button.secondary:hover table a {
border: 0 solid #919191;
}
table.button.secondary:hover table td a {
color: #fefefe;
}
table.button.secondary:active table td a {
color: #fefefe;
}
table.button.secondary table td a:visited {
color: #fefefe;
}
table.button.success table td {
background: #3adb76;
border: 0px solid #3adb76;
}
table.button.success table a {
border: 0 solid #3adb76;
}
table.button.success:hover table td {
background: #23bf5d;
}
table.button.success:hover table a {
border: 0 solid #23bf5d;
}
table.button.alert table td {
background: #ec5840;
border: 0px solid #ec5840;
}
table.button.alert table a {
border: 0 solid #ec5840;
}
table.button.alert:hover table td {
background: #e23317;
}
table.button.alert:hover table a {
border: 0 solid #e23317;
}
table.button.warning table td {
background: #ffae00;
border: 0px solid #ffae00;
}
table.button.warning table a {
border: 0px solid #ffae00;
}
table.button.warning:hover table td {
background: #cc8b00;
}
table.button.warning:hover table a {
border: 0px solid #cc8b00;
}
table.callout {
margin-bottom: 16px;
Margin-bottom: 16px;
}
th.callout-inner {
width: 100%;
border: 1px solid #cbcbcb;
padding: 10px;
background: #fefefe;
}
th.callout-inner.primary {
background: #def0fc;
border: 1px solid #444444;
color: #0a0a0a;
}
th.callout-inner.secondary {
background: #ebebeb;
border: 1px solid #444444;
color: #0a0a0a;
}
th.callout-inner.success {
background: #e1faea;
border: 1px solid #1b9448;
color: #fefefe;
}
th.callout-inner.warning {
background: #fff3d9;
border: 1px solid #996800;
color: #fefefe;
}
th.callout-inner.alert {
background: #fce6e2;
border: 1px solid #b42912;
color: #fefefe;
}
.thumbnail {
border: solid 4px #fefefe;
box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2);
display: inline-block;
line-height: 0;
max-width: 100%;
transition: box-shadow 200ms ease-out;
border-radius: 3px;
margin-bottom: 16px;
}
.thumbnail:hover,
.thumbnail:focus {
box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5);
}
table.menu {
width: 580px;
}
table.menu td.menu-item,
table.menu th.menu-item {
padding: 10px;
padding-right: 10px;
}
table.menu td.menu-item a,
table.menu th.menu-item a {
color: #2199e8;
}
table.menu.vertical td.menu-item,
table.menu.vertical th.menu-item {
padding: 10px;
padding-right: 0;
display: block;
}
table.menu.vertical td.menu-item a,
table.menu.vertical th.menu-item a {
width: 100%;
}
table.menu.vertical td.menu-item table.menu.vertical td.menu-item,
table.menu.vertical td.menu-item table.menu.vertical th.menu-item,
table.menu.vertical th.menu-item table.menu.vertical td.menu-item,
table.menu.vertical th.menu-item table.menu.vertical th.menu-item {
padding-left: 10px;
}
table.menu.text-center a {
text-align: center;
}
.menu[align="center"] {
width: auto !important;
}
body.outlook p {
display: inline !important;
}
@media only screen and (max-width: 596px) {
table.body img {
width: auto;
height: auto;
}
table.body center {
min-width: 0 !important;
}
table.body .container {
width: 95% !important;
}
table.body .columns,
table.body .column {
height: auto !important;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding-left: 16px !important;
padding-right: 16px !important;
}
table.body .columns .column,
table.body .columns .columns,
table.body .column .column,
table.body .column .columns {
padding-left: 0 !important;
padding-right: 0 !important;
}
table.body .collapse .columns,
table.body .collapse .column {
padding-left: 0 !important;
padding-right: 0 !important;
}
td.small-1,
th.small-1 {
display: inline-block !important;
width: 8.33333% !important;
}
td.small-2,
th.small-2 {
display: inline-block !important;
width: 16.66667% !important;
}
td.small-3,
th.small-3 {
display: inline-block !important;
width: 25% !important;
}
td.small-4,
th.small-4 {
display: inline-block !important;
width: 33.33333% !important;
}
td.small-5,
th.small-5 {
display: inline-block !important;
width: 41.66667% !important;
}
td.small-6,
th.small-6 {
display: inline-block !important;
width: 50% !important;
}
td.small-7,
th.small-7 {
display: inline-block !important;
width: 58.33333% !important;
}
td.small-8,
th.small-8 {
display: inline-block !important;
width: 66.66667% !important;
}
td.small-9,
th.small-9 {
display: inline-block !important;
width: 75% !important;
}
td.small-10,
th.small-10 {
display: inline-block !important;
width: 83.33333% !important;
}
td.small-11,
th.small-11 {
display: inline-block !important;
width: 91.66667% !important;
}
td.small-12,
th.small-12 {
display: inline-block !important;
width: 100% !important;
}
.columns td.small-12,
.column td.small-12,
.columns th.small-12,
.column th.small-12 {
display: block !important;
width: 100% !important;
}
table.body td.small-offset-1,
table.body th.small-offset-1 {
margin-left: 8.33333% !important;
Margin-left: 8.33333% !important;
}
table.body td.small-offset-2,
table.body th.small-offset-2 {
margin-left: 16.66667% !important;
Margin-left: 16.66667% !important;
}
table.body td.small-offset-3,
table.body th.small-offset-3 {
margin-left: 25% !important;
Margin-left: 25% !important;
}
table.body td.small-offset-4,
table.body th.small-offset-4 {
margin-left: 33.33333% !important;
Margin-left: 33.33333% !important;
}
table.body td.small-offset-5,
table.body th.small-offset-5 {
margin-left: 41.66667% !important;
Margin-left: 41.66667% !important;
}
table.body td.small-offset-6,
table.body th.small-offset-6 {
margin-left: 50% !important;
Margin-left: 50% !important;
}
table.body td.small-offset-7,
table.body th.small-offset-7 {
margin-left: 58.33333% !important;
Margin-left: 58.33333% !important;
}
table.body td.small-offset-8,
table.body th.small-offset-8 {
margin-left: 66.66667% !important;
Margin-left: 66.66667% !important;
}
table.body td.small-offset-9,
table.body th.small-offset-9 {
margin-left: 75% !important;
Margin-left: 75% !important;
}
table.body td.small-offset-10,
table.body th.small-offset-10 {
margin-left: 83.33333% !important;
Margin-left: 83.33333% !important;
}
table.body td.small-offset-11,
table.body th.small-offset-11 {
margin-left: 91.66667% !important;
Margin-left: 91.66667% !important;
}
table.body table.columns td.expander,
table.body table.columns th.expander {
display: none !important;
}
table.body .right-text-pad,
table.body .text-pad-right {
padding-left: 10px !important;
}
table.body .left-text-pad,
table.body .text-pad-left {
padding-right: 10px !important;
}
table.menu {
width: 100% !important;
}
table.menu td,
table.menu th {
width: auto !important;
display: inline-block !important;
}
table.menu.vertical td,
table.menu.vertical th,
table.menu.small-vertical td,
table.menu.small-vertical th {
display: block !important;
}
table.menu[align="center"] {
width: auto !important;
}
table.button.small-expand,
table.button.small-expanded {
width: 100% !important;
}
table.button.small-expand table,
table.button.small-expanded table {
width: 100%;
}
table.button.small-expand table a,
table.button.small-expanded table a {
text-align: center !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
table.button.small-expand center,
table.button.small-expanded center {
min-width: 0;
}
}

View File

@ -0,0 +1,67 @@
{% apply inky_to_html|inline_css %}
<html>
<head>
<style>
{% block style %}
{{ source("@email/zurb_2/main.css") }}
{{ source("@email/zurb_2/notification/local.css") }}
{% endblock %}
</style>
</head>
<body>
<spacer size="32"></spacer>
<wrapper class="body">
<container class="body_{{ ("urgent" == importance ? "alert" : ("high" == importance ? "warning" : "default")) }}">
<spacer size="16"></spacer>
<row>
<columns large="12" small="12">
{% block lead %}
{% if importance is not null %}<small><strong>{{ importance|upper }}</strong></small>{% endif %}
<p class="lead">
{{ email.subject }}
</p>
{% endblock %}
{% block content %}
{% if markdown %}
{{ include('@email/zurb_2/notification/content_markdown.html.twig') }}
{% else %}
{{ (raw ? content|raw : content)|nl2br }}
{% endif %}
{% endblock %}
{% block action %}
{% if action_url %}
<spacer size="16"></spacer>
<button href="{{ action_url }}">{{ action_text }}</button>
{% endif %}
{% endblock %}
{% block exception %}
{% if exception %}
<spacer size="16"></spacer>
<p><em>Exception stack trace attached.</em></p>
{% endif %}
{% endblock %}
</columns>
</row>
<wrapper class="secondary">
<spacer size="16"></spacer>
{% block footer %}
{% if footer_text is defined and footer_text is not null %}
<row>
<columns small="12" large="6">
{% block footer_content %}
<p><small>{{ footer_text }}</small></p>
{% endblock %}
</columns>
</row>
{% endif %}
{% endblock %}
</wrapper>
</container>
</wrapper>
</body>
</html>
{% endapply %}

View File

@ -0,0 +1,20 @@
{% block lead %}
{{ email.subject }}
{% endblock %}
{% block content %}
{{ content }}
{% endblock %}
{% block action %}
{% if action_url %}
{{ action_text }}: {{ action_url }}
{% endif %}
{% endblock %}
{% block exception %}
{% if exception %}
Exception stack trace attached.
{{ exception }}
{% endif %}
{% endblock %}

View File

@ -0,0 +1 @@
{{ content|markdown_to_html }}

View File

@ -0,0 +1,19 @@
body {
background: #f3f3f3;
}
.wrapper.secondary {
background: #f3f3f3;
}
.container.body_alert {
border-top: 8px solid #ec5840;
}
.container.body_warning {
border-top: 8px solid #ffae00;
}
.container.body_default {
border-top: 8px solid #aaaaaa;
}

View File

@ -0,0 +1,71 @@
{% use "bootstrap_3_layout.html.twig" %}
{% block form_start -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-horizontal')|trim}) %}
{{- parent() -}}
{%- endblock form_start %}
{# Labels #}
{% block form_label -%}
{%- if label is same as(false) -%}
<div class="{{ block('form_label_class') }}"></div>
{%- else -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ block('form_label_class'))|trim}) -%}
{{- parent() -}}
{%- endif -%}
{%- endblock form_label %}
{% block form_label_class -%}
col-sm-2
{%- endblock form_label_class %}
{# Rows #}
{% block form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ ((not compound or force_error|default(false)) and not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
{##}</div>
{%- endblock form_row %}
{% block submit_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock submit_row %}
{% block reset_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock reset_row %}
{% block form_group_class -%}
col-sm-10
{%- endblock form_group_class %}
{% block checkbox_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ (not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>{#--#}
</div>
{%- endblock checkbox_row %}

View File

@ -0,0 +1,216 @@
{% use "bootstrap_base_layout.html.twig" %}
{# Widgets #}
{% block form_widget_simple -%}
{% if type is not defined or type not in ['file', 'hidden'] %}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{% endif %}
{{- parent() -}}
{%- endblock form_widget_simple %}
{% block button_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-default') ~ ' btn')|trim}) -%}
{{- parent() -}}
{%- endblock button_widget %}
{% block money_widget -%}
{% set prepend = not (money_pattern starts with '{{') %}
{% set append = not (money_pattern ends with '}}') %}
{% if prepend or append %}
<div class="input-group">
{% if prepend %}
<span class="input-group-addon">{{ money_pattern|form_encode_currency }}</span>
{% endif %}
{{- block('form_widget_simple') -}}
{% if append %}
<span class="input-group-addon">{{ money_pattern|form_encode_currency }}</span>
{% endif %}
</div>
{% else %}
{{- block('form_widget_simple') -}}
{% endif %}
{%- endblock money_widget %}
{% block checkbox_widget -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{% if 'checkbox-inline' in parent_label_class %}
{{- form_label(form, null, { widget: parent() }) -}}
{% else -%}
<div class="checkbox">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- endif -%}
{%- endblock checkbox_widget %}
{% block radio_widget -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- if 'radio-inline' in parent_label_class -%}
{{- form_label(form, null, { widget: parent() }) -}}
{%- else -%}
<div class="radio">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- endif -%}
{%- endblock radio_widget %}
{% block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed %}
{# Labels #}
{% block form_label -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' control-label')|trim}) -%}
{{- parent() -}}
{%- endblock form_label %}
{% block choice_label -%}
{# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #}
{%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) -%}
{{- block('form_label') -}}
{% endblock %}
{% block checkbox_label -%}
{%- set label_attr = label_attr|merge({'for': id}) -%}
{{- block('checkbox_radio_label') -}}
{%- endblock checkbox_label %}
{% block radio_label -%}
{%- set label_attr = label_attr|merge({'for': id}) -%}
{{- block('checkbox_radio_label') -}}
{%- endblock radio_label %}
{% 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 -%}
{%- if required -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|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 -%}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{#- if statement must be kept on the same line, to force the space between widget and label -#}
{{- widget|raw }} {% 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 -%}
{%- endif -%}
</label>
{%- endif -%}
{%- endblock checkbox_radio_label %}
{# Rows #}
{% block form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ ((not compound or force_error|default(false)) and not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) }} {# -#}
{{ form_widget(form, widget_attr) }} {# -#}
{{- form_help(form) -}}
{{ form_errors(form) }} {# -#}
</div> {# -#}
{%- endblock form_row %}
{% block button_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
</div>
{%- endblock button_row %}
{% block choice_row -%}
{% set force_error = true %}
{{- block('form_row') }}
{%- endblock choice_row %}
{% block date_row -%}
{% set force_error = true %}
{{- block('form_row') }}
{%- endblock date_row %}
{% block time_row -%}
{% set force_error = true %}
{{- block('form_row') }}
{%- endblock time_row %}
{% block datetime_row -%}
{% set force_error = true %}
{{- block('form_row') }}
{%- endblock datetime_row %}
{% block checkbox_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ (not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock checkbox_row %}
{% block radio_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group' ~ (not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock radio_row %}
{# Errors #}
{% block form_errors -%}
{% if errors|length > 0 -%}
{% if form is not rootform %}<span class="help-block">{% else %}<div class="alert alert-danger">{% endif %}
<ul class="list-unstyled">
{%- for error in errors -%}
<li><span class="glyphicon glyphicon-exclamation-sign"></span> {{ error.message }}</li>
{%- endfor -%}
</ul>
{% if form is not rootform %}</span>{% else %}</div>{% endif %}
{%- endif %}
{%- endblock form_errors %}
{# Help #}
{% block form_help -%}
{%- if help is not empty -%}
{%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ ' help-block')|trim}) -%}
<span 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 -%}
</span>
{%- endif -%}
{%- endblock form_help %}

View File

@ -0,0 +1,87 @@
{% use "bootstrap_4_layout.html.twig" %}
{# Labels #}
{% block form_label -%}
{%- if label is same as(false) -%}
<div class="{{ block('form_label_class') }}"></div>
{%- else -%}
{%- if expanded is not defined or not expanded -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label')|trim}) -%}
{%- endif -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ block('form_label_class'))|trim}) -%}
{{- parent() -}}
{%- endif -%}
{%- endblock form_label %}
{% block form_label_class -%}
col-sm-2
{%- endblock form_label_class %}
{# Rows #}
{% block form_row -%}
{%- if expanded is defined and expanded -%}
{{ block('fieldset_form_row') }}
{%- else -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group row' ~ ((not compound or force_error|default(false)) and not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
</div>
{##}</div>
{%- endif -%}
{%- endblock form_row %}
{% block fieldset_form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<fieldset{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
<div class="row{% if (not compound or force_error|default(false)) and not valid %} is-invalid{% endif %}">
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
</div>
</div>
{##}</fieldset>
{%- endblock fieldset_form_row %}
{% block submit_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock submit_row %}
{% block reset_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock reset_row %}
{% block form_group_class -%}
col-sm-10
{%- endblock form_group_class %}
{% block checkbox_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
{{- form_help(form) -}}
</div>{#--#}
</div>
{%- endblock checkbox_row %}

View File

@ -0,0 +1,371 @@
{% use "bootstrap_base_layout.html.twig" %}
{# Widgets #}
{% block money_widget -%}
{%- set prepend = not (money_pattern starts with '{{') -%}
{%- set append = not (money_pattern ends with '}}') -%}
{%- if prepend or append -%}
<div class="input-group{{ group_class|default('') }}">
{%- if prepend -%}
<div class="input-group-prepend">
<span class="input-group-text">{{ money_pattern|form_encode_currency }}</span>
</div>
{%- endif -%}
{{- block('form_widget_simple') -}}
{%- if append -%}
<div class="input-group-append">
<span class="input-group-text">{{ money_pattern|form_encode_currency }}</span>
</div>
{%- endif -%}
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock money_widget %}
{% block datetime_widget -%}
{%- if widget != 'single_text' and not valid -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control is-invalid')|trim}) -%}
{% set valid = true %}
{%- endif -%}
{{- parent() -}}
{%- endblock datetime_widget %}
{% block date_widget -%}
{%- if widget != 'single_text' and not valid -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control is-invalid')|trim}) -%}
{% set valid = true %}
{%- endif -%}
{{- parent() -}}
{%- endblock date_widget %}
{% block time_widget -%}
{%- if widget != 'single_text' and not valid -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control is-invalid')|trim}) -%}
{% set valid = true %}
{%- endif -%}
{{- parent() -}}
{%- endblock time_widget %}
{% block dateinterval_widget -%}
{%- if widget != 'single_text' and not valid -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control is-invalid')|trim}) -%}
{% set valid = true %}
{%- endif -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
<div {{ block('widget_container_attributes') }}>
{%- if with_years -%}
<div class="col-auto">
{{ form_label(form.years) }}
{{ form_widget(form.years) }}
</div>
{%- endif -%}
{%- if with_months -%}
<div class="col-auto">
{{ form_label(form.months) }}
{{ form_widget(form.months) }}
</div>
{%- endif -%}
{%- if with_weeks -%}
<div class="col-auto">
{{ form_label(form.weeks) }}
{{ form_widget(form.weeks) }}
</div>
{%- endif -%}
{%- if with_days -%}
<div class="col-auto">
{{ form_label(form.days) }}
{{ form_widget(form.days) }}
</div>
{%- endif -%}
{%- if with_hours -%}
<div class="col-auto">
{{ form_label(form.hours) }}
{{ form_widget(form.hours) }}
</div>
{%- endif -%}
{%- if with_minutes -%}
<div class="col-auto">
{{ form_label(form.minutes) }}
{{ form_widget(form.minutes) }}
</div>
{%- endif -%}
{%- if with_seconds -%}
<div class="col-auto">
{{ form_label(form.seconds) }}
{{ form_widget(form.seconds) }}
</div>
{%- endif -%}
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
</div>
{%- endif -%}
{%- endblock dateinterval_widget %}
{% block percent_widget -%}
{%- if symbol -%}
<div class="input-group">
{{- block('form_widget_simple') -}}
<div class="input-group-append">
<span class="input-group-text">{{ symbol|default('%') }}</span>
</div>
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock percent_widget %}
{% block file_widget -%}
<{{ element|default('div') }} class="custom-file">
{%- set type = type|default('file') -%}
{%- set input_lang = 'en' -%}
{% if app is defined and app.request is defined %}{%- set input_lang = app.request.locale -%}{%- endif -%}
{%- set attr = {lang: input_lang} | merge(attr) -%}
{{- block('form_widget_simple') -}}
{%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim })|filter((value, key) => key != 'id') -%}
<label for="{{ form.vars.id }}" {% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if attr.placeholder is defined and attr.placeholder is not none -%}
{{- translation_domain is same as(false) ? attr.placeholder : attr.placeholder|trans({}, translation_domain) -}}
{%- endif -%}
</label>
</{{ element|default('div') }}>
{% endblock %}
{% block form_widget_simple -%}
{%- if type is not defined or type != 'hidden' -%}
{%- set className = ' form-control' -%}
{%- if type|default('') == 'file' -%}
{%- set className = ' custom-file-input' -%}
{%- elseif type|default('') == 'range' -%}
{%- set className = ' form-control-range' -%}
{%- endif -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ className)|trim}) -%}
{%- endif -%}
{%- if type is defined and (type == 'range' or type == 'color') %}
{# Attribute "required" is not supported #}
{%- set required = false -%}
{% endif %}
{{- parent() -}}
{%- endblock form_widget_simple %}
{% block widget_attributes -%}
{%- if not valid -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) %}
{%- endif -%}
{{ parent() }}
{%- endblock widget_attributes %}
{% block button_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-secondary') ~ ' btn')|trim}) -%}
{{- parent() -}}
{%- endblock button_widget %}
{% block submit_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-primary'))|trim}) -%}
{{- parent() -}}
{%- endblock submit_widget %}
{% block checkbox_widget -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- if 'checkbox-custom' in parent_label_class -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' custom-control-input')|trim}) -%}
<div class="custom-control custom-checkbox{{ 'checkbox-inline' in parent_label_class ? ' custom-control-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- elseif 'switch-custom' in parent_label_class -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' custom-control-input')|trim}) -%}
<div class="custom-control custom-switch{{ 'switch-inline' in parent_label_class ? ' custom-control-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
<div class="form-check{{ 'checkbox-inline' in parent_label_class ? ' form-check-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- endif -%}
{%- endblock checkbox_widget %}
{% block radio_widget -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- if 'radio-custom' in parent_label_class -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' custom-control-input')|trim}) -%}
<div class="custom-control custom-radio{{ 'radio-inline' in parent_label_class ? ' custom-control-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
<div class="form-check{{ 'radio-inline' in parent_label_class ? ' form-check-inline' }}">
{{- form_label(form, null, { widget: parent() }) -}}
</div>
{%- endif -%}
{%- endblock radio_widget %}
{% block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed %}
{% block choice_widget_expanded -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}
{{- form_widget(child, {
parent_label_class: label_attr.class|default(''),
translation_domain: choice_translation_domain,
valid: valid,
}) -}}
{% endfor -%}
</div>
{%- endblock choice_widget_expanded %}
{# Labels #}
{% block form_label -%}
{% if label is not same as(false) -%}
{%- if compound is defined and compound -%}
{%- set element = 'legend' -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label')|trim}) -%}
{%- else -%}
{%- set label_attr = label_attr|merge({for: id}) -%}
{%- endif -%}
{% 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_errors %}{{- form_errors(form) -}}{% endblock form_label_errors %}</{{ element|default('label') }}>
{%- else -%}
{%- if errors|length > 0 -%}
<div id="{{ id }}_errors" class="mb-2">
{{- form_errors(form) -}}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock form_label %}
{% 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 is_parent_custom = parent_label_class is defined and ('checkbox-custom' in parent_label_class or 'radio-custom' in parent_label_class or 'switch-custom' in parent_label_class) %}
{% set is_custom = label_attr.class is defined and ('checkbox-custom' in label_attr.class or 'radio-custom' in label_attr.class or 'switch-custom' in label_attr.class) %}
{%- if is_parent_custom or is_custom -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' custom-control-label')|trim}) -%}
{%- else %}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-check-label')|trim}) -%}
{%- endif %}
{%- if not compound -%}
{% set label_attr = label_attr|merge({'for': id}) %}
{%- endif -%}
{%- if required -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|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 -%}
{%- endif -%}
{{- form_errors(form) -}}
</label>
{%- endif -%}
{%- endblock checkbox_radio_label %}
{# Rows #}
{% block form_row -%}
{%- if compound is defined and compound -%}
{%- set element = 'fieldset' -%}
{%- endif -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
</{{ element|default('div') }}>
{%- endblock form_row %}
{# Errors #}
{% block form_errors -%}
{%- if errors|length > 0 -%}
<span class="{% if form is not rootform %}invalid-feedback{% else %}alert alert-danger{% endif %} d-block">
{%- for error in errors -%}
<span class="d-block">
<span class="form-error-icon badge badge-danger text-uppercase">{{ 'Error'|trans({}, 'validators') }}</span> <span class="form-error-message">{{ error.message }}</span>
</span>
{%- endfor -%}
</span>
{%- endif %}
{%- endblock form_errors %}
{# Help #}
{% block form_help -%}
{%- 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 -%}
</small>
{%- endif -%}
{%- endblock form_help %}

View File

@ -0,0 +1,130 @@
{% use "bootstrap_5_layout.html.twig" %}
{# Labels #}
{% block form_label -%}
{%- if label is same as(false) -%}
<div class="{{ block('form_label_class') }}"></div>
{%- else -%}
{%- set row_class = row_class|default(row_attr.class|default('')) -%}
{%- if 'form-floating' not in row_class and 'input-group' not in row_class -%}
{%- if expanded is not defined or not expanded -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label')|trim}) -%}
{%- endif -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ block('form_label_class'))|trim}) -%}
{%- endif -%}
{{- parent() -}}
{%- endif -%}
{%- endblock form_label %}
{% block form_label_class -%}
col-sm-2
{%- endblock form_label_class %}
{# Rows #}
{% block form_row -%}
{%- if expanded is defined and expanded -%}
{{ block('fieldset_form_row') }}
{%- else -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
{%- set row_class = row_class|default(row_attr.class|default('mb-3')) -%}
{%- set is_form_floating = is_form_floating|default('form-floating' in row_class) -%}
{%- set is_input_group = is_input_group|default('input-group' in row_class) -%}
{#- Remove behavior class from the main container -#}
{%- set row_class = row_class|replace({'form-floating': '', 'input-group': ''}) -%}
<div{% with {attr: row_attr|merge({class: (row_class ~ ' row' ~ ((not compound or force_error|default(false)) and not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
{%- if is_form_floating or is_input_group -%}
<div class="{{ block('form_label_class') }}"></div>
<div class="{{ block('form_group_class') }}">
{%- if is_form_floating -%}
<div class="form-floating">
{{- form_widget(form, widget_attr) -}}
{{- form_label(form) -}}
</div>
{%- elseif is_input_group -%}
<div class="input-group">
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{#- Hack to properly display help with input group -#}
{{- form_help(form) -}}
</div>
{%- endif -%}
{%- if not is_input_group -%}
{{- form_help(form) -}}
{%- endif -%}
{{- form_errors(form) -}}
</div>
{%- else -%}
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
{%- endif -%}
{##}</div>
{%- endif -%}
{%- endblock form_row %}
{% block fieldset_form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<fieldset{% with {attr: row_attr|merge({class: row_attr.class|default('mb-3')|trim})} %}{{ block('attributes') }}{% endwith %}>
<div class="row{% if (not compound or force_error|default(false)) and not valid %} is-invalid{% endif %}">
{{- form_label(form) -}}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
</div>
</fieldset>
{%- endblock fieldset_form_row %}
{% block submit_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock submit_row %}
{% block reset_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock reset_row %}
{% block button_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
</div>{#--#}
</div>
{%- endblock button_row %}
{% block checkbox_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('mb-3') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>{#--#}
<div class="{{ block('form_label_class') }}"></div>{#--#}
<div class="{{ block('form_group_class') }}">
{{- form_widget(form) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>{#--#}
</div>
{%- endblock checkbox_row %}
{% block form_group_class -%}
col-sm-10
{%- endblock form_group_class %}

View File

@ -0,0 +1,374 @@
{% use "bootstrap_base_layout.html.twig" %}
{# Widgets #}
{% block money_widget -%}
{%- set prepend = not (money_pattern starts with '{{') -%}
{%- set append = not (money_pattern ends with '}}') -%}
{%- if prepend or append -%}
<div class="input-group{{ group_class|default('') }}">
{%- if prepend -%}
<span class="input-group-text">{{ money_pattern|form_encode_currency }}</span>
{%- endif -%}
{{- block('form_widget_simple') -}}
{%- if append -%}
<span class="input-group-text">{{ money_pattern|form_encode_currency }}</span>
{%- endif -%}
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock money_widget %}
{% block date_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
{%- if datetime is not defined or not datetime -%}
<div {{ block('widget_container_attributes') -}}>
{%- endif %}
{%- if label is not same as(false) -%}
<div class="visually-hidden">
{{- form_label(form.year) -}}
{{- form_label(form.month) -}}
{{- form_label(form.day) -}}
</div>
{%- endif -%}
<div class="input-group">
{{- date_pattern|replace({
'{{ year }}': form_widget(form.year),
'{{ month }}': form_widget(form.month),
'{{ day }}': form_widget(form.day),
})|raw -}}
</div>
{%- if datetime is not defined or not datetime -%}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock date_widget %}
{% block time_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
{%- if datetime is not defined or false == datetime -%}
<div {{ block('widget_container_attributes') -}}>
{%- endif -%}
{%- if label is not same as(false) -%}
<div class="visually-hidden">
{{- form_label(form.hour) -}}
{%- if with_minutes -%}{{ form_label(form.minute) }}{%- endif -%}
{%- if with_seconds -%}{{ form_label(form.second) }}{%- endif -%}
</div>
{%- endif -%}
{% if with_minutes or with_seconds %}
<div class="input-group">
{% endif %}
{{- form_widget(form.hour) -}}
{%- if with_minutes -%}
<span class="input-group-text">:</span>
{{- form_widget(form.minute) -}}
{%- endif -%}
{%- if with_seconds -%}
<span class="input-group-text">:</span>
{{- form_widget(form.second) -}}
{%- endif -%}
{% if with_minutes or with_seconds %}
</div>
{% endif %}
{%- if datetime is not defined or false == datetime -%}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock time_widget %}
{% block datetime_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
<div {{ block('widget_container_attributes') }}>
{{- form_widget(form.date, { datetime: true } ) -}}
{{- form_errors(form.date) -}}
{{- form_widget(form.time, { datetime: true } ) -}}
{{- form_errors(form.time) -}}
</div>
{%- endif -%}
{%- endblock datetime_widget %}
{% block dateinterval_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) -%}
{% set valid = true %}
{% endif %}
<div {{ block('widget_container_attributes') }}>
{%- if with_years -%}
<div class="col-auto mb-3">
{{ form_label(form.years) }}
{{ form_widget(form.years) }}
</div>
{%- endif -%}
{%- if with_months -%}
<div class="col-auto mb-3">
{{ form_label(form.months) }}
{{ form_widget(form.months) }}
</div>
{%- endif -%}
{%- if with_weeks -%}
<div class="col-auto mb-3">
{{ form_label(form.weeks) }}
{{ form_widget(form.weeks) }}
</div>
{%- endif -%}
{%- if with_days -%}
<div class="col-auto mb-3">
{{ form_label(form.days) }}
{{ form_widget(form.days) }}
</div>
{%- endif -%}
{%- if with_hours -%}
<div class="col-auto mb-3">
{{ form_label(form.hours) }}
{{ form_widget(form.hours) }}
</div>
{%- endif -%}
{%- if with_minutes -%}
<div class="col-auto mb-3">
{{ form_label(form.minutes) }}
{{ form_widget(form.minutes) }}
</div>
{%- endif -%}
{%- if with_seconds -%}
<div class="col-auto mb-3">
{{ form_label(form.seconds) }}
{{ form_widget(form.seconds) }}
</div>
{%- endif -%}
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
</div>
{%- endif -%}
{%- endblock dateinterval_widget %}
{% block percent_widget -%}
{%- if symbol -%}
<div class="input-group">
{{- block('form_widget_simple') -}}
<span class="input-group-text">{{ symbol|default('%') }}</span>
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock percent_widget %}
{% block form_widget_simple -%}
{%- if type is not defined or type != 'hidden' %}
{%- set widget_class = ' form-control' %}
{%- if type|default('') == 'color' -%}
{%- set widget_class = widget_class ~ ' form-control-color' -%}
{%- elseif type|default('') == 'range' -%}
{%- set widget_class = ' form-range' -%}
{%- endif -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ widget_class)|trim}) -%}
{% endif -%}
{%- if type is defined and type in ['range', 'color'] %}
{# Attribute "required" is not supported #}
{% set required = false %}
{% endif -%}
{{- parent() -}}
{%- endblock form_widget_simple %}
{%- block widget_attributes -%}
{%- if not valid %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) %}
{% endif -%}
{{ parent() }}
{%- endblock widget_attributes -%}
{%- block button_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-secondary') ~ ' btn')|trim}) -%}
{{- parent() -}}
{%- endblock button_widget %}
{%- block submit_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('btn-primary'))|trim}) -%}
{{- parent() -}}
{%- endblock submit_widget %}
{%- block checkbox_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|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>
{%- endblock checkbox_widget %}
{%- block radio_widget -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|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>
{%- endblock radio_widget %}
{%- block choice_widget_collapsed -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-select')|trim}) -%}
{{- parent() -}}
{%- endblock choice_widget_collapsed -%}
{%- block choice_widget_expanded -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}
{{- form_widget(child, {
parent_label_class: label_attr.class|default(''),
translation_domain: choice_translation_domain,
valid: valid,
}) -}}
{% endfor -%}
</div>
{%- endblock choice_widget_expanded %}
{# Labels #}
{%- block form_label -%}
{% if label is not same as(false) -%}
{%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%}
{%- if compound is defined and compound -%}
{%- set element = 'legend' -%}
{%- if 'col-form-label' not in parent_label_class -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' col-form-label' )|trim}) -%}
{%- endif -%}
{%- else -%}
{%- set row_class = row_class|default(row_attr.class|default('')) -%}
{%- set label_attr = label_attr|merge({for: id}) -%}
{%- if 'col-form-label' not in parent_label_class -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ('input-group' in row_class ? ' input-group-text' : ' form-label') )|trim}) -%}
{%- endif -%}
{%- endif -%}
{%- endif -%}
{{- parent() -}}
{%- endblock form_label %}
{%- 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}) -%}
{%- if not compound -%}
{% set label_attr = label_attr|merge({'for': id}) %}
{%- endif -%}
{%- if required -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) -%}
{%- endif -%}
{%- 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 -%}
{%- endif -%}
</label>
{%- endif -%}
{%- endblock checkbox_radio_label %}
{# Rows #}
{%- block form_row -%}
{%- if compound is defined and compound -%}
{%- set element = 'fieldset' -%}
{%- endif -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
{%- set row_class = row_class|default(row_attr.class|default('mb-3')|trim) -%}
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: row_class})} %}{{ block('attributes') }}{% endwith %}>
{%- if 'form-floating' in row_class -%}
{{- form_widget(form, widget_attr) -}}
{{- form_label(form) -}}
{%- else -%}
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{%- endif -%}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</{{ element|default('div') }}>
{%- endblock form_row %}
{%- block button_row -%}
<div{% with {attr: row_attr|merge({class: row_attr.class|default('mb-3')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
</div>
{%- endblock button_row %}
{# Errors #}
{%- block form_errors -%}
{%- if errors|length > 0 -%}
{%- for error in errors -%}
<div class="invalid-feedback d-block">{{ error.message }}</div>
{%- endfor -%}
{%- endif %}
{%- endblock form_errors %}
{# Help #}
{%- block form_help -%}
{% set row_class = row_attr.class|default('') %}
{% set help_class = ' form-text' %}
{% if 'input-group' in row_class %}
{#- Hack to properly display help with input group -#}
{% set help_class = ' input-group-text' %}
{% endif %}
{%- if help is not empty -%}
{%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ help_class ~ ' mb-0')|trim}) -%}
{%- endif -%}
{{- parent() -}}
{%- endblock form_help %}

View File

@ -0,0 +1,208 @@
{% use "form_div_layout.html.twig" %}
{# Widgets #}
{% block textarea_widget -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %}
{{- parent() -}}
{%- endblock textarea_widget %}
{% block money_widget -%}
{% set prepend = not (money_pattern starts with '{{') %}
{% set append = not (money_pattern ends with '}}') %}
{% if prepend or append %}
<div class="input-group{{ group_class|default('') }}">
{% if prepend %}
<span class="input-group-addon">{{ money_pattern|form_encode_currency }}</span>
{% endif %}
{{- block('form_widget_simple') -}}
{% if append %}
<span class="input-group-addon">{{ money_pattern|form_encode_currency }}</span>
{% endif %}
</div>
{% else %}
{{- block('form_widget_simple') -}}
{% endif %}
{%- endblock money_widget %}
{% block percent_widget -%}
{%- if symbol -%}
<div class="input-group">
{{- block('form_widget_simple') -}}
<span class="input-group-addon">{{ symbol|default('%') }}</span>
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock percent_widget %}
{% block datetime_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
<div {{ block('widget_container_attributes') }}>
{{- form_errors(form.date) -}}
{{- form_errors(form.time) -}}
<div class="sr-only">
{%- if form.date.year is defined %}{{ form_label(form.date.year) }}{% endif -%}
{%- if form.date.month is defined %}{{ form_label(form.date.month) }}{% endif -%}
{%- if form.date.day is defined %}{{ form_label(form.date.day) }}{% endif -%}
{%- if form.time.hour is defined %}{{ form_label(form.time.hour) }}{% endif -%}
{%- if form.time.minute is defined %}{{ form_label(form.time.minute) }}{% endif -%}
{%- if form.time.second is defined %}{{ form_label(form.time.second) }}{% endif -%}
</div>
{{- form_widget(form.date, { datetime: true } ) -}}
{{- form_widget(form.time, { datetime: true } ) -}}
</div>
{%- endif -%}
{%- endblock datetime_widget %}
{% block date_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
{%- if datetime is not defined or not datetime -%}
<div {{ block('widget_container_attributes') -}}>
{%- endif %}
{%- if label is not same as(false) -%}
<div class="sr-only">
{{ form_label(form.year) }}
{{ form_label(form.month) }}
{{ form_label(form.day) }}
</div>
{%- endif -%}
{{- date_pattern|replace({
'{{ year }}': form_widget(form.year),
'{{ month }}': form_widget(form.month),
'{{ day }}': form_widget(form.day),
})|raw -}}
{%- if datetime is not defined or not datetime -%}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock date_widget %}
{% block time_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
{%- if datetime is not defined or false == datetime -%}
<div {{ block('widget_container_attributes') -}}>
{%- endif -%}
{%- if label is not same as(false) -%}<div class="sr-only">{{ form_label(form.hour) }}</div>{%- endif -%}
{{- form_widget(form.hour) -}}
{%- if with_minutes -%}:{%- if label is not same as(false) -%}<div class="sr-only">{{ form_label(form.minute) }}</div>{%- endif -%}{{ form_widget(form.minute) }}{%- endif -%}
{%- if with_seconds -%}:{%- if label is not same as(false) -%}<div class="sr-only">{{ form_label(form.second) }}</div>{%- endif -%}{{ form_widget(form.second) }}{%- endif -%}
{%- if datetime is not defined or false == datetime -%}
</div>
{%- endif -%}
{%- endif -%}
{%- endblock time_widget %}
{%- block dateinterval_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%}
<div {{ block('widget_container_attributes') }}>
{{- form_errors(form) -}}
<div class="table-responsive">
<table class="table {{ table_class|default('table-bordered table-condensed table-striped') }}" role="presentation">
<thead>
<tr>
{%- if with_years %}<th>{{ form_label(form.years) }}</th>{% endif -%}
{%- if with_months %}<th>{{ form_label(form.months) }}</th>{% endif -%}
{%- if with_weeks %}<th>{{ form_label(form.weeks) }}</th>{% endif -%}
{%- if with_days %}<th>{{ form_label(form.days) }}</th>{% endif -%}
{%- if with_hours %}<th>{{ form_label(form.hours) }}</th>{% endif -%}
{%- if with_minutes %}<th>{{ form_label(form.minutes) }}</th>{% endif -%}
{%- if with_seconds %}<th>{{ form_label(form.seconds) }}</th>{% endif -%}
</tr>
</thead>
<tbody>
<tr>
{%- if with_years %}<td>{{ form_widget(form.years) }}</td>{% endif -%}
{%- if with_months %}<td>{{ form_widget(form.months) }}</td>{% endif -%}
{%- if with_weeks %}<td>{{ form_widget(form.weeks) }}</td>{% endif -%}
{%- if with_days %}<td>{{ form_widget(form.days) }}</td>{% endif -%}
{%- if with_hours %}<td>{{ form_widget(form.hours) }}</td>{% endif -%}
{%- if with_minutes %}<td>{{ form_widget(form.minutes) }}</td>{% endif -%}
{%- if with_seconds %}<td>{{ form_widget(form.seconds) }}</td>{% endif -%}
</tr>
</tbody>
</table>
</div>
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
</div>
{%- endif -%}
{%- endblock dateinterval_widget -%}
{% block choice_widget_expanded -%}
{%- if '-inline' in label_attr.class|default('') -%}
{%- for child in form %}
{{- form_widget(child, {
parent_label_class: label_attr.class|default(''),
translation_domain: choice_translation_domain,
}) -}}
{% endfor -%}
{%- else -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}
{{- form_widget(child, {
parent_label_class: label_attr.class|default(''),
translation_domain: choice_translation_domain,
}) -}}
{%- endfor -%}
</div>
{%- endif -%}
{%- endblock choice_widget_expanded %}
{# Labels #}
{% block choice_label -%}
{# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #}
{%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': '', 'switch-custom': ''})|trim}) -%}
{{- block('form_label') -}}
{% endblock choice_label %}
{% block checkbox_label -%}
{{- block('checkbox_radio_label') -}}
{%- endblock checkbox_label %}
{% block radio_label -%}
{{- block('checkbox_radio_label') -}}
{%- endblock radio_label %}
{# Rows #}
{% block button_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
</div>
{%- endblock button_row %}
{% block choice_row -%}
{%- set force_error = true -%}
{{- block('form_row') -}}
{%- endblock choice_row %}
{% block date_row -%}
{%- set force_error = true -%}
{{- block('form_row') -}}
{%- endblock date_row %}
{% block time_row -%}
{%- set force_error = true -%}
{{- block('form_row') -}}
{%- endblock time_row %}
{% block datetime_row -%}
{%- set force_error = true -%}
{{- block('form_row') -}}
{%- endblock datetime_row %}

View File

@ -0,0 +1,476 @@
{# Widgets #}
{%- block form_widget -%}
{% if compound %}
{{- block('form_widget_compound') -}}
{% else %}
{{- block('form_widget_simple') -}}
{% endif %}
{%- endblock form_widget -%}
{%- block form_widget_simple -%}
{%- set type = type|default('text') -%}
{%- if type == 'range' or type == 'color' -%}
{# Attribute "required" is not supported #}
{%- set required = false -%}
{%- endif -%}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{%- endblock form_widget_simple -%}
{%- block form_widget_compound -%}
<div {{ block('widget_container_attributes') }}>
{%- if form is rootform -%}
{{ form_errors(form) }}
{%- endif -%}
{{- block('form_rows') -}}
{{- form_rest(form) -}}
</div>
{%- endblock form_widget_compound -%}
{%- block collection_widget -%}
{% if prototype is defined and not prototype.rendered %}
{%- set attr = attr|merge({'data-prototype': form_row(prototype) }) -%}
{% endif %}
{{- block('form_widget') -}}
{%- endblock collection_widget -%}
{%- block textarea_widget -%}
<textarea {{ block('widget_attributes') }}>{{ value }}</textarea>
{%- endblock textarea_widget -%}
{%- block choice_widget -%}
{% if expanded %}
{{- block('choice_widget_expanded') -}}
{% else %}
{{- block('choice_widget_collapsed') -}}
{% endif %}
{%- endblock choice_widget -%}
{%- block choice_widget_expanded -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}
{{- form_widget(child) -}}
{{- form_label(child, null, {translation_domain: choice_translation_domain}) -}}
{% endfor -%}
</div>
{%- endblock choice_widget_expanded -%}
{%- block choice_widget_collapsed -%}
{%- if required and placeholder is none and not placeholder_in_choices and not multiple and (attr.size is not defined or attr.size <= 1) -%}
{% set required = false %}
{%- 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>
{%- endif -%}
{%- if preferred_choices|length > 0 -%}
{% set options = preferred_choices %}
{% set render_preferred_choices = true %}
{{- block('choice_widget_options') -}}
{%- if choices|length > 0 and separator is not none -%}
<option disabled="disabled">{{ separator }}</option>
{%- endif -%}
{%- endif -%}
{%- set options = choices -%}
{%- set render_preferred_choices = false -%}
{{- block('choice_widget_options') -}}
</select>
{%- endblock choice_widget_collapsed -%}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ choice_translation_domain is same as(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
<option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if not render_preferred_choices|default(false) and choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans(choice.labelTranslationParameters, choice_translation_domain) }}</option>
{%- endif -%}
{% endfor %}
{%- 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 %} />
{%- endblock checkbox_widget -%}
{%- block radio_widget -%}
<input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock radio_widget -%}
{%- block datetime_widget -%}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{%- else -%}
<div {{ block('widget_container_attributes') }}>
{{- form_errors(form.date) -}}
{{- form_errors(form.time) -}}
{{- form_widget(form.date) -}}
{{- form_widget(form.time) -}}
</div>
{%- endif -%}
{%- endblock datetime_widget -%}
{%- block date_widget -%}
{%- if widget == 'single_text' -%}
{{ block('form_widget_simple') }}
{%- else -%}
<div {{ block('widget_container_attributes') }}>
{{- date_pattern|replace({
'{{ year }}': form_widget(form.year),
'{{ month }}': form_widget(form.month),
'{{ day }}': form_widget(form.day),
})|raw -}}
</div>
{%- endif -%}
{%- endblock date_widget -%}
{%- block time_widget -%}
{%- if widget == 'single_text' -%}
{{ block('form_widget_simple') }}
{%- else -%}
{%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%}
<div {{ block('widget_container_attributes') }}>
{{ form_widget(form.hour, vars) }}{% if with_minutes %}:{{ form_widget(form.minute, vars) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second, vars) }}{% endif %}
</div>
{%- endif -%}
{%- endblock time_widget -%}
{%- block dateinterval_widget -%}
{%- if widget == 'single_text' -%}
{{- block('form_widget_simple') -}}
{%- else -%}
<div {{ block('widget_container_attributes') }}>
{{- form_errors(form) -}}
<table class="{{ table_class|default('') }}" role="presentation">
<thead>
<tr>
{%- if with_years %}<th>{{ form_label(form.years) }}</th>{% endif -%}
{%- if with_months %}<th>{{ form_label(form.months) }}</th>{% endif -%}
{%- if with_weeks %}<th>{{ form_label(form.weeks) }}</th>{% endif -%}
{%- if with_days %}<th>{{ form_label(form.days) }}</th>{% endif -%}
{%- if with_hours %}<th>{{ form_label(form.hours) }}</th>{% endif -%}
{%- if with_minutes %}<th>{{ form_label(form.minutes) }}</th>{% endif -%}
{%- if with_seconds %}<th>{{ form_label(form.seconds) }}</th>{% endif -%}
</tr>
</thead>
<tbody>
<tr>
{%- if with_years %}<td>{{ form_widget(form.years) }}</td>{% endif -%}
{%- if with_months %}<td>{{ form_widget(form.months) }}</td>{% endif -%}
{%- if with_weeks %}<td>{{ form_widget(form.weeks) }}</td>{% endif -%}
{%- if with_days %}<td>{{ form_widget(form.days) }}</td>{% endif -%}
{%- if with_hours %}<td>{{ form_widget(form.hours) }}</td>{% endif -%}
{%- if with_minutes %}<td>{{ form_widget(form.minutes) }}</td>{% endif -%}
{%- if with_seconds %}<td>{{ form_widget(form.seconds) }}</td>{% endif -%}
</tr>
</tbody>
</table>
{%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%}
</div>
{%- endif -%}
{%- endblock dateinterval_widget -%}
{%- block number_widget -%}
{# type="number" doesn't work with floats in localized formats #}
{%- set type = type|default('text') -%}
{{ block('form_widget_simple') }}
{%- endblock number_widget -%}
{%- block integer_widget -%}
{%- set type = type|default('number') -%}
{{ block('form_widget_simple') }}
{%- endblock integer_widget -%}
{%- block money_widget -%}
{{ money_pattern|form_encode_currency(block('form_widget_simple')) }}
{%- endblock money_widget -%}
{%- block url_widget -%}
{%- set type = type|default('url') -%}
{{ block('form_widget_simple') }}
{%- endblock url_widget -%}
{%- block search_widget -%}
{%- set type = type|default('search') -%}
{{ block('form_widget_simple') }}
{%- endblock search_widget -%}
{%- block percent_widget -%}
{%- set type = type|default('text') -%}
{{ block('form_widget_simple') }}{% if symbol %} {{ symbol|default('%') }}{% endif %}
{%- endblock percent_widget -%}
{%- block password_widget -%}
{%- set type = type|default('password') -%}
{{ block('form_widget_simple') }}
{%- endblock password_widget -%}
{%- block hidden_widget -%}
{%- set type = type|default('hidden') -%}
{{ block('form_widget_simple') }}
{%- endblock hidden_widget -%}
{%- block email_widget -%}
{%- set type = type|default('email') -%}
{{ block('form_widget_simple') }}
{%- endblock email_widget -%}
{%- block range_widget -%}
{% set type = type|default('range') %}
{{- block('form_widget_simple') -}}
{%- endblock range_widget %}
{%- block button_widget -%}
{%- if label is empty -%}
{%- if label_format is not empty -%}
{% set label = label_format|replace({
'%name%': name,
'%id%': id,
}) %}
{%- elseif label is not same as(false) -%}
{% set label = name|humanize %}
{%- endif -%}
{%- endif -%}
<button type="{{ type|default('button') }}" {{ block('button_attributes') }}>
{%- 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 -%}
</button>
{%- endblock button_widget -%}
{%- block submit_widget -%}
{%- set type = type|default('submit') -%}
{{ block('button_widget') }}
{%- endblock submit_widget -%}
{%- block reset_widget -%}
{%- set type = type|default('reset') -%}
{{ block('button_widget') }}
{%- endblock reset_widget -%}
{%- block tel_widget -%}
{%- set type = type|default('tel') -%}
{{ block('form_widget_simple') }}
{%- endblock tel_widget -%}
{%- block color_widget -%}
{%- set type = type|default('color') -%}
{{ block('form_widget_simple') }}
{%- endblock color_widget -%}
{%- block week_widget -%}
{%- if widget == 'single_text' -%}
{{ block('form_widget_simple') }}
{%- else -%}
{%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%}
<div {{ block('widget_container_attributes') }}>
{{ form_widget(form.year, vars) }}-{{ form_widget(form.week, vars) }}
</div>
{%- endif -%}
{%- endblock week_widget -%}
{# Labels #}
{%- block form_label -%}
{% if label is not same as(false) -%}
{% if not compound -%}
{% set label_attr = label_attr|merge({'for': id}) %}
{%- endif -%}
{% 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 -%}
</{{ element|default('label') }}>
{%- endif -%}
{%- endblock form_label -%}
{%- block button_label -%}{%- endblock -%}
{# Help #}
{% 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>
{%- endif -%}
{%- endblock form_help %}
{# Rows #}
{%- block repeated_row -%}
{#
No need to render the errors here, as all errors are mapped
to the first child (see RepeatedTypeValidatorExtension).
#}
{{- block('form_rows') -}}
{%- endblock repeated_row -%}
{%- block form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
{{- form_errors(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
</div>
{%- endblock form_row -%}
{%- block button_row -%}
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
</div>
{%- endblock button_row -%}
{%- block hidden_row -%}
{{ form_widget(form) }}
{%- endblock hidden_row -%}
{# Misc #}
{%- block form -%}
{{ form_start(form) }}
{{- form_widget(form) -}}
{{ form_end(form) }}
{%- endblock form -%}
{%- block form_start -%}
{%- do form.setMethodRendered() -%}
{% set method = method|upper %}
{%- if method in ["GET", "POST"] -%}
{% set form_method = method %}
{%- else -%}
{% set form_method = "POST" %}
{%- 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 }}" />
{%- endif -%}
{%- endblock form_start -%}
{%- block form_end -%}
{%- if not render_rest is defined or render_rest -%}
{{ form_rest(form) }}
{%- endif -%}
</form>
{%- endblock form_end -%}
{%- block form_errors -%}
{%- if errors|length > 0 -%}
<ul>
{%- for error in errors -%}
<li>{{ error.message }}</li>
{%- endfor -%}
</ul>
{%- endif -%}
{%- endblock form_errors -%}
{%- block form_rest -%}
{% for child in form -%}
{% if not child.rendered %}
{{- form_row(child) -}}
{% endif %}
{%- endfor -%}
{% if not form.methodRendered and form is rootform %}
{%- do form.setMethodRendered() -%}
{% set method = method|upper %}
{%- if method in ["GET", "POST"] -%}
{% set form_method = method %}
{%- else -%}
{% set form_method = "POST" %}
{%- endif -%}
{%- if form_method != method -%}
<input type="hidden" name="_method" value="{{ method }}" />
{%- endif -%}
{% endif -%}
{% endblock form_rest %}
{# Support #}
{%- block form_rows -%}
{% for child in form|filter(child => not child.rendered) %}
{{- form_row(child) -}}
{% endfor %}
{%- endblock form_rows -%}
{%- block widget_attributes -%}
id="{{ id }}" name="{{ full_name }}"
{%- if disabled %} disabled="disabled"{% endif -%}
{%- if required %} required="required"{% endif -%}
{{ block('attributes') }}
{%- endblock widget_attributes -%}
{%- block widget_container_attributes -%}
{%- if id is not empty %}id="{{ id }}"{% endif -%}
{{ block('attributes') }}
{%- endblock widget_container_attributes -%}
{%- block button_attributes -%}
id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif -%}
{{ block('attributes') }}
{%- endblock button_attributes -%}
{% block attributes -%}
{%- for attrname, attrvalue in attr -%}
{{- " " -}}
{%- if attrname in ['placeholder', 'title'] -%}
{{- attrname }}="{{ translation_domain is same as(false) or attrvalue is null ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain) }}"
{%- elseif attrvalue is same as(true) -%}
{{- attrname }}="{{ attrname }}"
{%- elseif attrvalue is not same as(false) -%}
{{- attrname }}="{{ attrvalue }}"
{%- endif -%}
{%- endfor -%}
{%- endblock attributes -%}

View File

@ -0,0 +1,50 @@
{% use "form_div_layout.html.twig" %}
{%- block form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<tr{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
<td>
{{- form_label(form) -}}
</td>
<td>
{{- form_errors(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
</td>
</tr>
{%- endblock form_row -%}
{%- block button_row -%}
<tr{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
<td></td>
<td>
{{- form_widget(form) -}}
</td>
</tr>
{%- endblock button_row -%}
{%- block hidden_row -%}
{%- set style = row_attr.style is defined ? (row_attr.style ~ (row_attr.style|trim|last != ';' ? '; ')) -%}
<tr{% with {attr: row_attr|merge({style: (style ~ ' display: none')|trim})} %}{{ block('attributes') }}{% endwith %}>
<td colspan="2">
{{- form_widget(form) -}}
</td>
</tr>
{%- endblock hidden_row -%}
{%- block form_widget_compound -%}
<table {{ block('widget_container_attributes') }}>
{%- if form is rootform and errors|length > 0 -%}
<tr>
<td colspan="2">
{{- form_errors(form) -}}
</td>
</tr>
{%- endif -%}
{{- block('form_rows') -}}
{{- form_rest(form) -}}
</table>
{%- endblock form_widget_compound -%}

View File

@ -0,0 +1,340 @@
{% extends "form_div_layout.html.twig" %}
{# Based on Foundation 5 Doc #}
{# Widgets #}
{% block form_widget_simple -%}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{{- parent() -}}
{%- endblock form_widget_simple %}
{% block textarea_widget -%}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{{- parent() -}}
{%- endblock textarea_widget %}
{% block button_widget -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' button')|trim}) %}
{{- parent() -}}
{%- endblock button_widget %}
{% block money_widget -%}
<div class="row collapse">
{% set prepend = '{{' == money_pattern[0:2] %}
{% if not prepend %}
<div class="small-3 large-2 columns">
<span class="prefix">{{ money_pattern|form_encode_currency }}</span>
</div>
{% endif %}
<div class="small-9 large-10 columns">
{{- block('form_widget_simple') -}}
</div>
{% if prepend %}
<div class="small-3 large-2 columns">
<span class="postfix">{{ money_pattern|form_encode_currency }}</span>
</div>
{% endif %}
</div>
{%- endblock money_widget %}
{% block percent_widget -%}
<div class="row collapse">
{%- if symbol -%}
<div class="small-9 large-10 columns">
{{- block('form_widget_simple') -}}
</div>
<div class="small-3 large-2 columns">
<span class="postfix">{{ symbol|default('%') }}</span>
</div>
{%- else -%}
<div class="small-12 large-12 columns">
{{- block('form_widget_simple') -}}
</div>
{%- endif -%}
</div>
{%- endblock percent_widget %}
{% block datetime_widget -%}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{% else %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' row')|trim}) %}
<div class="row">
<div class="large-7 columns">{{ form_errors(form.date) }}</div>
<div class="large-5 columns">{{ form_errors(form.time) }}</div>
</div>
<div {{ block('widget_container_attributes') }}>
<div class="large-7 columns">{{ form_widget(form.date, { datetime: true } ) }}</div>
<div class="large-5 columns">{{ form_widget(form.time, { datetime: true } ) }}</div>
</div>
{% endif %}
{%- endblock datetime_widget %}
{% block date_widget -%}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{% else %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' row')|trim}) %}
{% if datetime is not defined or not datetime %}
<div {{ block('widget_container_attributes') }}>
{% endif %}
{{- date_pattern|replace({
'{{ year }}': '<div class="large-4 columns">' ~ form_widget(form.year) ~ '</div>',
'{{ month }}': '<div class="large-4 columns">' ~ form_widget(form.month) ~ '</div>',
'{{ day }}': '<div class="large-4 columns">' ~ form_widget(form.day) ~ '</div>',
})|raw -}}
{% if datetime is not defined or not datetime %}
</div>
{% endif %}
{% endif %}
{%- endblock date_widget %}
{% block time_widget -%}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{% else %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' row')|trim}) %}
{% if datetime is not defined or false == datetime %}
<div {{ block('widget_container_attributes') -}}>
{% endif %}
{% if with_seconds %}
<div class="large-4 columns">{{ form_widget(form.hour) }}</div>
<div class="large-4 columns">
<div class="row collapse">
<div class="small-3 large-2 columns">
<span class="prefix">:</span>
</div>
<div class="small-9 large-10 columns">
{{ form_widget(form.minute) }}
</div>
</div>
</div>
<div class="large-4 columns">
<div class="row collapse">
<div class="small-3 large-2 columns">
<span class="prefix">:</span>
</div>
<div class="small-9 large-10 columns">
{{ form_widget(form.second) }}
</div>
</div>
</div>
{% else %}
<div class="large-6 columns">{{ form_widget(form.hour) }}</div>
<div class="large-6 columns">
<div class="row collapse">
<div class="small-3 large-2 columns">
<span class="prefix">:</span>
</div>
<div class="small-9 large-10 columns">
{{ form_widget(form.minute) }}
</div>
</div>
</div>
{% endif %}
{% if datetime is not defined or false == datetime %}
</div>
{% endif %}
{% endif %}
{%- endblock time_widget %}
{% block choice_widget_collapsed -%}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{% if multiple -%}
{% set attr = attr|merge({style: (attr.style|default('') ~ ' height: auto; background-image: none;')|trim}) %}
{% endif %}
{% if required and placeholder is none and not placeholder_in_choices and not multiple -%}
{% set required = false %}
{%- 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>
{%- endif %}
{%- if preferred_choices|length > 0 -%}
{% set options = preferred_choices %}
{% set render_preferred_choices = true %}
{{- block('choice_widget_options') -}}
{% if choices|length > 0 and separator is not none -%}
<option disabled="disabled">{{ separator }}</option>
{%- endif %}
{%- endif -%}
{% set options = choices -%}
{%- set render_preferred_choices = false -%}
{{- block('choice_widget_options') -}}
</select>
{%- endblock choice_widget_collapsed %}
{% block choice_widget_expanded -%}
{% if '-inline' in label_attr.class|default('') %}
<ul class="inline-list">
{% for child in form %}
<li>{{ form_widget(child, {
parent_label_class: label_attr.class|default(''),
}) }}</li>
{% endfor %}
</ul>
{% else %}
<div {{ block('widget_container_attributes') }}>
{% for child in form %}
{{ form_widget(child, {
parent_label_class: label_attr.class|default(''),
}) }}
{% endfor %}
</div>
{% endif %}
{%- endblock choice_widget_expanded %}
{% block checkbox_widget -%}
{% set parent_label_class = parent_label_class|default('') %}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{% if 'checkbox-inline' in parent_label_class %}
{{ form_label(form, null, { widget: parent() }) }}
{% else %}
<div class="checkbox">
{{ form_label(form, null, { widget: parent() }) }}
</div>
{% endif %}
{%- endblock checkbox_widget %}
{% block radio_widget -%}
{% set parent_label_class = parent_label_class|default('') %}
{% if 'radio-inline' in parent_label_class %}
{{ form_label(form, null, { widget: parent() }) }}
{% else %}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
<div class="radio">
{{ form_label(form, null, { widget: parent() }) }}
</div>
{% endif %}
{%- endblock radio_widget %}
{# Labels #}
{% block form_label -%}
{% if errors|length > 0 -%}
{% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{{- parent() -}}
{%- endblock form_label %}
{% block choice_label -%}
{% if errors|length > 0 -%}
{% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #}
{% set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) %}
{{- block('form_label') -}}
{%- endblock choice_label %}
{% block checkbox_label -%}
{{- block('checkbox_radio_label') -}}
{%- endblock checkbox_label %}
{% block radio_label -%}
{{- block('checkbox_radio_label') -}}
{%- endblock radio_label %}
{% block checkbox_radio_label -%}
{% if required %}
{% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) %}
{% endif %}
{% if errors|length > 0 -%}
{% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' error')|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 %}
<label{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{{ widget|raw }}
{{ translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain) }}
</label>
{%- endblock checkbox_radio_label %}
{# Rows #}
{% block form_row -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>
<div class="large-12 columns{% if (not compound or force_error|default(false)) and not valid %} error{% endif %}">
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}
</div>
</div>
{%- endblock form_row %}
{% block choice_row -%}
{% set force_error = true %}
{{ block('form_row') }}
{%- endblock choice_row %}
{% block date_row -%}
{% set force_error = true %}
{{ block('form_row') }}
{%- endblock date_row %}
{% block time_row -%}
{% set force_error = true %}
{{ block('form_row') }}
{%- endblock time_row %}
{% block datetime_row -%}
{% set force_error = true %}
{{ block('form_row') }}
{%- endblock datetime_row %}
{% block checkbox_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>
<div class="large-12 columns{% if not valid %} error{% endif %}">
{{ form_widget(form) }}
{{- form_help(form) -}}
{{ form_errors(form) }}
</div>
</div>
{%- endblock checkbox_row %}
{% block radio_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' row')|trim})} %}{{ block('attributes') }}{% endwith %}>
<div class="large-12 columns{% if not valid %} error{% endif %}">
{{ form_widget(form) }}
{{- form_help(form) -}}
{{ form_errors(form) }}
</div>
</div>
{%- endblock radio_row %}
{# Errors #}
{% block form_errors -%}
{% if errors|length > 0 -%}
{% if form is not rootform %}<small class="error">{% else %}<div data-alert class="alert-box alert">{% endif %}
{%- for error in errors -%}
{{ error.message }}
{% if not loop.last %}, {% endif %}
{%- endfor -%}
{% if form is not rootform %}</small>{% else %}</div>{% endif %}
{%- endif %}
{%- endblock form_errors %}

View File

@ -0,0 +1,50 @@
{% extends "form_div_layout.html.twig" %}
{%- block checkbox_row -%}
{%- set parent_class = parent_class|default(attr.class|default('')) -%}
{%- if 'switch-input' in parent_class -%}
{{- form_label(form) -}}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' switch-input')|trim}) -%}
{{- form_widget(form) -}}
<label class="switch-paddle" for="{{ form.vars.id }}"></label>
{{- form_errors(form) -}}
{%- else -%}
{{- block('form_row') -}}
{%- endif -%}
{%- endblock checkbox_row -%}
{% block money_widget -%}
{% set prepend = not (money_pattern starts with '{{') %}
{% set append = not (money_pattern ends with '}}') %}
{% if prepend or append %}
<div class="input-group">
{% if prepend %}
<span class="input-group-label">{{ money_pattern|form_encode_currency }}</span>
{% endif %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' input-group-field')|trim}) %}
{{- block('form_widget_simple') -}}
{% if append %}
<span class="input-group-label">{{ money_pattern|form_encode_currency }}</span>
{% endif %}
</div>
{% else %}
{{- block('form_widget_simple') -}}
{% endif %}
{%- endblock money_widget %}
{% block percent_widget -%}
{%- if symbol -%}
<div class="input-group">
{% set attr = attr|merge({class: (attr.class|default('') ~ ' input-group-field')|trim}) %}
{{- block('form_widget_simple') -}}
<span class="input-group-label">{{ symbol|default('%') }}</span>
</div>
{%- else -%}
{{- block('form_widget_simple') -}}
{%- endif -%}
{%- endblock percent_widget %}
{% block button_widget -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' button')|trim}) %}
{{- parent() -}}
{%- endblock button_widget %}

View File

@ -0,0 +1,69 @@
{% use 'form_div_layout.html.twig' %}
{%- block form_row -%}
{%- set row_attr = row_attr|merge({ class: row_attr.class|default(row_class|default('mb-6')) }) -%}
{{- parent() -}}
{%- endblock form_row -%}
{%- block widget_attributes -%}
{%- set attr = attr|merge({ class: attr.class|default(widget_class|default('mt-1 w-full')) ~ (disabled ? ' ' ~ widget_disabled_class|default('border-gray-300 text-gray-500')) ~ (errors|length ? ' ' ~ widget_errors_class|default('border-red-700')) }) -%}
{{- parent() -}}
{%- endblock widget_attributes -%}
{%- block form_label -%}
{%- set label_attr = label_attr|merge({ class: label_attr.class|default(label_class|default('block text-gray-800')) }) -%}
{{- parent() -}}
{%- endblock form_label -%}
{%- block form_help -%}
{%- set help_attr = help_attr|merge({ class: help_attr.class|default(help_class|default('mt-1 text-gray-600')) }) -%}
{{- parent() -}}
{%- endblock form_help -%}
{%- block form_errors -%}
{%- if errors|length > 0 -%}
<ul>
{%- for error in errors -%}
<li class="{{ error_item_class|default('text-red-700') }}">{{ error.message }}</li>
{%- endfor -%}
</ul>
{%- endif -%}
{%- endblock form_errors -%}
{%- block choice_widget_expanded -%}
{%- set attr = attr|merge({ class: attr.class|default('mt-2') }) -%}
<div {{ block('widget_container_attributes') }}>
{%- for child in form %}
<div class="flex items-center">
{{- form_widget(child) -}}
{{- form_label(child, null, { translation_domain: choice_translation_domain }) -}}
</div>
{% endfor -%}
</div>
{%- endblock choice_widget_expanded -%}
{%- block checkbox_row -%}
{%- set row_attr = row_attr|merge({ class: row_attr.class|default(row_class|default('mb-6')) }) -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
{{- form_errors(form) -}}
<div class="inline-flex items-center">
{{- form_widget(form, widget_attr) -}}
{{- form_label(form) -}}
</div>
{{- form_help(form) -}}
</div>
{%- endblock checkbox_row -%}
{%- block checkbox_widget -%}
{%- set widget_class = widget_class|default('mr-2') -%}
{{- parent() -}}
{%- endblock checkbox_widget -%}
{%- block radio_widget -%}
{%- set widget_class = widget_class|default('mr-2') -%}
{{- parent() -}}
{%- endblock radio_widget -%}

View File

@ -0,0 +1,53 @@
<?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\TokenParser;
use Symfony\Bridge\Twig\Node\DumpNode;
use Twig\Node\Node;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Token Parser for the 'dump' tag.
*
* Dump variables with:
*
* {% dump %}
* {% dump foo %}
* {% dump foo, bar %}
*
* @author Julien Galenski <julien.galenski@gmail.com>
*/
final class DumpTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$values = null;
if (!$this->parser->getStream()->test(Token::BLOCK_END_TYPE)) {
$values = $this->parser->getExpressionParser()->parseMultitargetExpression();
}
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new DumpNode($this->parser->getVarName(), $values, $token->getLine(), $this->getTag());
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'dump';
}
}

View File

@ -0,0 +1,64 @@
<?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\TokenParser;
use Symfony\Bridge\Twig\Node\FormThemeNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Node;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Token Parser for the 'form_theme' tag.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class FormThemeTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$form = $this->parser->getExpressionParser()->parseExpression();
$only = false;
if ($this->parser->getStream()->test(Token::NAME_TYPE, 'with')) {
$this->parser->getStream()->next();
$resources = $this->parser->getExpressionParser()->parseExpression();
if ($this->parser->getStream()->nextIf(Token::NAME_TYPE, 'only')) {
$only = true;
}
} else {
$resources = new ArrayExpression([], $stream->getCurrent()->getLine());
do {
$resources->addElement($this->parser->getExpressionParser()->parseExpression());
} while (!$stream->test(Token::BLOCK_END_TYPE));
}
$stream->expect(Token::BLOCK_END_TYPE);
return new FormThemeNode($form, $resources, $lineno, $this->getTag(), $only);
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'form_theme';
}
}

View File

@ -0,0 +1,64 @@
<?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\TokenParser;
use Symfony\Bridge\Twig\Node\StopwatchNode;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Node;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Token Parser for the stopwatch tag.
*
* @author Wouter J <wouter@wouterj.nl>
*/
final class StopwatchTokenParser extends AbstractTokenParser
{
protected $stopwatchIsAvailable;
public function __construct(bool $stopwatchIsAvailable)
{
$this->stopwatchIsAvailable = $stopwatchIsAvailable;
}
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
// {% stopwatch 'bar' %}
$name = $this->parser->getExpressionParser()->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);
// {% endstopwatch %}
$body = $this->parser->subparse([$this, 'decideStopwatchEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
if ($this->stopwatchIsAvailable) {
return new StopwatchNode($name, $body, new AssignNameExpression($this->parser->getVarName(), $token->getLine()), $lineno, $this->getTag());
}
return $body;
}
public function decideStopwatchEnd(Token $token): bool
{
return $token->test('endstopwatch');
}
public function getTag(): string
{
return 'stopwatch';
}
}

View File

@ -0,0 +1,45 @@
<?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\TokenParser;
use Symfony\Bridge\Twig\Node\TransDefaultDomainNode;
use Twig\Node\Node;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Token Parser for the 'trans_default_domain' tag.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TransDefaultDomainTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$expr = $this->parser->getExpressionParser()->parseExpression();
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
return new TransDefaultDomainNode($expr, $token->getLine(), $this->getTag());
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'trans_default_domain';
}
}

View File

@ -0,0 +1,95 @@
<?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\TokenParser;
use Symfony\Bridge\Twig\Node\TransNode;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Node;
use Twig\Node\TextNode;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Token Parser for the 'trans' tag.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class TransTokenParser extends AbstractTokenParser
{
/**
* {@inheritdoc}
*/
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$count = null;
$vars = new ArrayExpression([], $lineno);
$domain = null;
$locale = null;
if (!$stream->test(Token::BLOCK_END_TYPE)) {
if ($stream->test('count')) {
// {% trans count 5 %}
$stream->next();
$count = $this->parser->getExpressionParser()->parseExpression();
}
if ($stream->test('with')) {
// {% trans with vars %}
$stream->next();
$vars = $this->parser->getExpressionParser()->parseExpression();
}
if ($stream->test('from')) {
// {% trans from "messages" %}
$stream->next();
$domain = $this->parser->getExpressionParser()->parseExpression();
}
if ($stream->test('into')) {
// {% trans into "fr" %}
$stream->next();
$locale = $this->parser->getExpressionParser()->parseExpression();
} elseif (!$stream->test(Token::BLOCK_END_TYPE)) {
throw new SyntaxError('Unexpected token. Twig was looking for the "with", "from", or "into" keyword.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
}
}
// {% trans %}message{% endtrans %}
$stream->expect(Token::BLOCK_END_TYPE);
$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());
}
$stream->expect(Token::BLOCK_END_TYPE);
return new TransNode($body, $domain, $count, $vars, $locale, $lineno, $this->getTag());
}
public function decideTransFork(Token $token): bool
{
return $token->test(['endtrans']);
}
/**
* {@inheritdoc}
*/
public function getTag(): string
{
return 'trans';
}
}

View File

@ -0,0 +1,104 @@
<?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\Translation;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\MessageCatalogue;
use Twig\Environment;
use Twig\Error\Error;
use Twig\Source;
/**
* TwigExtractor extracts translation messages from a twig template.
*
* @author Michel Salib <michelsalib@hotmail.com>
* @author Fabien Potencier <fabien@symfony.com>
*/
class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface
{
/**
* Default domain for found messages.
*
* @var string
*/
private $defaultDomain = 'messages';
/**
* Prefix for found message.
*
* @var string
*/
private $prefix = '';
private $twig;
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
/**
* {@inheritdoc}
*/
public function extract($resource, MessageCatalogue $catalogue)
{
foreach ($this->extractFiles($resource) as $file) {
try {
$this->extractTemplate(file_get_contents($file->getPathname()), $catalogue);
} catch (Error $e) {
// ignore errors, these should be fixed by using the linter
}
}
}
/**
* {@inheritdoc}
*/
public function setPrefix(string $prefix)
{
$this->prefix = $prefix;
}
protected function extractTemplate(string $template, MessageCatalogue $catalogue)
{
$visitor = $this->twig->getExtension('Symfony\Bridge\Twig\Extension\TranslationExtension')->getTranslationNodeVisitor();
$visitor->enable();
$this->twig->parse($this->twig->tokenize(new Source($template, '')));
foreach ($visitor->getMessages() as $message) {
$catalogue->set(trim($message[0]), $this->prefix.trim($message[0]), $message[1] ?: $this->defaultDomain);
}
$visitor->disable();
}
/**
* @return bool
*/
protected function canBeExtracted(string $file)
{
return $this->isFile($file) && 'twig' === pathinfo($file, \PATHINFO_EXTENSION);
}
/**
* {@inheritdoc}
*/
protected function extractFromDirectory($directory)
{
$finder = new Finder();
return $finder->files()->name('*.twig')->in($directory);
}
}

View File

@ -0,0 +1,107 @@
<?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;
use Symfony\Bundle\FullStack;
use Twig\Error\SyntaxError;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* @internal
*/
class UndefinedCallableHandler
{
private const FILTER_COMPONENTS = [
'humanize' => 'form',
'trans' => 'translation',
'yaml_encode' => 'yaml',
'yaml_dump' => 'yaml',
];
private const FUNCTION_COMPONENTS = [
'asset' => 'asset',
'asset_version' => 'asset',
'dump' => 'debug-bundle',
'encore_entry_link_tags' => 'webpack-encore-bundle',
'encore_entry_script_tags' => 'webpack-encore-bundle',
'expression' => 'expression-language',
'form_widget' => 'form',
'form_errors' => 'form',
'form_label' => 'form',
'form_help' => 'form',
'form_row' => 'form',
'form_rest' => 'form',
'form' => 'form',
'form_start' => 'form',
'form_end' => 'form',
'csrf_token' => 'form',
'logout_url' => 'security-http',
'logout_path' => 'security-http',
'is_granted' => 'security-core',
'link' => 'web-link',
'preload' => 'web-link',
'dns_prefetch' => 'web-link',
'preconnect' => 'web-link',
'prefetch' => 'web-link',
'prerender' => 'web-link',
'workflow_can' => 'workflow',
'workflow_transitions' => 'workflow',
'workflow_has_marked_place' => 'workflow',
'workflow_marked_places' => 'workflow',
];
private const FULL_STACK_ENABLE = [
'form' => 'enable "framework.form"',
'security-core' => 'add the "SecurityBundle"',
'security-http' => 'add the "SecurityBundle"',
'web-link' => 'enable "framework.web_link"',
'workflow' => 'enable "framework.workflows"',
];
/**
* @return TwigFilter|false
*/
public static function onUndefinedFilter(string $name)
{
if (!isset(self::FILTER_COMPONENTS[$name])) {
return false;
}
throw new SyntaxError(self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name]));
}
/**
* @return TwigFunction|false
*/
public static function onUndefinedFunction(string $name)
{
if (!isset(self::FUNCTION_COMPONENTS[$name])) {
return false;
}
if ('webpack-encore-bundle' === self::FUNCTION_COMPONENTS[$name]) {
return new TwigFunction($name, static function () { return ''; });
}
throw new SyntaxError(self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name]));
}
private static function onUndefined(string $name, string $type, string $component): string
{
if (class_exists(FullStack::class) && isset(self::FULL_STACK_ENABLE[$component])) {
return sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name);
}
return sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name);
}
}

View File

@ -0,0 +1,88 @@
{
"name": "symfony/twig-bridge",
"type": "symfony-bridge",
"description": "Provides integration for Twig with various Symfony components",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16",
"symfony/translation-contracts": "^1.1|^2|^3",
"twig/twig": "^2.13|^3.0.4"
},
"require-dev": {
"doctrine/annotations": "^1.12",
"egulias/email-validator": "^2.1.10|^3",
"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.3|^6.0",
"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/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/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",
"twig/cssinliner-extra": "^2.12|^3",
"twig/inky-extra": "^2.12|^3",
"twig/markdown-extra": "^2.12|^3"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/console": "<5.3",
"symfony/form": "<5.3",
"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"
},
"autoload": {
"psr-4": { "Symfony\\Bridge\\Twig\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}