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

Add proposal for template renderer #1280

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

alexander-schranz
Copy link

@alexander-schranz alexander-schranz commented May 30, 2022

Old description

It is common that a library, application or CMSs need to have a template renderer / engine for rendering data for their websites or emails. As a library author I want to make it free that my service can be used with any template engine the developer want to use.

Also for projects when somebody wants maybe switch in future from twig to latte templates as it consider better safety for xss.

The interface for the template renderer is simple with a template string and an array context:

/**
 * Render the template with the given context data.
 *
 * @param string $template
 * @param array<string, mixed> $context
 *
 * @return string
 *
 * @throw TemplateNotFoundExceptionInterface
 */
public function render(string $template, array $context = []): string;

The $template does not need to be a real filePath. The used template engine
just need to support it. As an example in twig like template path @namespace/directory/file.twig.html is supported or for blade something like website, or latte something like mail.latte. So $template the is a string. It was considered not to be an object given
to the render method as such complex View class things like example in laminas view
where the View can have subviews via children are not supported in most other template engines.
So it is easier for a template engine like laminas view to have a bridge which
converts $template and $context into their View object as for example Twig, Smarty, Latte to have the need to create a View object.

The context need to be an array which key are a string and can contain any additional objects in it. Where some template engines supporting to give object as context into the renderer it was for maximum compatibility implemented to support only array by this proposal.

The implementation of the TemplateRendererInterface should throw a TemplateNotFoundExceptionInterface when the given template was not found. Any other exception thrown are up to the template engine. There is explicit no method to check if a service exist as it is not supported by all template renderer or are in example case like twig totally different services.

The proposal should describe all requirements for the template renderers.

There are maybe template engines / renderer which will not directly implementing the PSR but it is should hopefully this way not be much work for example creating a laminas-view-psr-bridge package to also support that type of template engine over the PSR TemplateRendererInterface.

I'm new to PHPFig and I'm sure I forget some process, so please help and direct me into the correct process.

PSR Template Renderer Proposal

A proposal for psr for rendering templates.

Goal

It is common that a library, application or CMSs need to have a template renderer / engine for rendering data for their websites or emails.

More and more application are going here to support data provider way. This application are the one which would benifit from the TemplateRendererInterface as they not only can provide them headless over an API but also make it possible that somebody can render the data via a Template Engine.

As a library author I want to make it free that my service can be used with any template engine the developer want to use. Typical usecases are PHP rendered CMSs like Sulu, Typo3, Drupal, Contao which maybe could benifit from this. But also all other data provider based libraries which ship configureable controller or have template to render like email tools / libraries.

Also for projects when somebody wants maybe switch in future from twig to latte templates as it consider better safety for xss a common interface can benifit here and avoid refractorings.

Defining the scope

The scope of the TemplateRenderer is only on rendering a given template with a given context. The template render interface will not take care of registering template paths or how to configure the template engine to find the templates. Similar how PSR-18 HttpClient does not care how the client is created or configured.

Analysis

In this section I did analyse the following existing template engines and added example how the render there templates.

Twig

Repository: https://github.com/twigphp/Twig
Current Version: v3.4.1
Supported PHP Version: >=7.2.5
Template Type Hint: string|TemplateWrapper
Context Type Hint: array
Return Type Hint: string or output to buffer
Supports Stream: true

Render a template:

// render to variable
$content = $twig->render('test.template.twig', ['optional' => 'key-value']);

// render to output buffer
$twig->display('template.html.twig', ['optional' => 'value']);

Smarty

Repository: https://github.com/smarty-php/smarty
Current Version: v4.1.1
Supported PHP Version: ^7.1 || ^8.0
Template Type Hint: string
Context Type Hint: array
Return Type Hint: none
Supports Stream: true (only)

Render a template:

// render to output buffer
$smarty->assign('optional', 'value');
$smart->display('template.tpl');

Latte

Repository: https://github.com/nette/latte
Current Version: v3.0.0
Supported PHP Version: >=8.0 <8.2
Template Type Hint: string
Context Type Hint: object|mixed[]
Return Type Hint: string or output to buffer
Supports Stream: true

Render a template:

// render to variable
$latte->renderToString('template.latte', ['optional' => 'value']);

// render to output buffer
$latte->render('template.latte', ['optional' => 'value']);

Laminas View

Repository: https://github.com/laminas/laminas-view
Current Version: ^2.20.0
Supported PHP Version: ^7.4 || ~8.0.0 || ~8.1.0
Template Type Hint: string
Context Type Hint: ViewModel<null|array|Traversable|ArrayAccess>
Return Type Hint: null|string
Supports Stream: false

// render to variable
$viewModel = new ViewModel(['headline' => 'Example']);
$viewModel->setTemplate('index');
$content = $this->view($viewModel)->render();

Blade

Repository: https://github.com/illuminate/view
Current Version: v9.15.0
Supported PHP Version: ^8.1
Template Type Hint: string
Context Type Hint: array
Return Type Hint: string
Supports Stream: false ?

Render a template:

// render to variable
$content = view('welcome', ['name' => 'Samantha']);
// same as:
$content = $viewFactory->make($view, $data, $mergeData)->render();

Fluid

Repository: https://github.com/TYPO3/Fluid
Current Version: 2.7.1
Supported PHP Version: >=5.5.0
Template Type Hint: string
Context Type Hint: array
Return Type Hint: string
Supports Stream: false ?

