Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Support for multiple paths #314

Open
alexander-schranz opened this issue Aug 22, 2022 · 5 comments
Open

[Feature Request] Support for multiple paths #314

alexander-schranz opened this issue Aug 22, 2022 · 5 comments

Comments

@alexander-schranz
Copy link

alexander-schranz commented Aug 22, 2022

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
Copy link

loilo commented Oct 24, 2022

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
Copy link
Author

alexander-schranz commented Oct 24, 2022

@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
Copy link

loilo commented Oct 24, 2022

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
Copy link
Author

@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
Copy link
Member

dg commented Oct 31, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants