nette/latte

[Feature Request] Support for multiple paths

alexander-schranz opened this issue · 5 comments

Currently Latte does only provide a FileLoader which allows to bind templates to a single directory.

I think it would be great if Latte would provide additional Loader which allows to register multiple directories via a namespace. So Latte could example use in Laminas Framework as Renderer also by multiple Modules which could registering additional paths.

In twig example the loader accepts multiple paths and the loader will then look one after the other directory.

Another possibility in twig is a namespace example I can have @app/test.html.twig and @other/test.html.twig and register paths like:

[
    'app' => __DIR__ . '/templates',
    'other' => __DIR__ . '/vendor/other/module/templates',
]

While in twig the @ symbol is used, I found while working on my abstraction that other frameworks use the :: syntax for this e.g. other::blade.

A implementation could look like the following:

MultiPathLoader.php
<?php

/**
 * This file is part of the Latte (https://latte.nette.org)
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Latte\Loaders;

use Latte;


/**
 * Template loader.
 */
class MultiPathLoader implements Latte\Loader
{
    private array $loaders = [];


	public function __construct(?array $baseDirs = ['' => null])
	{
        foreach ($baseDirs as $key => $baseDir) {
            $this->loaders[$key] = new FileLoader($baseDir);
        }
	}


	/**
	 * Returns template source code.
	 */
	public function getContent(string $name): string
	{
        [$loader, $name] = $this->extractLoaderAndName($name);

        return $loader->isExpired($name, $name);
	}


	public function isExpired(string $file, int $time): bool
	{
        [$loader, $name] = $this->extractLoaderAndName($file);

        return $loader->isExpired($name, $time);
	}


	/**
	 * Returns referred template name.
	 */
	public function getReferredName(string $name, string $referringName): string
	{
        [$loader, $name] = $this->extractLoaderAndName($name);

        return $loader->getReferredName($name, $referringName);
	}


	/**
	 * Returns unique identifier for caching.
	 */
	public function getUniqueId(string $name): string
	{
        [$loader, $name] = $this->extractLoaderAndName($name);

        return $loader->getUniqueId($name);
	}


    private function extractLoaderAndName(string $name): array
    {
        if (\str_starts_with('@', $name)) {
            // the `@module/template` syntax
            [$loaderKey, $fileName] = \explode('/', substr($name, 1), 2);
            // alternative `module::template` syntax
            [$loaderKey, $fileName] = \explode('::', $name, 2);

            return [
                $this->loaders[$loaderKey],
                $fileName,
            ];
        }

        return [
            $this->loaders[''],
            $name,
        ];
    }
}

What do you think about this. Is this a Loader which you think make sense to live inside Latte Core and you are open for a pull request for it?

loilo commented

I'd consider a more abstracted solution to this. (It introduces a breaking change so it's not anything for the short term, but may be considered for Latte v4.)

In my mind the Latte\Loader interface should have an additional method that indicates whether a template can be handled by the loader (e.g. hasTemplate). This would add a lot of flexibility for writing custom loaders.

For example, it would make it near trivial to write a wrapper loader which walks through a list of loaders like this:
new StackLoader([ $fileLoader1, $fileLoader2, $fallbackStringLoader ]).

Something like this is not really possible today (without relying on getContent() + catching exceptions, which of course adds a lot of overhead to a simple template existence check).

@loilo It is already possible to implement a stack loader without BC breaks. Currently you need to catch the RuntimeException but if that is changed to a TemplateNotFoundException extending RuntimeException it is easy possible to add StackLoader. Sure exist method would be easier but it still not required to implement such a StackLoader.

The target of a namespace is another one, it is not about fallback mechanism. It is about loading template from completely different directories and not some kind of fallback mechanism.

loilo commented

Currently you need to catch the RuntimeException

That's what I described above, it's just a lot of overhead because a template will possibly be rendered with the need for its actual content.

The target of a namespace is another one [...] It is about loading template from completely different directories

I should've read your original issue more carefully, sorry for chiming in like that with only partly related content. My stack loader use case originated from a "multiple paths file loader" approach as well, but did not consider your alias approach properly. Sorry again.

@loilo I think a StackLoader make still sense also in combination with the namespace / MultiPath loader.

Example Symfony allows to overwrite any Bundle templates via a special directory e.g.: templates/<Namespace> and if that not exist it fallbacks to vendor/some/vendor/templates dir like configured.

So in that case it would be something like new StackLoader([new MultiPathLoader(['other' => 'templates/other']), new MultiPathLoader(['other' => 'vendor/some/vendor/templates'])]));.

So a StackLoader and a MultiPath/Namespace Loader make sense. I just would not mix them both into the same class as the target different behaviour.