// render to variable
$view = new StandaloneView();
$view->setTemplatePathAndFilename('template.html');
$view->assignMultiple(['optional' => 'key-value']);
$content = $view->render();

Contao

Repository: https://github.com/contao/core-bundle
Current Version: 4.13.4
Supported PHP Version: ^7.4 || ^8.0
Template Type Hint: string
Context Type Hint: object<string, mixed> via dynamic properties
Return Type Hint: string
Supports Stream: false ?

// render to variable
$template = new FrontendTemplate('template');
$template->optional = 'value';
$content = $template->parse();

Mezzio

Repository: https://github.com/mezzio/mezzio
Current Version: 3.10.0
Supported PHP Version: ~7.4.0||~8.0.0||~8.1.0
Template Type Hint: string
Context Type Hint: array|object
Return Type Hint: string
Supports Stream: false

// render to variable
$content = $templateRenderer->render('template', ['optional' => 'value']);

Plates

Repository: https://github.com/thephpleague/plates
Current Version: v3.4.0
Supported PHP Version: ^7.0|^8.0
Template Type Hint: string
Context Type Hint: array
Return Type Hint: string
Supports Stream: false

// render to variable
$content = $plates->render('template', ['optional' => 'value']);

Mustache

Repository: https://github.com/bobthecow/mustache.php
Current Version: v2.14.1
Supported PHP Version: >=5.2.4
Template Type Hint: string
Context Type Hint: array
Return Type Hint: string
Supports Stream: false

// render to variable
$content = $mustache->render('template', ['optional' => 'value']);

Yii View

Repository: https://github.com/yiisoft/view
Current Version: 5.0.0
Supported PHP Version: ^7.4|^8.0
Template Type Hint: string
Context Type Hint: array
Return Type Hint: string
Supports Stream: false

// render to variable
$content = $view->render('template', ['optional' => 'value']);

Spiral View

Repository: https://github.com/spiral/views
Current Version: 2.13.1
Supported PHP Version: >=^7.4
Template Type Hint: string
Context Type Hint: array
Return Type Hint: string
Supports Stream: ?

// render to variable
$content = $view->render('template', ['optional' => 'value']);

The proposal

The interface for a TemplateRender I would recommend is the following based on my analysis of exist template engines and what is the easiest way to put them together and have maximum interoperability:

/**
 * Render the template with the given context data.
 *
 * @param string $template
 * @param array<string, mixed> $context
 *
 * @return string
 *
 * @throw TemplateNotFoundExceptionInterface
 */
public function render(string $template, array $context = []): string;

For maximum compatibility we even could consider to publish 2 version of the template renderer v1 without typehints so exist template engine still supporting old php version can already implement it and v2 with typehints.

Exist TemplateRenderer Discussion

There was already an exist disussion about implementing a TemplateRendererInterface here: https://groups.google.com/g/php-fig/c/w1cugJ9DaFg/m/TPTnYY5LBgAJ.

The discussion goes over several topics just to mention the main parts:

  • Template should be objects
  • Context should be objects
  • TemplateRender should stream to output even asynchronity

To target this specific points. I would focus in this PSR on exist solution as we see most work for template with logical string based names and do not require an object.

I want mention here also developer experience as example in the past why there was created PSR 16 (Simple Cache) where we did still have PSR 6.

So the name of the proposal should maybe be "Simple TemplateRenderer" and not try to reinventing the wheel.

By analysing exist template engine, not everybody support to have an object as a context so I would keep the interface to array for context only. This way it is easy to make exist template engine compatible with PSR interface.

For streaming the output I must say I did not see one project since 7 years which did use for example the streaming functionality of twig and for maximum compatibility I would also remove that requirement from the PSR as the analysis give there are template engines which do not support that functionality.

The proposal I did write can be found here: https://github.com/php-fig/fig-standards/pull/1280/files. I know I did skip here some process by already writing something, still I hope we can put most of the template engine creators / maintainers on one template to dicuss if they are willing to add such an interface or for which we maybe can provide a bridge, so system like CMSs, Librariy, Newsletter Tools, can make use of it. I will also bring this topic on the table for our next CMS Garden call which we have every month where several members of different CMS have a call together discussion common topics.

TODO List

  • Create list which libraries would benifite using a TemplateRendererInterface and which would not
  • Analyse exist Template engine of their ability to check if a template exist or not

@alexander-schranz alexander-schranz requested a review from a team as a code owner May 30, 2022 21:24
@alexander-schranz alexander-schranz marked this pull request as draft May 30, 2022 22:08
@alexander-schranz
Copy link
Author

alexander-schranz commented May 30, 2022

This proposal could be interesting for following template engines and CMSs:

And would make it so easier to implement libraries which has to render something into this systems and frameworks. But the rendered thing is project specific, specially CMSs could support so multiple template engines.

I hope its fine that I tagged current maintainers of the libraries, Please feel free to unsubscribe this PR to avoid spaming your inbox if you are not interested into this topic.

proposed/template-renderer.md Outdated Show resolved Hide resolved
proposed/template-renderer.md Outdated Show resolved Hide resolved

Having common interfaces for rendering templates allows developers to create libraries that can interact with many frameworks and other libraries in a common fashion.

Some examples may use the Interface:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think details should be moved to a meta-document.

