Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3d10b53

Browse filesBrowse files
committed
add and configure TemplateHierarchy + DynamicExtendsTokenParser
1 parent 784af57 commit 3d10b53
Copy full SHA for 3d10b53

File tree

Expand file treeCollapse file tree

22 files changed

+751
-77
lines changed
Filter options
Expand file treeCollapse file tree

22 files changed

+751
-77
lines changed

‎core-bundle/src/DependencyInjection/Compiler/TwigPathsPass.php

Copy file name to clipboardExpand all lines: core-bundle/src/DependencyInjection/Compiler/TwigPathsPass.php
+52-26Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212

1313
namespace Contao\CoreBundle\DependencyInjection\Compiler;
1414

15-
use Contao\CoreBundle\Twig\Interop\ContaoTwigTemplateLocator;
15+
use Contao\CoreBundle\Twig\Inheritance\ContaoTwigTemplateLocator;
16+
use Contao\CoreBundle\Twig\Inheritance\TemplateHierarchy;
1617
use Contao\CoreBundle\Twig\Loader\FilesystemLoader;
1718
use Symfony\Component\Config\Resource\FileExistenceResource;
1819
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
20-
use Symfony\Component\DependencyInjection\Definition;
2121

2222
/**
2323
* @internal
@@ -26,18 +26,18 @@ class TwigPathsPass implements CompilerPassInterface
2626
{
2727
public function process(ContainerBuilder $container): void
2828
{
29-
$baseLoader = $container->getDefinition('twig.loader.native_filesystem');
30-
$loader = $container->getDefinition(FilesystemLoader::class);
31-
32-
$this->migrateAddPathCalls($baseLoader, $loader);
33-
$this->registerContaoTwigTemplates($container, $loader);
29+
$this->migrateAddPathCalls($container);
30+
$this->registerContaoTwigTemplates($container);
3431
}
3532

3633
/**
3734
* Rewires the registered "addPath" method calls to our filesystem loader.
3835
*/
39-
private function migrateAddPathCalls(Definition $from, Definition $to): void
36+
private function migrateAddPathCalls(ContainerBuilder $container): void
4037
{
38+
$from = $container->getDefinition('twig.loader.native_filesystem');
39+
$to = $container->getDefinition(FilesystemLoader::class);
40+
4141
$calls = array_filter(
4242
$from->getMethodCalls(),
4343
static function (array $call): bool {
@@ -57,35 +57,61 @@ static function (array $call): bool {
5757
}
5858

5959
/**
60-
* Find template locations for overwritten Contao templates and register
61-
* them under the 'ContaoLegacy' and 'ContaoLegacy_<theme>' namespaces.
60+
* Registers Contao Twig templates and template paths.
6261
*/
63-
private function registerContaoTwigTemplates(ContainerBuilder $container, Definition $loader): void
62+
private function registerContaoTwigTemplates(ContainerBuilder $container): void
6463
{
64+
$loader = $container->getDefinition(FilesystemLoader::class);
65+
$templateHierarchy = $container->getDefinition(TemplateHierarchy::class);
66+
6567
$defaultPath = $container->getParameterBag()->resolveValue('%twig.default_path%');
6668
$bundleMetadata = $container->getParameter('kernel.bundles_metadata');
6769

68-
$locator = new ContaoTwigTemplateLocator();
70+
$templateLocator = new ContaoTwigTemplateLocator();
6971

70-
$basePaths = array_filter(
71-
array_merge(
72-
[$locator->getAppPath($defaultPath)],
73-
array_values($locator->getBundlePaths($bundleMetadata)),
74-
)
75-
);
72+
$registerPath = static function (string $path, string $namespace) use ($loader): void {
73+
$loader->addMethodCall(
74+
'addPath',
75+
[$path, $namespace]
76+
);
77+
};
7678

77-
$addPath = static function (string $path, string $namespace) use ($container, $loader): void {
78-
$container->addResource(new FileExistenceResource($path));
79+
// App paths
80+
foreach ($templateLocator->getAppThemePaths($defaultPath) as $themeSlug => $path) {
81+
$registerPath($path, TemplateHierarchy::getAppThemeNamespace($themeSlug));
7982

80-
$loader->addMethodCall('addPath', [$path, $namespace]);
81-
};
83+
$templateHierarchy->addMethodCall(
84+
'setAppThemeTemplates',
85+
[$templateLocator->findTemplates($path), $themeSlug]
86+
);
87+
}
88+
89+
if (null !== ($path = $templateLocator->getAppPath($defaultPath))) {
90+
$registerPath($path, 'Contao');
91+
$registerPath($path, TemplateHierarchy::getAppNamespace());
92+
93+
$container->addResource(new FileExistenceResource($path));
8294

83-
foreach ($basePaths as $path) {
84-
$addPath($path, 'ContaoLegacy');
95+
$templateHierarchy->addMethodCall(
96+
'setAppTemplates',
97+
[$templateLocator->findTemplates($path)]
98+
);
8599
}
86100

87-
foreach ($locator->getAppThemePaths($defaultPath) as $theme => $path) {
88-
$addPath($path, "ContaoLegacy_$theme");
101+
// Bundle paths (loaded later = higher priority)
102+
foreach (array_reverse($templateLocator->getBundlePaths($bundleMetadata)) as $bundle => $path) {
103+
$registerPath($path, 'Contao');
104+
$registerPath($path, TemplateHierarchy::getBundleNamespace($bundle));
105+
106+
$container->addResource(new FileExistenceResource($path));
107+
108+
$templateHierarchy->addMethodCall(
109+
'setBundleTemplates',
110+
[$templateLocator->findTemplates($path), $bundle]
111+
);
89112
}
113+
114+
// todo: do we need DirectoryResource instead of FileExistenceResource
115+
// for some resources? (tracking changes inside files)
90116
}
91117
}

‎core-bundle/src/Resources/config/services.yml

Copy file name to clipboardExpand all lines: core-bundle/src/Resources/config/services.yml
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -886,11 +886,16 @@ services:
886886
tags:
887887
- { name: kernel.reset, method: reset }
888888

889+
Contao\CoreBundle\Twig\Inheritance\TemplateHierarchy:
890+
arguments:
891+
- '%kernel.bundles_metadata%'
892+
889893
Contao\CoreBundle\Twig\Extension\ImageExtension: ~
890894

891-
Contao\CoreBundle\Twig\Extension\InteropExtension:
895+
Contao\CoreBundle\Twig\Extension\ContaoExtension:
892896
arguments:
893897
- '@twig'
898+
- '@Contao\CoreBundle\Twig\Inheritance\TemplateHierarchy'
894899
public: true
895900

896901
Contao\CoreBundle\Twig\Extension\TextExtension: ~

‎core-bundle/src/Resources/contao/library/Contao/TemplateInheritance.php

Copy file name to clipboardExpand all lines: core-bundle/src/Resources/contao/library/Contao/TemplateInheritance.php
+14-15Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
namespace Contao;
1212

1313
use Contao\CoreBundle\Monolog\ContaoContext;
14-
use Contao\CoreBundle\Twig\Extension\InteropExtension;
14+
use Contao\CoreBundle\Twig\Extension\ContaoExtension;
15+
use Contao\CoreBundle\Twig\Inheritance\TemplateHierarchy;
1516
use Psr\Log\LogLevel;
1617
use Symfony\Component\DependencyInjection\ContainerInterface;
17-
use Webmozart\PathUtil\Path;
1818

1919
/**
2020
* Provides the template inheritance logic
@@ -348,28 +348,27 @@ private function renderTwigSurrogateIfExists(): ?string
348348
}
349349

350350
$templateCandidates = array(
351-
"@ContaoLegacy/{$this->strTemplate}.html.twig",
351+
"@Contao/{$this->strTemplate}.html.twig",
352352
);
353353

354-
if (null !== ($page = $GLOBALS['objPage'] ?? null) && null !== $themePath = $page->templateGroup)
355-
{
356-
// fixme: a 'theme folder' can live anywhere - we should probably
357-
// either slugify the path (including subfolders), use
358-
// another identifier (e.g. slug from from tl_theme.name)
359-
// or add a new reference (e.g. tl_theme.slug)
360-
$theme = Path::makeRelative($themePath, 'templates');
361-
362-
array_unshift(
363-
$templateCandidates,
364-
"@ContaoLegacy_$theme/{$this->strTemplate}.html.twig",
354+
if (
355+
// todo: add alias/slug to tl_theme
356+
null !== ($page = $GLOBALS['objPage'] ?? null) &&
357+
null !== ($theme = ThemeModel::findOneByFolders($page->templateGroup)) &&
358+
!empty($slug = $theme->slug)
359+
) {
360+
$themeNamespace = TemplateHierarchy::getAppThemeNamespace($slug);
361+
362+
array_unshift($templateCandidates,
363+
"@$themeNamespace/{$this->strTemplate}.html.twig",
365364
);
366365
}
367366

368367
foreach ($templateCandidates as $templateCandidate)
369368
{
370369
if ($twig->getLoader()->exists($templateCandidate))
371370
{
372-
$container->get(InteropExtension::class)->registerTemplateForInputEncoding($templateCandidate);
371+
$container->get(ContaoExtension::class)->registerTemplateForInputEncoding($templateCandidate);
373372

374373
return $twig->render($templateCandidate, $this->arrData);
375374
}

‎core-bundle/src/Twig/Extension/InteropExtension.php renamed to ‎core-bundle/src/Twig/Extension/ContaoExtension.php

Copy file name to clipboardExpand all lines: core-bundle/src/Twig/Extension/ContaoExtension.php
+22-2Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,27 @@
1212

1313
namespace Contao\CoreBundle\Twig\Extension;
1414

15+
use Contao\CoreBundle\Twig\Inheritance\DynamicExtendsTokenParser;
16+
use Contao\CoreBundle\Twig\Inheritance\TemplateHierarchy;
1517
use Contao\CoreBundle\Twig\Interop\ContaoEscaper;
1618
use Contao\CoreBundle\Twig\Interop\ContaoEscaperNodeVisitor;
1719
use Twig\Environment;
1820
use Twig\Extension\AbstractExtension;
1921
use Twig\Extension\EscaperExtension;
2022

21-
class InteropExtension extends AbstractExtension
23+
class ContaoExtension extends AbstractExtension
2224
{
25+
/**
26+
* @var TemplateHierarchy
27+
*/
28+
private $templateHierarchy;
29+
2330
/**
2431
* @var array
2532
*/
2633
private $affectedTemplates = [];
2734

28-
public function __construct(Environment $environment)
35+
public function __construct(Environment $environment, TemplateHierarchy $templateHierarchy)
2936
{
3037
/** @var EscaperExtension $escaperExtension */
3138
$escaperExtension = $environment->getExtension(EscaperExtension::class);
@@ -34,6 +41,8 @@ public function __construct(Environment $environment)
3441
'contao_html',
3542
[(new ContaoEscaper()), '__invoke']
3643
);
44+
45+
$this->templateHierarchy = $templateHierarchy;
3746
}
3847

3948
/**
@@ -55,11 +64,22 @@ public function registerTemplateForInputEncoding(string $template): void
5564
public function getNodeVisitors(): array
5665
{
5766
return [
67+
// Enables the 'contao_twig' escaper for Contao templates with
68+
// input encoding
5869
new ContaoEscaperNodeVisitor(
5970
function () {
6071
return $this->affectedTemplates;
6172
}
6273
),
6374
];
6475
}
76+
77+
public function getTokenParsers(): array
78+
{
79+
return [
80+
// Registers a parser for the 'extends' tag which will overwrite
81+
// the one of Twig's CoreExtension
82+
new DynamicExtendsTokenParser($this->templateHierarchy),
83+
];
84+
}
6585
}

‎core-bundle/src/Twig/Interop/ContaoTwigTemplateLocator.php renamed to ‎core-bundle/src/Twig/Inheritance/ContaoTwigTemplateLocator.php

Copy file name to clipboardExpand all lines: core-bundle/src/Twig/Inheritance/ContaoTwigTemplateLocator.php
+26-2Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* @license LGPL-3.0-or-later
1111
*/
1212

13-
namespace Contao\CoreBundle\Twig\Interop;
13+
namespace Contao\CoreBundle\Twig\Inheritance;
1414

1515
use Symfony\Component\Filesystem\Filesystem;
1616
use Symfony\Component\Finder\Finder;
@@ -48,6 +48,7 @@ public function getAppThemePaths(string $twigDefaultPath): array
4848
$themePaths = (new Finder())
4949
->directories()
5050
->in($appPath)
51+
->path('/^@/')
5152
->depth('< 1')
5253
->sortByName()
5354
;
@@ -56,7 +57,7 @@ public function getAppThemePaths(string $twigDefaultPath): array
5657

5758
/** @var SplFileInfo $path */
5859
foreach ($themePaths as $path) {
59-
$paths[$path->getBasename()] = $path->getPathname();
60+
$paths[substr($path->getBasename(), 1)] = $path->getPathname();
6061
}
6162

6263
return $paths;
@@ -81,4 +82,27 @@ public function getBundlePaths(array $bundleMetadata): array
8182

8283
return $paths;
8384
}
85+
86+
/**
87+
* @return array<string, string>
88+
*/
89+
public function findTemplates(string $path): array
90+
{
91+
$files = (new Finder())
92+
->files()
93+
->in($path)
94+
->name('*.html.twig')
95+
->notPath('/^@/')
96+
->sortByName()
97+
;
98+
99+
$templates = [];
100+
101+
/** @var SplFileInfo $file */
102+
foreach ($files as $file) {
103+
$templates[$file->getRelativePathname()] = $file->getPathname();
104+
}
105+
106+
return $templates;
107+
}
84108
}
+89Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Contao.
7+
*
8+
* (c) Leo Feyer
9+
*
10+
* @license LGPL-3.0-or-later
11+
*/
12+
13+
namespace Contao\CoreBundle\Twig\Inheritance;
14+
15+
use Twig\Error\SyntaxError;
16+
use Twig\Node\Expression\ConstantExpression;
17+
use Twig\Node\Node;
18+
use Twig\Token;
19+
use Twig\TokenParser\AbstractTokenParser;
20+
use Twig\TokenStream;
21+
22+
/**
23+
* This parser is a drop in replacement for @\Twig\TokenParser\ExtendsTokenParser.
24+
* It loosely follows the same implementation but additionally dynamically
25+
* rewrites 'extends' expressions to follow the Contao template hierarchy.
26+
*/
27+
class DynamicExtendsTokenParser extends AbstractTokenParser
28+
{
29+
/**
30+
* @var TemplateHierarchy
31+
*/
32+
private $templateHierarchy;
33+
34+
public function __construct(TemplateHierarchy $templateHierarchy)
35+
{
36+
$this->templateHierarchy = $templateHierarchy;
37+
}
38+
39+
public function parse(Token $token): Node
40+
{
41+
$stream = $this->parser->getStream();
42+
43+
if ($this->parser->peekBlockStack()) {
44+
throw new SyntaxError('Cannot use "extend" in a block.', $token->getLine(), $stream->getSourceContext());
45+
}
46+
47+
if (!$this->parser->isMainScope()) {
48+
throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
49+
}
50+
51+
if (null !== $this->parser->getParent()) {
52+
throw new SyntaxError('Multiple extends tags are forbidden.', $token->getLine(), $stream->getSourceContext());
53+
}
54+
55+
$parent = $this->parser->getExpressionParser()->parseExpression();
56+
57+
$this->handleDynamicExtends($parent, $stream);
58+
59+
$this->parser->setParent($parent);
60+
61+
$stream->expect(Token::BLOCK_END_TYPE);
62+
63+
return new Node();
64+
}
65+
66+
public function getTag(): string
67+
{
68+
return 'extends';
69+
}
70+
71+
private function handleDynamicExtends(Node $parent, TokenStream $stream): void
72+
{
73+
if (!$parent instanceof ConstantExpression) {
74+
return;
75+
}
76+
77+
if (1 !== preg_match('%^@Contao/(.*)%', $parent->getAttribute('value'), $matches)) {
78+
return;
79+
}
80+
81+
$sourcePath = $stream->getSourceContext()->getPath();
82+
83+
// Adjust parent template according to the template hierarchy
84+
$parent->setAttribute(
85+
'value',
86+
$this->templateHierarchy->getDynamicParent($matches[1], $sourcePath)
87+
);
88+
}
89+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.