schranz-templating / templating

A template abstraction prototype for php template engines.
MIT License
23 stars 0 forks source link

Add a `TemplateFunctionProviderInterface` for better Framework integration #69

Open alexander-schranz opened 1 year ago

alexander-schranz commented 1 year ago

Beside the TemplateRendererInterface a separate package is in mind for a total different usecase then the exist Renderer. It is targeting Framework authors which want to provide Framework functionality to different Template engines.

Most PHP Frameworks provides for there Template engines some functions commonly is something like generate a URL based on a named Route. In most Template engines this kind of things are handled via Extensions like in Twig or Latte.

There are currently some issues with a TemplateFunctionProviderInterface abstraction.

A. Different implementation of TemplateFunction

The most common Template engines which does not have such implementation of Extension is Blade. Blade work the way that you can access all PHP functions and methods as it is {{ any_method($part) }}, as this is just PHP code which has advantages and disadvantages. Blade is by default used in Laravel which make a lot of usage of Global Function to provide helpers which are used in PHP Code but also in the Template itself. With this opens two questions. A1 should a TemplateFunctionProviderInterface for Blade under Laravel just register Global Function. A2 should a TemplateFunctionProviderInterface for Blade under different decorate Blade emulate global functions. A different way could be used that TemplateFunctionProviderInterface provides in Blade instead of functions Blade directives but as there usage is very different I'm not sure here. Other template engine like Spiral Stempler uses directives more often for things like route generation like here, so how this kind of mechanism should be implemented for this kind of engines is very complicated.

B. Not all Template engines has extensions

This is also the part why it make sense that the whole TemplateFunctionProviderInterface is totally seperate from the Rendering of templates. Library and Framework authors should keep in mind that not all Template engines so they always should have the possibility todo the things already in the controller which is the way why the TemplateRendererInterface is also small and should be keep small. That way this Controller can not support only rendering the content via any template engine but also provide the data as JSON for any API based data receivers.
Still from a Framework perspective a TemplateFunctionProviderInterface make sense so atleast a few engines can has full access to all available Template Functions of a Framework


How could a TemplateFunctionProviderInterface look like?

As background of CMS where a lot of TwigExtension exist we did see a lot difference in performance how many Twig Extensions are registered. This means the TemplateFunctionProviderInterface should solve this issue that providing which functions are available does not required to initialize the Service. It is very common how in twig the twig.runtime works. A TemplateFunctionPRoviderInterface requires a static function this means the class don't need to initialized when it is checked what functions exist. Example:

<?php

namespace App\Templating\FunctionProvider;

use Schranz\Templating\TemplateFunctionProvider\TemplateFunctionProviderInterface;

class MyFunctionProvider implements TemplateFunctionProviderInterface
{
    /**
     * @return iterable<string, string> FCQNorServiceId and Method Name
     */
    public static function getFunctions(): iterable
    {
         return [
              'my_method' => new TemplateFunction(MyFunctionProvider::class, 'myMethod'),
         ];
    }

    public function myMethod(string $text): string
    {
        return $text + '-modified';
    }
}

There will then be a TwigExtension for example which will be a Bridge to all TemplateFunctionProvider and uses a PSR Container to access the service:

namespace App\Twig;

use Psr\Container\ContainerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class TwigFunction extends AbstractExtension
{
    // maybe $providerClasses and $container can be combined but not sure
    public function __construct(private array $providerClasses, ContainerInterface $container)
    {
    }

    public function getFunctions()
    {
        $functions = [];
        foreach ($this->providerClasses as $class) {
             foreach ($class::getFunctions() as $functionName => $function) {
                 $functions[] = new TwigFunction($functionName, function($params...) {
                      $this->callFunctionProvider($function, params);
                 });
             }
        }

        return $functions;
    }

    public function callFunctionProvider(TemplateFunction $function, $params)
    {
        $serviceName = $function->getServiceName();
        $methodName = $function->getMethodName();

        $service = $this->container($serviceName); // only when really used a TemplateProvider will be initialized here

        return $service->{$methodName}(...$params);
    }
}