- Newsletter/Mail Libraries (Symfony Mailer, Abstractions over Mailchimp and other Newsletter tools)
- Anything following ["Separating content from presentation"](https://en.wikipedia.org/wiki/Separation_of_content_and_presentation) where the presentation is project specific.

Some examples may implement the Interface or providing a bridge:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yii has alike interface https://github.com/yiisoft/view/blob/master/src/TemplateRendererInterface.php with the difference that we're passing a rendering context as $this for nested template rendering.

Copy link
Author

@alexander-schranz alexander-schranz Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for mentioning yii views. Looking at the example it works the same way ->render($template, $params): https://github.com/yiisoft/view/blob/master/docs/basic-functionality.md#rendering

That yii uses then in the template $this is totally fine the TemplateRendererInterface doesn't force how the the variables need to access in the template. If it is {variable}, {{variable}} or <?php echo $this->variable ?> is up to the template renderer.

I will add it also the the list. Please check my details in the PR description about it if I did write nothing wrong about it.

proposed/template-renderer.md Outdated Show resolved Hide resolved
proposed/template-renderer.md Outdated Show resolved Hide resolved
proposed/template-renderer.md Outdated Show resolved Hide resolved
proposed/template-renderer.md Outdated Show resolved Hide resolved
proposed/template-renderer.md Outdated Show resolved Hide resolved
A implementor of a template renderer is allowed to support as context also an object, but a user is REQUIRED whennusing
The `TemplateRendererInterface` to give the context as array to support also simple template renderers.

## Usage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section definitely belongs to metadata document.

@samdark samdark added the WIP label May 31, 2022
@fabpot
Copy link
Contributor

fabpot commented May 31, 2022

I would love to see the proposed interface in action for some template engines mentioned in the description. Having some examples of non-trivial use cases might help understanding if the abstraction is useful in real-life scenarios.

@dg
Copy link

dg commented May 31, 2022

Hi, I never understood the point of these interfaces and I certainly won't implement them, so I'm unsubscribing. Bye.

@gsteel
Copy link

gsteel commented May 31, 2022

I'm not actually a maintainer for laminas/view, just a contributor so paging @weierophinney and @froschdesign for their input.

My personal initial reaction is generally 👍, with the exception that a well-typed view model makes for a pleasant template editing experience so rejecting an object for context is a shame… that said, it would be easy enough to write a bridge component as already mentioned.

@boesing
Copy link

boesing commented May 31, 2022

I'd suggest having a look at the mezzio template renderer which already supports multiple engines such as laminas-view, twig, etc.

Not expecting to use that, just to get an idea of how it could work.

@froschdesign
Copy link

I'd suggest having a look at the mezzio template renderer which already supports multiple engines such as laminas-view, twig, etc.

Reference:

@bmack
Copy link
Contributor

bmack commented May 31, 2022

Hey @alexander-schranz

I've been trying to make TYPO3 (as it currently heavily depends on its own rendering engine called "Fluid") more interoperable to allow e.g. twig or blade as engine under the hood.

Our current implementation looks like this, where we would start creating adapters from it:
https://github.com/TYPO3/typo3/blob/main/typo3/sysext/core/Classes/View/ViewInterface.php#L23-L41

Next to the render() method (with $context which is fine), I think an assign() method would be much appreciated (but can be optional of course). I'd be really happy if we could find the least common denominator for this and make templating engine adapters interoperable.

@weierophinney
Copy link
Contributor

@bmack I've found assign() and/or addDefaultParam() and the like to be problematic when you start using stateful systems such as OpenSwoole, ReactPHP, etc.. Since those methods can be called anywhere in the request timeline, you get in the habit of doing them in listeners or middleware so that the values are available when you later call render()... but then the values never go away.

What's the problem? Consider things like authenticated identities, which might persist between different requests made by different users.

We'd already had addDefaultParam() in the Mezzio template renderers when we added Open/Swoole support, unfortunately, otherwise I'd have kept that out. What I recommend in those situations is a request-specific container, which we added to Mezzio some time back: https://docs.mezzio.dev/mezzio/v3/features/helpers/template-variable-container/

I'd prefer we omit any mechanism that would provide default template variables from the interface.

@alexander-schranz
Copy link
Author

alexander-schranz commented May 31, 2022

Thx you already all for your feedback. To not skip the official way I also did inform the mailing list via the google groups about this PR: https://groups.google.com/g/php-fig/c/X4e1z5IaG9E

Above I did update the whole pull request description with my analysis of different template engines. Please reread it so I did not have an error in my analysis about your template renderers.


@dg Sad to hear, was really looking forward add something like that to latte as it is a promosing template engine which a lot of systems can benifit from. Seems like we need to go the bridge way if accepted to add support for Latte.

@fabpot Thx for attending here. I think CMS are a nice usecase for it. In example Sulu provide to define the form (structure) for content management in a xml file which creates the form for the content manager. Sulu itself already provides a Controller which will load all data for the the site. This controller is called when the url is triggered via a dynamic router, but the template which is render is not provided by Sulu, it is implemented in the project and configured there in the project xml file. So Sulu would not care what template engine is used to render its data it does just provide it over the configured controller and could in this case render the configured template over a configured template engine.

@gsteel Thx for mentioning the correct people.

@boesing @froschdesign Nice to have you onboard, didn't know mezzio has something like that looks great that you did for rendering end with similar method. I would keep the other method out of scope for a renderer.

@bmack Thx for your feedback, great to see that you are also interested into this topic. I would avoid a ViewInterface as we would end then into the problems with the factory which was in psr-7 why then psr-17 did came up with. Still a library can internal use a View object very easily and work with it and call then the renderer via $this->templateRenderer($view->getTemplate(), $view->getContext()); if they really need it.

@weierophinney I agree the TemplateRender should by stateless service, sure we can not avoid that every template engine is stateless as its there code/class but there should not be something like setDefaultParams and assign on the TemplateRendererInterface itself and think we should go with a single method for rendering here and concentrate on that usecase. Thx for your feedback.

Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
@harikt
Copy link
Contributor

harikt commented Jun 1, 2022

I would add one more templating engine into the list, as you are comparing multiple ones.

https://github.com/qiqphp/qiq by @pmjones .

Mezzio was trying to solve the same problem some years back. So their inputs will be valuable here.

proposed/template-renderer.md Outdated Show resolved Hide resolved
@fabpot
Copy link
Contributor

fabpot commented Jun 1, 2022

Some quick thoughts:

  • For Twig, I thought more than once about switching to an object for parameters instead of an array.
  • Having to catch an exception for non-existing templates was a performance issue in Twig as we have a feature that let you fall back to other templates. So we added a way to check if a template exists that returns a Boolean.

I'm struggling with the use cases though. It's not similar to PSR-18 as HTTP clients has a well-defined scope, based on a standard. I can see how I can switch from Guzzle to Symfony HTTP client in a project and avoid having two HTTP client implementations. But for templating engines, I think it's very different. If you decide to switch from Twig to Blade, you would need to start from scratch. And supporting Blade and Twig for a CMS also probably mean writing helpers, filters, functions, or whatever extension points the engine has for all supported systems. I'm probably missing something here about the use cases though.

Co-authored-by: Sergei Predvoditelev <sergey.predvoditelev@gmail.com>
@weierophinney
Copy link
Contributor

@fabpot

For Twig, I thought more than once about switching to an object for parameters instead of an array.

What would that look like? Would you typehint it as object, or would you expect an object similar to the PSR-11 container (i.e., get() and has() methods)?

@Crell
Copy link
Contributor

Crell commented Jun 1, 2022

I'll note that, if a template engine wants to have extra/standard/default stuff, it can always implement PSR-14 internally to pass the context set to listeners that can set default values for things. That would eliminate the need for such support in a common template interface.

I mostly share @fabpot's skepticism, though. As I said on the mailing list, while a type-compatible common interface for different template engines seems simple enough, a semantically-compatible interface is a much bigger deal. Just how to identify a given template by name is right now entirely unstandardized, but would need to be standardized for the goal of a library being able to work with "whatever template engine a framework/CMS is using." If the relevant implementers are able to agree on a format for that, cool, I'm all for it, but let's not under-sell the challenge here.

I'd expect an object-based context set to be more like how Go's template engine works. The foo template expects a Foo object, which has known properties (or getter methods if we must) bar and baz, so the template author knows exactly what to expect, and debugging tools can build off of that. Of course, that potentially creates issues for the side-band addition of properties via events I mentioned above. So yeah, not an easy problem space. 😄

@alexander-schranz
Copy link
Author

alexander-schranz commented Jun 1, 2022

Thank you both for your responses and attending this discussion. I think we maybe have different usecases for the TemplateRenderInterface in mind. So I will try to answer your comments in best way and my point of view on it.

For Twig, I thought more than once about switching to an object for parameters instead of an array. @fabpot

I'm also curious here what did you think about and what problems you are trying to solve with it? Can you show an example you had in mind?

If you decide to switch from Twig to Blade, you would need to start from scratch. @fabpot

That is a great hint and totally false usecase or bad example "TemplateRendererInterface" from my side. As the one calling "TemplateRendererInterface" are the one which do not care how the Template is rendered. The ones implemented the template do care and so are the one which decide what is used as the engine, so they benifit as they can decide which template engine they want to use without there used library will force them. Still yeah switching is a bad example, but would be allowed or even you could maybe migrate better via the TemplateRendererInterface from old template engine by using some kind of ChainTemplateRenderer.

Having to catch an exception for non-existing templates was a performance issue in Twig as we have a feature that let you fall back to other templates. So we added a way to check if a template exists that returns a Boolean. @fabpot

That is a great hint, I did go with an exception currently because I thought it is easier to implement. I did I think most TemplateEngine splitted TemplateLoading and TemplateRenderer, where I really would like to have something like TemplateRendererInterface->exists I think even Twig there you need to go over $twig->getLoader()->exists. Adding a TemplateLoaderInterface to the PSR I think would force something strange. What is your opinion here about having 2 methods on the TemplateRender render and exists. So the Interfaces do not care if there is a TemplateLoader or something like that?

And supporting Blade and Twig for a CMS also probably mean writing helpers, filters, functions, or whatever extension points the engine has for all supported systems. @fabpot

I agree with you they can not use helpers, filters, function provided by the library. But as example this is already the case in Sulu CMS, we have a punch of website which are using Sulu in Headless way. So the controller provides the data which is in headless way the data which normally the template ngine gets, serialized in JSON and requested from a Node JS (Next, Nuxt, Angular, ..) application. They also have only the data available by the controller and so not any helper. More and more are going this kind of hybrid way so the frontend technolgy can be used what they want, and the TemplateRendererInterface would fit here also well.

I'm struggling with the use cases though. It's not similar to PSR-18 as HTTP clients has a well-defined scope, based on a standard. @fabpot

I think the whole PSR-18 was misleading here in the description what is the Goal and how a TemplateRendererInterface is recommended to be used. So ignore the PSR-18 part. So the scope of this proposal is really, how a library can provide data which which is rendered but don't want to take care how it is rendered.

while a type-compatible common interface for different template engines seems simple enough, a semantically-compatible interface is a much bigger deal Just how to identify a given template by name is right now entirely unstandardized, but would need to be standardized for the goal of a library being able to work with "whatever template engine a framework/CMS is using." @Crell

Does really the template name, path, ... needed to be standardized. I don't think so. Because I think you are thinking from a false point of view where the template name which is rendered in a such library is coming from. As it should not be defined by the library it is coming from it should be come from configuration the user using the library, will go in the next question into it why I have this point of view on that topic.

I'd expect an object-based context set to be more like how Go's template engine works. The foo template expects a Foo object, which has known properties (or getter methods if we must) bar and baz, so the template author knows exactly what to expect. @Crell

That is nice for simple application and totally aggree that they work well in there case but this kind of one to one mapping between object and template is very limited, and really do not work well for for dynamic systems like content management systems where I get a dynamic data and the template from Page A and Page B is different but the Object is the same. Forcing that every template need then to have representing PHP class is from my point of view a bad developer expierence for such case and would force such system to generate dynamic classes.

So we should not forget which libraries are the one which use a Template Engine. On the first hand there are libraries which just render HTML to represent them or send it via email. This are not the libraries which benifit a lot from a TemplateRendererInterface, because they don't care, they even need to require a specific template engine for rendering there things. Mostly its just for them something internal the template lays in there package and is mostly even not changeable over a config and looks like it looks.

So what are the libraries benifit from the a TemplateRendererInterface. It are libraries where the project need to care how the Data is render, yeah I'm bringing here again the CMS example as it is the nearest for me. But maybe it is better if I use something more common here like the TemplateController of Symfony.

acme_privacy:
    path:          /privacy
    controller:    Symfony\Bundle\FrameworkBundle\Controller\TemplateController
    defaults:
        template:  'static/privacy.html.twig'

The controller is a really simple example how a CMS like Sulu works, with small differences that you do not configure a route path, but Sulu provide like here Symfony a Controller just with data in our case and like here the user of the library/framework configure template and control how it is rendered.

This controller is also a great example why a one to one maching from object class to template is not great as there doesn't exist an object in this case and I have ther can be different static pages which look differently.

I really don't want to say that the golang type of templating engine is bad or is even bad practice. It really is great when you create such application. I just think that the application which would benifit from the TemplateRendererInterface are others then the one using that type of template engine.
As you @Crell are working for Typo3 I think you already stumble over the usecase where Typo3 did render a Page and that page had 2 different template one for HTML version of the page and one for the AMP version of the page and the both work with the same data. Theming is also a great case where one to one mapping to object to template is bad as I don't want to create new object class just because a language A looks different to language B.

So should we standardized the template name path or even have a mapping from context object to a template. From my point of view there are to this cases which speaks against it, to restrict the renderer this way and to less what speaks for it. Beside I would also link and mention again why I think that string, array is a better developer experience than having a project: https://groups.google.com/g/php-fig/c/X4e1z5IaG9E/m/Ubz4Id2GAQAJ

I hope I did target all comments. For targeting the concern about the scope of the PSR I want to provide better example this day via some code of real world usecases to make it better understandable.

Thank you all for attending here and for all the interests in this topic really great that this topic does have this kind of interest and is supported being discussed.

@alexander-schranz
Copy link
Author

I added 2 points to the TODO list which I will target next and will update the description of the Pull Request with it when done:

  • Create list which libraries/application would benifit from a TemplateRendererInterface and why. And which one would not.
  • Analyse exist Template engine of their ability to check if a template exist or not and how they do it as an alternative to the NotFoundException.

If somebody already know something let me know.

@mbabker
Copy link

mbabker commented Jun 1, 2022

Does really the template name, path, ... needed to be standardized. I don't think so. Because I think you are thinking from a false point of view where the template name which is rendered in a such library is coming from.

I've been through this exercise before for my own amusement trying to find a way to migrate a CMS away from its own inconsistent homegrown rendering patterns and I do think that for the best interoperability, having a standardized naming convention is essential here (otherwise the implementation details about what's underneath the interface are just going to leak back out somewhere when trying to do a $renderer->render($template) call and at the end of the day it's really not much better than just injecting your own abstraction or the renderer directly). As proposed, TemplateRendererInterface::render() would work just fine as far as giving it some parameters and getting back a rendered thing. But, not having the $template parameter standardized kind of defeats the whole point of the abstraction layer.

I work a lot in Laravel and Symfony; neither Blade nor Twig have a consistent template naming structure.

Blade (no namespaces): static.privacy (loads views/static/privacy.blade.php)
Twig (no namespaces): static/privacy.html.twig (loads views/static/privacy.html.twig)

Blade (with namespaces): pagination::tailwind (loads /path/to/namespace/tailwind.blade.php)
Twig (with namespaces): @pagination/tailwind.html.twig (loads /path/to/namespace/tailwind.html.twig)

If the goal is just to add an interface that Twig\Environment or Illuminate\Contracts\View\Factory or Mustache_Engine could implement so libraries don't need their own abstraction layer or to be hard-coupled to a single-engine, then I guess things are fine as is. If the goal is to have interoperable templating engines, then template path standardization goes a long way to make that viable.

@wisskid
Copy link

wisskid commented Jun 2, 2022

I agree with @fabpot here. I don't quite see the use case yet. This doesn't mean there isn't one, of course. Maybe a list which libraries/application that would benifit from a TemplateRendererInterface as @alexander-schranz proposed will help in this regard, so standing by for that.

@m-vo
Copy link

m-vo commented Jun 2, 2022

Typical usecases are PHP rendered CMSs

I think this proposal would rather be helpful for libraries/packages/components that need to render something very simple and don't want to tie themselves to an engine. For example, if there is the need to output some formatted text in a single template file. As soon as the template itself needs some logic, you would end up providing an example in template language X. Or X and Y? Or all of them?

In a CMS, on top, you have base templates, partials, custom functions, … that make things work in a useful way. Without a standard covering all of this as well, it's IMHO not really feasible to provide these in a template engine agnostic way. The Twig implementation in Contao CMS for instance allows every extension to extend from the same template without the need to know each other. This ecosystem works well - until you would allow arbitrary template engines. Then, interoperability between extensions would suffer.

Besides this, here are some concerns regarding the $template parameter:

  • The template name/fully qualified name/path heavily depends on the engine and framework it is used in. So what should a library render then? render('foo') or render('foo.html') or render('vendor/package/foo')?

  • Where is the rendered template embedded in (context) and/or what should it output? Some engines might not support arbitrary formats (is it used in HTML or JSON or JS or …?) and some - like Twig - support escaping based on the file extension. So foo.css.twig will be treated differently than foo.html.twig.

Also, looking at the last example above: The .twig extension should probably not be in the name to be agnostic. So, the need for a standard naming scheme is IMHO equally important for this proposal to work. I fear that's the harder problem 🙈.

@weierophinney
Copy link
Contributor

I'll chime in here on the template naming as well. As @mbabker notes, each template engine loads them differently. Some use full or relative file paths, including the extension. Others use a template "name", which a resolver than resolves to a template (whether that's in a file or in-memory or a string or something else). Some provide namespacing features (Blade, PlatesPHP, laminas-view to an extent).

