nette / latte

☕ Latte: the safest & truly intuitive templates for PHP. Engine for those who want the most secure PHP sites.
https://latte.nette.org
Other
1.09k stars 107 forks source link

[Feature Request] Support for multiple paths #314

Open alexander-schranz opened 1 year ago

alexander-schranz commented 1 year ago

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 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 1 year ago

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).

alexander-schranz commented 1 year ago

@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 1 year ago

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.

alexander-schranz commented 1 year ago

@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.

dg commented 1 year ago

Related thread https://forum.nette.org/en/33954-including-latte-templates-without-caring-the-directory-structure#p217157