diff --git a/src/ViewTrait.php b/src/ViewTrait.php index 11c19b9e7..bc6fabe92 100644 --- a/src/ViewTrait.php +++ b/src/ViewTrait.php @@ -25,7 +25,9 @@ use function is_file; use function pathinfo; use function str_ends_with; +use function strlen; use function substr; +use function uksort; /** * `ViewTrait` could be used as a base implementation of {@see ViewInterface}. @@ -90,6 +92,8 @@ public function withBasePath(string|null $basePath): static public function withRenderers(array $renderers): static { $new = clone $this; + // Sort by extension length (descending) to match more specific extensions first + uksort($renderers, static fn (string $a, string $b): int => strlen($b) <=> strlen($a)); $new->renderers = $renderers; return $new; } diff --git a/tests/ViewTest.php b/tests/ViewTest.php index cb7283ee9..f00d54eff 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -16,11 +16,13 @@ use Yiisoft\View\Event\View\PageEnd; use Yiisoft\View\Exception\ViewNotFoundException; use Yiisoft\View\PhpTemplateRenderer; +use Yiisoft\View\TemplateRendererInterface; use Yiisoft\View\Tests\TestSupport\TestHelper; use Yiisoft\View\Tests\TestSupport\TestTrait; use Yiisoft\View\Theme; use Yiisoft\View\View; use Yiisoft\View\ViewContextInterface; +use Yiisoft\View\ViewInterface; use function crc32; use function dechex; @@ -253,6 +255,54 @@ public function testDoubleExtensionRenderer(): void ); } + /** + * Test that longer extensions are matched before shorter ones when there are overlapping extensions. + * @link https://github.com/yiisoft/view/pull/291#discussion_r2663151134 + */ + public function testOverlappingExtensionRendererPriority(): void + { + $filename = 'test'; + $baseRenderer = new PhpTemplateRenderer(); + + // Create a renderer that adds a marker to identify which renderer was used + $phpRenderer = new class ($baseRenderer) implements TemplateRendererInterface { + public function __construct(private readonly PhpTemplateRenderer $baseRenderer) + { + } + + public function render(ViewInterface $view, string $template, array $parameters): string + { + return '[php]' . $this->baseRenderer->render($view, $template, $parameters); + } + }; + + $bladePhpRenderer = new class ($baseRenderer) implements TemplateRendererInterface { + public function __construct(private readonly PhpTemplateRenderer $baseRenderer) + { + } + + public function render(ViewInterface $view, string $template, array $parameters): string + { + return '[blade.php]' . $this->baseRenderer->render($view, $template, $parameters); + } + }; + + // Register both "php" and "blade.php" renderers + $view = $this + ->createViewWithBasePath($this->tempDirectory) + ->withContext($this->createContext($this->tempDirectory)) + ->withRenderers([ + 'php' => $phpRenderer, + 'blade.php' => $bladePhpRenderer, + ]); + + file_put_contents("$this->tempDirectory/$filename.blade.php", 'content'); + + // The blade.php renderer should be used because it's more specific (longer extension) + $result = $view->render($filename); + $this->assertStringStartsWith('[blade.php]', $result); + } + public function testLocalize(): void { $view = $this->createViewWithBasePath($this->tempDirectory);