When we created mezzio-template, we opted to use a naming convention that all renderers then needed to support, and it looks basically like Blade's ({namespace}::{template}). Namespacing allows segregating templates by purpose without worrying about where on the filesystem they reside; the resolver then locates them based on the namespace and name.

That said... conventions are brittle.

If I were to do the exercise again, I'd use a value object for the template name, something like:

interface TemplateName
{
    public function getNamespace(): ?string;
    public function getTemplate(): string;
}

One possible addition to this interface would also be something along the lines of a "type", so that you could, for instance, vary the template resolved based on the type of content to return (e.g., "html", "text", "xml", etc."). An example of that in the wild is Twig, where you will often have the type of content being returned as an additional suffix to the template name (e.g. privacy.html.twig vs privacy.xml.twig).

public function getType(): ?string;

The renderer interface then consumes the TemplateName:

interface TemplateRenderer
{
    public function render(TemplateName $template, array $context): string;
}

This way, an implementation can pull information from the TemplateName and use that to resolve to a template. Ideally, template names would omit any file extensions, leaving that to the renderer to determine.

Regarding the question posed by @Crell and @fabpot: the use case is in 3rd party libraries. Bundles, modules, etc. that provide controllers or handlers and additionally want to provide templated results, but may not know exactly which template engine is in use. (Yes, you generally know it for a framework, but it's not a guarantee.) These can then typehint on the TemplateRenderer, and provide guidelines about what they send in the context; users can then write their own templates that target the template library they actually use in their project.

@slavcodev
Copy link

slavcodev commented Jun 2, 2022

...
The renderer interface then consumes the TemplateName
...

The alternative could be making first argument of the renderer a URI reference and let implementation choose how to resolve it.

@weierophinney
Copy link
Contributor

The alternative could be making first argument of the renderer a URI reference and let implementation choose how to resolve it.

Yes, similar idea. The main thrust is to have something that (a) is not a path, (b) allows for differentiating templates of the same name but different contexts, and optionally (c) allows for differentiating based on requested format for the final templated content. We would need to agree on what that reference looks like and what parts it would expose to the consumer (a rendering library).

@alexander-schranz
Copy link
Author

Wanted to give an update here as the last post is already 22 days ago. I still analysing different template engines, also about other features and things they support, to better understand them and their need for different things. Also understanding how different template engines are currently integrated into there core framework and also looking which engines are also integrated currently into other frameworks, as example twig into spiral or mezzio and so on.

For experementig with a common interface and different template engines I created the following repository:

https://github.com/schranz-templating/templating

In that repository I will currently try to collect my analysis and I will try to keep you here up2date with them. So we have a good base about how a common interface could look like.

@taylorotwell
Copy link

Hello! Speaking on behalf of Laravel here. I do agree with @fabpot that it's unclear to me how this would pan out in non-trivial real world scenarios. If you are switching to a different templating engine it seems like you would want to use the syntax and features and DX affordances of that particular engine.

@alexander-schranz
Copy link
Author

@taylorotwell Thank you for attending here and representing Laravel :).

For framework it would not change a lot, they would just provide that example by default "blade" is the TemplateRenderer service and in symfony "twig" the TemplateRenderer service.

But example for libraries which allow to render "project" content via their code it is easier to implement this way into different framework.

Usecase A

Lets have a look as example at the Symfony/Mailer component. It allows to render emails over twig via:

$email = (new TemplatedEmail())
    ->to(new Address($meail))

    ->htmlTemplate($this->templateName)

    ->context([
        'url' => $url,
    ])
;

But the library would not care which "template engine" would be behind and it could be used with "Twig" or "Blade".

But a better example, I'm greating library on top of the symfony/mailer which example sends in the process a double-opt in email. By default that email ist just a link which I send via:

$email = (new Email())
    ->to('you@example.com')
    ->html($linkHtml);

Now as a library author I want to make it configurable how the "email" is looking for that project. But as a library author I want not force which "template" engine they use to render that email. So I'm just requiring that a TemplateRendererInterface Service implementation need to be provided and so I can go with something like this in my library:

$html = '<a href="' . $url . '">' . $url . '</a>';

if ($this->templateName) {
    $html = $this->templateRenderer($this->templateName, ['url' => $url]));
} 

