Skip to content

Commit d9be20c

Browse files
authored
Fix renderer selection for view files with double extensions (#300)
1 parent 533b8b0 commit d9be20c

5 files changed

Lines changed: 121 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Enh #289: Add validation of renderers configuration to `ViewTrait::withRenderers()` (@samdark)
66
- Bug #295: Remove unnecessary `CacheKeyNormalizer` instance creation in `CachedContent` (@samdark)
77
- Enh #295: Minor refactor `ViewTrait::getParameter()` and `ViewTrait::resolveViewFilePath()` (@samdark)
8+
- Bug #300: Fix renderer selection for view files with double extensions (@vjik)
89

910
## 12.2.2 December 07, 2025
1011

src/ViewTrait.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ trait ViewTrait
4444
private array $fallbackExtensions = [self::PHP_EXTENSION];
4545

4646
/**
47-
* @var array A list of available renderers indexed by their corresponding
48-
* supported file extensions.
47+
* @var array A list of available renderers indexed by their corresponding supported file extensions.
48+
*
49+
* The renderers are stored in descending order by extension length (longest first).
50+
* This ensures that more specific extensions (e.g., "blade.php") are matched before
51+
* shorter ones (e.g., "php").
52+
*
4953
* @psalm-var array<string, TemplateRendererInterface>
5054
*/
5155
private array $renderers = [];
@@ -122,6 +126,11 @@ public function withRenderers(array $renderers): static
122126
}
123127
}
124128

129+
uksort(
130+
$renderers,
131+
static fn(string $a, string $b): int => strlen($b) <=> strlen($a)
132+
);
133+
125134
$new = clone $this;
126135
$new->renderers = $renderers;
127136
return $new;
@@ -477,9 +486,7 @@ public function render(string $view, array $parameters = []): string
477486
];
478487

479488
if ($this->beforeRender($viewFile, $parameters)) {
480-
$ext = pathinfo($viewFile, PATHINFO_EXTENSION);
481-
$renderer = $this->renderers[$ext] ?? new PhpTemplateRenderer();
482-
$output = $renderer->render($this, $viewFile, $parameters);
489+
$output = $this->getRenderer($viewFile)->render($this, $viewFile, $parameters);
483490
$output = $this->afterRender($viewFile, $parameters, $output);
484491
}
485492

@@ -568,6 +575,27 @@ abstract protected function createAfterRenderEvent(
568575
string $result
569576
): AfterRenderEventInterface;
570577

578+
/**
579+
* Returns the appropriate template renderer for the given view file.
580+
*
581+
* Selects a renderer based on the view file extension. If no matching renderer is found
582+
* for the file extension, returns a default {@see PhpTemplateRenderer}.
583+
*
584+
* @param string $viewFile The view file path.
585+
*
586+
* @return TemplateRendererInterface The template renderer instance.
587+
*/
588+
private function getRenderer(string $viewFile): TemplateRendererInterface
589+
{
590+
$fileName = basename($viewFile);
591+
foreach ($this->renderers as $extension => $renderer) {
592+
if (str_ends_with($fileName, '.' . $extension)) {
593+
return $renderer;
594+
}
595+
}
596+
return new PhpTemplateRenderer();
597+
}
598+
571599
/**
572600
* This method is invoked right before {@see render()} renders a view file.
573601
*
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\View\Tests\View\DoubleExtension;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Yiisoft\View\PhpTemplateRenderer;
9+
use Yiisoft\View\View;
10+
11+
final class DoubleExtensionTest extends TestCase
12+
{
13+
public function testBase(): void
14+
{
15+
$renderer = new TemplateRendererSpy();
16+
$view = (new View(__DIR__ . '/views'))
17+
->withRenderers([
18+
'blade.php' => $renderer,
19+
]);
20+
21+
$view->render('two.blade.php');
22+
23+
$this->assertSame(
24+
[__DIR__ . '/views/two.blade.php'],
25+
$renderer->getTemplates(),
26+
);
27+
}
28+
29+
public function testWithSeveralRenderers1(): void
30+
{
31+
$renderer = new TemplateRendererSpy();
32+
$view = (new View(__DIR__ . '/views'))
33+
->withRenderers([
34+
'blade.php' => $renderer,
35+
'php' => new PhpTemplateRenderer(),
36+
]);
37+
38+
$view->render('two.blade.php');
39+
40+
$this->assertSame(
41+
[__DIR__ . '/views/two.blade.php'],
42+
$renderer->getTemplates(),
43+
);
44+
}
45+
46+
public function testWithSeveralRenderers2(): void
47+
{
48+
$renderer = new TemplateRendererSpy();
49+
$view = (new View(__DIR__ . '/views'))
50+
->withRenderers([
51+
'php' => new PhpTemplateRenderer(),
52+
'blade.php' => $renderer,
53+
]);
54+
55+
$view->render('two.blade.php');
56+
57+
$this->assertSame(
58+
[__DIR__ . '/views/two.blade.php'],
59+
$renderer->getTemplates(),
60+
);
61+
}
62+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\View\Tests\View\DoubleExtension;
6+
7+
use Yiisoft\View\TemplateRendererInterface;
8+
use Yiisoft\View\ViewInterface;
9+
10+
final class TemplateRendererSpy implements TemplateRendererInterface
11+
{
12+
private array $templates = [];
13+
14+
public function getTemplates(): array
15+
{
16+
return $this->templates;
17+
}
18+
19+
public function render(ViewInterface $view, string $template, array $parameters): string
20+
{
21+
$this->templates[] = $template;
22+
return file_get_contents($template);
23+
}
24+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2

0 commit comments

Comments
 (0)