$email = (new Email())
    ->to('you@example.com')
    ->html($html);

The $this->templateName is coming from a configuration from the project using the library. The TemplateRendererInterface can be used for libraries which provide data, but do not care how that data is rendered.

So in this usecase it is not about using twig in laravel or using blade in symfony. But creating library which work in symfony with twig and in laravel with blade.

Usecase B

Another usecase where I'm coming from is Content Management, which are typical "Data Providers". @sulu Which I'm part of it as example works for both cases as headless cms providing all data as json for a JS based rendering or as a classic cms rendering the pages via twig currently. A page template is sulu is configured in a project like this currently. The only important parts for this are the following:

<view>pages/homepage</view>
<controller>Sulu\Bundle\WebsiteBundle\Controller\DefaultController::indexAction</controller>

The controller which is provided by sulu and the view which need to be implemented in the project. Sulu would not care if the page itself is rendered via blade or twig as sulu itself does not provide any default templates, all need to be implemented in the project. And so a project just install the template engine they want to use.

Other content management system like example Typo3 are currently working on bringing support to different "Template Engine" and a common TemplateRendererInterface make it a lot easier.

Usecase C

Another usecase for a TemplateRendererInterface could be projects which are migrate currently from example twig to latte oder twig to blade or whatever around. A common TemplateRendererInterface allows also to create a AdapterTemplateRenderer which could look like the following:

class ChainTemplateRenderer implements TemplateRendererInterface {

    public function __construct(
       TemplateRendererInterface $twig,
       TemplateRendererInterface $blade,
    ) {
    }
    
    public function render(string $templateName, array $context = []): string
    {
        if (str_ends_with($templateName, '.twig')) { // or using $this->twig->exists
            return $this-twig->render($template, $context);
        }
        
        return $this-blade->render($template, $context);
    }
}

This way without changing the whole dependency injection a project can easier step by step migrated to a new template engine.


@taylorotwell I hope I could give you here better understanding about advantages of a common TemplateRendererInterface. Let me know about your thoughts about it. Also what you are thinking a RendererInterface should look like. As their are already some discussion about using objects instead of string $templateName and using object instead of array $contexts. Let me know your opinion about it. Also I'm kind of interested if their are any changes planned on blade itself.

I want to thank all again about attending here and give their opinion on this topic 🙏. I also learned a lot, that there are more template engines as I thought and they focus on a lot of different things, Which is really interesting and just let me want more to get them under a common interface.

@Jean85 Jean85 added the New PSR label Jul 18, 2022
@weierophinney
Copy link
Contributor

I personally see value in this from a couple of perspectives:

  • For third-party libraries that may want to use a template for non-HTTP response-related things, such as emails, creating file assets, etc. Having interfaces like this means that those libraries can be framework agnostic: they can be dropped into a Laravel, Symfony, Laminas, CakePHP, or other application, and as long as the templating library adheres to the interface, and is wired, that library can be used seamlessly.

  • For libraries that target other FIG standards, particularly PSR-15. This allows them to declare and use templates, but not need to lock the app consuming them into a specific template solution. They may even provide templates for one or more specific implementations, but ultimately, the consumer has the freedom to choose whichever implementation they want.

The point is allowing the ecosystem to be agnostic about the implementations, so that they can be used in more than one context. I see a ton of libraries out there that simply adapt existing libraries to work under a specific framework. Interfaces such as this one would make it easier to just consume the original library without a wrapper.

@Voltra
Copy link

Voltra commented Sep 7, 2022

Something I don't think I've seen in the discussion yet: we don't have to change all the libraries' and frameworks' rendering engines, they can just provide (façade?) adapters that satisfy that interface.

@WyriHaximus
Copy link
Member

WyriHaximus commented Sep 11, 2022 via email

@alexander-schranz
Copy link
Author

@Voltra sure that is always possible in most cases and currently also the way I'm experimenting with the exist template engines by going over adapter: https://github.com/schranz-templating/templating/tree/0.1/src/Adapter. And test and try out how the different template engines work in other frameworks and how they can be implemented.
Still the best way would be that the engine implements the interface and not have to go over an adapter. The baddest way would be that the adapter living in another package like I'm currently doing but still is possible for engine which don't want implement such interface or adapter in there library.


In the last days I also did found some other template engines which I was not yet aware of. PHPTAL and older engine based on xml based syntax (reminds me a little bit on how vue and angular works today via xml attributes) and Brainy a fork of smarty template engine. Both seems be maintained and got releases lately. Latest release from PHPTAL by @Ocramius and Brainy by @DavidSchnepper and (maybe you want also join this discussion). I already experimented with the engines by implementing an adapter:

I'm also trying how different template engines can be used in different frameworks. Currently already got the following framework the following template engines included and working over the TemplateRendererInterface:

Symfony
  • Blade
  • Handlebars
  • Latte
  • Mustache
  • Plates
  • Smarty
  • Twig
Laminas
  • LaminasView
  • Blade
  • Handlebars
  • Latte
  • Mustache
  • Plates
  • Smarty
  • Twig
Laravel
  • Blade
  • Handlebars
  • Latte
  • Mustache
  • Plates
  • Smarty
  • Twig

Next one I want to have a look is Mezzio and how there the different engines could be integrated or used, I mostly want better understand the frameworks and in which direction they go with template engines and see if such an Interface is even possible. I still think it would be a great addition to the PHP eco system to have general interface for template engines to make integration of different libraries into different frameworks easier. And agree with the points mention by @weierophinney.

@DavidSchnepper
Copy link

Brainy isn't in active development. It was a fork of Smarty made several years ago by Box employee @mattbasta, who still does some maintenance on it (and it's still in use at Box.com).

@slepic
Copy link

slepic commented Aug 20, 2024

Hi @alexander-schranz

I just encoutered this proposal, and first of all I like it :)

I see this is a bit old but hopefully not completely abandoned yet, and so I wanted to add my 50 cents.

I agree there is no need to standardize the template names and I have real package that supports your cause.

Namely the usecase A that you mentioned earlier - a library just wants to render some data and it doesnt care how or what rendering engine is used.

This is the example you provided:

$html = '<a href="' . $url . '">' . $url . '</a>';

if ($this->templateName) {
    $html = $this->templateRenderer->render($this->templateName, ['url' => $url]));
} 

$email = (new Email())
    ->to('you@example.com')
    ->html($html);

See, in the code above $this->templateName exists as a property alongside $this->templateRenderer only to be passed to the renderer at some point. But for all this code snippet cares, the template doesn't even have to have name, in fact it doesnt even have to have a file associated with it.

See this modified version:

$html = $this->template->render(['url' => $url]));

$email = (new Email())
    ->to('you@example.com')
    ->html($html);

and whether a named tempate that uses an engine, whether a file or template name exists at all, where there is a default implementation that looks as below is no concern of the example library code.

class MyEmailDefaultTemplate implements TemplateInterface {
   public function render(array $data): string {
       $url = $data['url'] ?? null;
       return  $url ? '<a href="' . $url . '">' . $url . '</a>' : '';
   }
}

Of course easy implemetation using a template engine would look like this:

class EngineTemplate implements TemplateInterface {
    public function __construct(private TemplateRendererInterface $renderer, private string $templateName) {}
    public function render(array $data): string {
        return $this->renderer->render($this->templateName, $data);
    }
}

Here is the TemplateInterface that I refer to https://github.com/slepic/php-template/blob/master/src/TemplateInterface.php
And here is how the interface is used in a library https://github.com/slepic/templated-tracy-bar-panel/blob/master/src/TemplatedBarPanel.php
which is further instantiated here https://github.com/slepic/psr-http-message-tracy-panel/blob/master/src/Factory.php

It is just a very simple tool but it shows a practical example of the hypothetical usecase that you provided.
Many times I was thinking about introducing a template renderer interface.
But I really didn't want to care about using a rendering engine at all. I never really needed the part when someone would actually want to change the template used by the library. And i didnt care when a template name must be translated to template file and how this would be done. And so I ended up doing just this - decoupling the template identification from actually rendering some data.

Hopefully this is an interesting viewpoint on the problem at hand.

Cheers, and once again I hope this initiative is not dead, because i really like the proposed interface.

@alexander-schranz
Copy link
Author

alexander-schranz commented Aug 21, 2024

Hi @slepic,

yeah that is sure one usecase I had in mind for a TemplateRendererInterface. I created a little prototype around it here: https://github.com/schranz-templating/templating

After following the next steps of Twig Template Engine. I'm rethinking about the return type. While string is a nice simple way. It has the disadvantage that it all need to be inside the memory when you render big templates. Twig did later refactor its internals to use PHP Generators more. This allows that they without a lot of effort can render to a string but also stream a template output step by step via.

So maybe the better return type would be a iterable<string>. Example:

TemplateRendererInterface::render(string $template, array $parameters): iterable<string>
$templateParts = $this->templateRenderer->render('template', ['params' => 'value']);
foreach ($templateParts as $templatePart) {
    echo $templatePart;
}

But I don't like that for the end user is ideal case. Another possibility would be have 2 methods render and stream on the Interface. But not all template engine supports streaming or supports stream only part by part. Sure we can then only return one big string in that case. But yeah not sure what the ideal interface here is. Creating 2 Interfaces I would avoid as that would make creating a abstraction around the template engines hard, better one interface and fallback mechanism to return 1 string in streamable. But as I think ~95% (no research done for this) of the current used templating calls are none streamables maybe that streamable part should not be part of the such interface.

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

Successfully merging this pull request may close these issues.