New TemplateRendererInterface Proposal

346 views
Skip to first unread message

Alexander Schranz

unread,
May 31, 2022, 7:15:24 PM5/31/22
to PHP Framework Interoperability Group
Hi my name is Alex,

I want to bring a new proposal to PHP-Fig, which could be interesting specially for Frameworks and CMSs, as I'm one of the core developers of Sulu CMS we have todo much with template renderers.

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 the 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: v3.4.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/illuminate/view
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 a template:

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


Contao

Repository: https://github.com/TYPO3/Fluid
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 a template:

// 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 a template:

// 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 a template:

// 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 a template:

// render to variable
$content = $mustache->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 table to dicuss if they are willing to add such an interface or for which we maybe can provide a bridge, so system like CMSs, Library, 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. So maybe I can reach there a CMS which maybe did not yet part of this mailing list / github discussion.

Best Regards,
Alex

Alexander Schranz

unread,
May 31, 2022, 8:11:38 PM5/31/22
to PHP Framework Interoperability Group
Quick Update,

was missing  another important library the yii view in my analysis:

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']);

So not match change. If somebody see any errors in my analysis please let me know.

Best Regards,
Alex

Woody Gilk

unread,
Jun 1, 2022, 9:28:21 AM6/1/22
to PHP Framework Interoperability Group
Alexander,

This is really great work and I appreciate the effort you put into it.

I definitely support this continuation of this proposal.

Regards,


--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/3d47a925-9f63-4477-8ca4-654d4cfec180n%40googlegroups.com.

Larry Garfield

unread,
Jun 1, 2022, 12:00:22 PM6/1/22
to PHP-FIG
On Tue, May 31, 2022, at 6:12 PM, Alexander Schranz wrote:


> *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 was one of the people arguing in the past that a templating interface needs to be more robust than a dict->string mapper. It does seem that there is a de facto consensus on that model anyway, so even if it's a poor model there is potential value in standardizing that much.

My concern is that while there's obviously a straightforward way to make common render() methods *type* compatible, that doesn't mean they're *semantically* compatible. The template name string is, AFAIK, wildly variable between different systems right now, and the value part of the replacement array is also a huge ball of undefined. For a standard to actually be viable, those would need to be standardized at least enough that a library could use it and expect it to then "just work" whether it's plugged into Twig, Plates, Sulu, TYPO3 Fluid, or whatever. That also gets into questions of escaping, and what is escaped where.

Those are extremely non-trivial questions. I don't know how resolvable those are in practice, but they would all happen outside the type system, or else involve not passing primitives (eg, a template object instead of just a string name, and then we're right back where we started). If the major players in the template world are on board for a working group to sort out *that* part of the problem space, I'm happy to support them doing so. But it would need buy-in from the major players there, because otherwise it wouldn't be useful.

--Larry Garfield

Alexander Schranz

unread,
Jun 1, 2022, 3:12:56 PM6/1/22
to PHP Framework Interoperability Group
Thank you Larry for your valuable Feedback and concerns about my proposal.

I understand your concern about primitive Types, but I think they are a little bit too much hated today.
Also I think we are creating bigger problems with less intercompatibility if we go with object for both the template or the context.
I would focus what the best and most developer friendly way is to get all under the hood.

In mostly all Templates engines I analysed the template itself is represented as a string.
Sure some template engines under the hood work with Template objects / Template Wrapper objects. But we are not on that Level we are on the Level above the caller.
So why is it that way that mostly all did end in that scenario of using a string.
I personally think it is because it is the most developer friendliest way.
Because of going with a object I would need to import a class, inject a factory or something like that unnecessary complex for that usecase.

A string is simple and very developer friendly in this way. The developer know the template they created and put the name into the method call and its done.
I also think that is also the case why Laravel got a lot of traction, why other including me trying to overcomplicate things with Object and Types.
You will find array and strings a lot in Laravel interfaces and methods, when you scroll through there Documentation.

Also asking where do templates coming from. They are mostly coming from 3 places.
Place 1: Configuration, Place 2: Database, Place 3 Hardcoded in all cases they are already a string.
There are no usecases to convert or manipulate them, I currently think of which would make them
usable as convert the string to object.

So now from the template name to the context. I mostly see the same problems for the context.
Why I see there more the usecase as mezzio mention there TemplateContainer.
I think it does overcomplicate the things and the array is still the better Developer Experience here.
The object will end us just in another rabbit hole.
And in the "big factory question", who is creating the object and providing it.
If we have directly a factory method on the TemplateRenderInterface::createView as example
which will mostly get string, array it is the question what value it does it really has then.
If we go the way of saying we do not care about who is creating that object. It requires
me as library author to create such an object or require a library which create it for me.
Which I think is the problem I would like to avoid.

With string and array I would not need to take care a lot and need to ask me that complicate question.
The Developer using my Library will give me the template they want to render and I will call the injected Render
with the data I will provide. I think really the best developer experience is keeping it simple with string, array.

Still I'm curious how the object you had in mind should look like.
Can you show an example what you had in mind?
Would help to understand maybe better what advantages it really would had.

Thx,
Alex

Carle Dev

unread,
Jun 1, 2022, 4:03:44 PM6/1/22
to php...@googlegroups.com
Alexander,

This is really great work and I appreciate the effort you put into it.

I support this continuation of this proposal.

Regards,

--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.

Larry Garfield

unread,
Jun 1, 2022, 7:05:53 PM6/1/22
to PHP-FIG
Please bottom-post if replying to someone who already bottom-posted.

Regarding the string for the template name, what mbabker replied on the GitHub thread is basically my point.

If I, as a random library dev that wants something rendered, want to call this method, then right now I would have to one of the following:

Twig:
$this->template->render('controller/welcome.html.twig', [...]);

Laravel Blade:
$this->template->render('welcome', [...]);

Smarty:
$this->template->render('welcome.tpl', [...]);

Latte:
$this->render('path/to/welcome.latte', [...]);

Just standardizing that the first argument is "a template name" isn't enough. Is that string a file name? File path? Relative to where? Lookup ID from some index maintained elsewhere? How do I then tell the framework my library is getting plugged into what template ID needs to be defined somewhere?

Those are all things that COULD be answered successfully, but MUST be answered consistently for this proposal to work. That's what a working group would be responsible for hammering out.

Also, how do I, as a random library author, provide a default template if that template is naturally going to be in one particular template system or another? Or am I not allowed to? If I don't, does that mean I cannot actually test my library without pulling in some template engine?

Again, there probably are solutions to that question, but they will only be found by bringing the right people to the table to make compromises and hash out those details.

In short, *do not focus on the spec right now*. The current proposal is very incomplete as it doesn't address these and other issues, but that's OK, it should be at this point. Focus on building the team who will think through finding these and other issues none of us have thought of yet, and figuring out a collective solution.

--Larry Garfield

Rasmus Schultz

unread,
Dec 18, 2023, 5:02:17 AM12/18/23
to PHP Framework Interoperability Group
Hi Alex,

I was wondering how my very simple template locator would fit with this:


As you can see, the interface for this is radically different - there is no string for the template name, and no array for the data.

After some thinking though, I realized this locator could actually use this PSR internally - meaning, my library, which currently uses plain PHP templates only, would be able to internally dispatch this interface, which means you could use the view-model approach (no string, no array) to dispatch any standard template engine. Pretty cool. :-)

The one area where this proposal falls short (as, from the discussion, you already know) is with its inability to stream. As you can see here, my library addresses this by having two methods, render() and capture()


What I would suggest, is to do the same for this proposal - rename render() to capture() and just have a second method with return-type void:

/**
 * Render the template with the given context data, directly to output.

 *

 * @param string $template

 * @param array<string, mixed> $context

 *

 * @return void

 *

 * @throw TemplateNotFoundExceptionInterface

 */

public function render(string $templatearray $context = []): void;

/**
 * Render the template with the given context data.

 *

 * @param string $template

 * @param array<string, mixed> $context

 *

 * @return void

 *

 * @throw TemplateNotFoundExceptionInterface

 */

public function capture(string $templatearray $context = []): string;

Now, I'm sure there will be some engines that are incapable of rendering directly to output - in those cases, render() would simply be implemented as e.g. echo $this->capture() and, of course, you wouldn't get the performance or memory advantage of streaming here.

But the proposal also wouldn't stand in the way of getting this advantage from template engines that are capable of streaming.

Looks like a good proposal, but addressing this issue seems easy enough and worth while, since it doesn't add any real complexity for implementors.

- Rasmus Schultz

Larry Garfield

unread,
Dec 18, 2023, 1:32:16 PM12/18/23
to PHP-FIG
> But the proposal also wouldn't *stand in the way* of getting this
> advantage from template engines that *are* capable of streaming.
>
> Looks like a good proposal, but addressing this issue seems easy enough
> and worth while, since it doesn't add any real complexity for
> implementors.
>
> - Rasmus Schultz

I like View Models as a conceptual design. It does side-step the "file name" problem, provides structure, etc. It's also consistent with how PSR-14 works, by using the PHP type itself as the branching/matching factor. There's two problems.

1. Virtually all frameworks today use response objects (whether PSR-7, HttpFoundation, or otherwise). The "streaming by default" templating approach is not compatible with that model. Basically everyone would need to switch from the common `render()` method to `capture()`, if done this way, and that's definitely not going to fly. While you could stream the template inside a streaming response object, I don't think anyone does, and that's rarely necessary or even a good idea.

2. While I'd love it if ViewModels were more common, as far as I know not a single major template engine that does that. Blade, Twig, and Smarty are all array-based. Latte supports view models, but still requires a template name. (https://latte.nette.org/en/develop#toc-parameters-as-a-class) Adopting a ViewModel-centric spec would require figuring out a way to allow the major engines to adopt it in a BC-compatible way (as PSR-14 was able to do).

If we could get 2-3 of the major engines on board with a ViewModel approach, using the class as the template indicator, I would absolutely support that. That seems like a large lift, however, especially as the two biggest engines (Twig and Blade) are part of projects that seem to not be interested in FIG. :-( We might be able to get someone from Twig involved, but Blade feels unlikely. (I've no idea about Smarty or Latte.)

(There would almost certainly be other things that would need to be standardized, like how to mark a given property as being already escaped so no further escaping is needed, but those discussions aren't relevant until the above are addressed.)

--Larry Garfield

Erick de Azevedo Lima

unread,
Dec 19, 2023, 1:25:07 PM12/19/23
to php...@googlegroups.com
> If we could get 2-3 of the major engines on board with a ViewModel approach, using the class as the template indicator, I would absolutely support that.  That seems like a large lift, however, especially as the two biggest engines (Twig and Blade) > are part of projects that seem to not be interested in FIG. :-(  We might be able to get someone from Twig involved, but Blade feels unlikely.  (I've no idea about Smarty or Latte.)

Hi. Blade kind of supports view models when using components. In the internal framework of my organization I developed a wrapper around the views, which is a View class partially based on the spatie/laravel-views-models package (https://packagist.org/packages/spatie/laravel-view-models). My controller methods usually return views like this: "return new RandomView(pieceOfInfo: $info, contentTitle:'My page');". But this view class is entirely optional. In our team, when it's overkill to create a view model to render just one small stuff, one can still call the template rendering function directly. The ViewModel class can guess the template file name or you can set one yourself.

About adoption of this possible PSR: If we look at the usage of this spatie package, it has almost 2 million downloads, but I think Laravel does not want to integrate it in the framework because as it did with the spatie/laravel-blade-x components package (https://packagist.org/packages/spatie/laravel-blade-x). I feel they don't want to spend lots of time with the template engine because of the JS FE libs/frameworks and their Inertia lib.

But if we think of interoperability, maybe think on a higher level, like a Renderable or something like that, it could have a higher acceptance among the community.

Best regards,
Erick

--
You received this message because you are subscribed to the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to php-fig+u...@googlegroups.com.

Rasmus Schultz

unread,
Dec 21, 2023, 8:01:24 AM12/21/23
to PHP Framework Interoperability Group
Just to clarify: I was not suggesting we switch to a view-model approach.

That would fly straight in the face of interoperability for most existing template engines.

I was pointing out the fact that a view-model approach can be implemented on top of this PSR - meaning, you would be able to add a view-model library on top of the PSR template renderer abstraction.

Point being, this PSR does not need to be opinionated about that - the lowest common denominator for a view-model vs filename/array-based template engine is the same.

So in terms of interoperability, it would work perfectly fine as proposed. :-)

Rasmus Schultz

unread,
Dec 21, 2023, 8:01:59 AM12/21/23
to PHP Framework Interoperability Group
Hey Alex,

Thinking more about this...

I have to wonder if this really brings any actual interoperability between template engines?

Here's the thing.

Let's say you have some sort of welcome email service, and it needs to render a template:

$mailer = new WelcomeMailService(new TwigRenderer( ..... ));

Your mailer instance can now call the abstraction, e.g. $view->render("something", [ ..... ])

But the PSR draft kind of explains why that doesn't work - about the template argument, it says:

"It MAY be a file path to the template file, but it can also be a virtual name or path supported only
by a specific template renderer. The template is not limited by specific characters by definition
but a template renderer MAY support only specific one."

In other words, the argument itself is implementation-specific - it sounds almost like the definition
of a "leaky abstraction".

(Which, just to recap, the term "leaky abstraction" refers to a situation in software development
where the abstraction layer, which is designed to hide the complexity of a lower-level system,
fails to completely insulate the higher-level software from the details of the underlying system.
In other words, the abstraction "leaks" details that it was supposed to hide.)

Net result, there is no real interoperability here - it needs to be a string, but those strings could
be wildly different types that just happen to be represented as a string. An absolute or relative
path is in no way compatible with, say, a logical template name, whatever that might mean to
a specific template engine.

To return to my previous example and explain with a real world scenario, your welcome email
service would need to accept an engine-specific template name via it's constructor anyway:

$mailer = new WelcomeMailService("templates/welcome.twig", new TwigRenderer( ..... ));

The welcome service needs a template name that works for the renderer implementation - and
it needs these dependencies only for one reason, so it can put them back together at run-time.

If we back up and think high-level about what the WelcomeMailService needs from the renderer,
it just needs it to render a template - the WelcomeMailService has no use for the template name
whatsoever, apart from passing it to the renderer.

If a template renderer is going to work only with a specific type of template name, why even
burden the consumer with knowledge of a template name that's meaningless to it anyway?

You might as well reduce the abstraction to this:

interface Template
{
    public function render(array $data): string;
}

And now your WelcomeMailService can be ignorant of how the template was located:

$mailer = new WelcomeMailService(new TwigTemplate( "templates/welcome.twig" ));

The WelcomeMailService still achieves everything it needs to: it's able to render the template
when it needs, passing the template data (which isn't engine specific) to the template and
get back the rendered content. It doesn't need to know anything about a template name,
which wouldn't do it any good anyway, unless it knew which renderer was being used, what
or the syntax of the template name is, etc. - things it isn't supposed to know about.

I see why it would "feel good" to put template engines behind a similar abstraction... but when
the abstraction leaks the only important implementation detail -- which template engine you're
using -- it's difficult to see what exactly this buys you.

I think perhaps you're trying to erase a difference that can't really be erased.

Unless perhaps you were to have a PSR-specific definition of "template name" - something like:

"the template name identifies the logical template to render - it consists of filename-compatible
characters separated by a forward slash, which the Renderer implementation may resolve to an
actual template, usually a path/filename specific to conventions used by the Renderer in question."

This wouldn't leak anything - the WelcomeMailService can use an engine-independent call, such as:

$view->render("WelcomeMailService/welcome", [ .... ]);

A TwigRenderer might map this to "WelcomeMailService/welcome.twig", while a PHP renderer
might map this to "WelcomeMailService/welcome.php", and so on.

If you were to switch engines, you'd end up with missing template errors, rather than engine A
attempting to render a template written in engine B syntax.

I'm not sure which approach is better.

But I don't think the current approach works as-is?

On Wednesday, June 1, 2022 at 1:15:24 AM UTC+2 Alexander Schranz wrote:

Larry Garfield

unread,
Dec 21, 2023, 11:23:11 AM12/21/23
to PHP-FIG
All of the above is correct, and is the main reason this proposal has so far gone nowhere. :-)
There's a subtle difference here in approach, which is significant. What most (all?) engines today do is:

$engine->render($file, $args);

The problem is, as you note, $file is engine-specific, so non-portable.

Your earlier view-model suggestion would instead use:

$engine->render($view_model);

Where the type of the $view_model gets translated to a template file however the engine wants, and the properties of the $view_model are the $args. This solves the genericity problem, at the cost of being unconventional.

Your latest suggestion with TwigTemplate above becomes:

new Service($templateDefObject);

But... there is no engine. Presumably you would also have to provide an $engine to Service:

new Service($engine, $templateDefObject);

So that Service could internally do:

$engine->render($templateDefObject, $args);

Which is effectively isomorphic to your second suggestion above:

$engine->render($template_def_string, $args);

Just using a genericized string for the definition vs a carrier object. The genericized string is, effectively, what I asked for a year ago for this proposal to have a chance of going anywhere. :-) It never happened, so it never went anywhere.

Personally, I prefer the full-on view model approach. PHP types can encapsulate quite a bit these days, which would (as in PSR-14) offer decent fallback support via parent types and interfaces, as well as be more self-debugging, etc. It would also be template agnostic. Additional context could be provided by optional named arguments (like $format for "html", "rss", "text", etc.), and those could be engine-specific-extended without breaking anything if we define a few base ones.

There's probably somewhere that would break, but the biggest blocker is, as noted, getting existing engines on board with this. If we can do that, we have options. If not, there's nothing to do.

--Larry Garfield

Rasmus Schultz

unread,
Dec 21, 2023, 11:50:30 AM12/21/23
to PHP Framework Interoperability Group
The thing about view-models is, you can already do that, with any engine - just pass ["view" => new ViewModel()]

So there is really no reason to try to push this opinion on the ecosystem - many developers are perfectly
happy and comfortable with just passing an array.

To be clear: my "view-model suggestion" was never a suggestion - I am not and was never suggesting that. :-)

> Your latest suggestion with TwigTemplate above becomes:

> new Service($templateDefObject);

> But... there is no engine. Presumably you would also have to provide an $engine to Service:

Yes, sorry, I was too quick with that example.

What I meant to suggest was, we'd have an abstraction representing a *specific* template:

interface Template
{
    public function render(array $data): string;
}

But the example I showed was confusing - what I meant was that e.g. Twig would provide a factory
method for a Template instance, abstracting away the template and template-specific engine:

class TwigTemplate implements Template
{
    public function __construct(private string $template, private Twig $twig)
    {}

    public function render(array $data): string {
        return $this->twig->render($this->template, $data);
    }
}

Maybe this would be a third-party adapter, maybe Twig would add a create() method, not important.

The point is your service knows nothing about the name of the template being rendered - so:

$template = $twig->create("templates/welcome.twig");
// or:
$template = new TwigTemplate("templates/welcome.twig", $twig);

and then all your service knows is there's a template:

$mailer = new WelcomeMailService($template);

It doesn't know anything about a path - each Template instance represents a single template.

You don't get the convenience of avoiding engine-specific bootstrapping this way - you only
get the decoupling from the engine and it's implementation details, e.g. logical name or filename.

I'm not saying which is "best" or making a recommendation - just trying to lay out the facts. :-)


Matthew Weier O'Phinney

unread,
Dec 27, 2023, 4:29:58 PM12/27/23
to php...@googlegroups.com
<snip>
Interestingly, this approach is exactly what we chose for Expressive years ago, and which now persists in Mezzio. Because we understood that different engines had different ways to refer to template paths, we enforce a "namespace" syntax:


It works, but you do end up with a leaky abstraction that may not work across all systems. Past the initial namespace, you can have a mixture of `::` and `/` to delineate templates in subpaths, and it gets messy. But it does mean that you largely do not need to care what the implementation is in order to render something.

We enforce the basic namespace functionality via a test suite that implementations are expected to pass. The nice part is that you don't need to build this into the existing template engines; you can instead have a library that decorates the existing engine and proxies to it under the hood.

Going back to this bit from Rasmus from earlier:

> If a template renderer is going to work only with a specific type of
> template name, why even
> burden the consumer with knowledge of a template name that's
> meaningless to it anyway?
>
> You might as well reduce the abstraction to this:
>
> interface Template
> {
>     public function render(array $data): string;
> }
>
> And now your WelcomeMailService can be ignorant of how the template was located:
>
> $mailer = new WelcomeMailService(new TwigTemplate( "templates/welcome.twig" ));
>
> The WelcomeMailService still achieves everything it needs to: it's able
> to render the template
> when it needs, passing the template data (which isn't engine specific)
> to the template and
> get back the rendered content. It doesn't need to know anything about a
> template name,
> which wouldn't do it any good anyway, unless it knew which renderer was
> being used, what
> or the syntax of the template name is, etc. - things it isn't supposed
> to know about.

My initial feeling is that this feels like an inversion of responsibilities. It should be the _engine's_ responsibility to render a template, not the template's responsibility to render _itself_. Essentially, every `Template` instance would compose the engine; it feels a lot like active record in that way. It also means that if you were potentially rendering more than one possible template in a given service/handler/whatever, you end up having to pass each and every one via dependency injection, which can balloon quickly (imagine having several different different error templates based on the type of error, or displaying a form error versus a form continuation, etc.) Passing the _renderer_ makes this simpler in those cases.

On the flip side, I could see a `Template` being a stateful instance within a given request, allowing it to aggregate variables/data (e.g., authentication status, authorization roles, etc.) via middleware, until the handler renders it. The problem with this, however, is that it contains the engine itself, which has all the same drawbacks as an ActiveRecord when it comes to debugging.

Returning to Larry's argument...
 
Personally, I prefer the full-on view model approach.  PHP types can encapsulate quite a bit these days, which would (as in PSR-14) offer decent fallback support via parent types and interfaces, as well as be more self-debugging, etc.  It would also be template agnostic.  Additional context could be provided by optional named arguments (like $format for "html", "rss", "text", etc.), and those could be engine-specific-extended without breaking anything if we define a few base ones.

There's probably somewhere that would break, but the biggest blocker is, as noted, getting existing engines on board with this.  If we can do that, we have options.  If not, there's nothing to do.

Having worked with PSR-14 a fair bit, I _do_ like this approach. Mapping a _type_ to a _template_ is relatively easy, and allows for both a variety of approaches as well as things like extension and decoration. I disagree with having named arguments to determine format; I think that information can be baked into the view model. One way to do this effectively would be to decorate a view model into a generic one representing the content type to generate, and then allow the renderer to compose strategies based on that:

    final class RssViewModel
    {
        public function __construct(public object $decoratedModel) {}
    }

The renderer sees the RssViewModel, and passes it to a strategy that knows how to render an RSS feed, which in turn pulls the $decoratedModel to get the data to use in the feed. This would allow having helpers like the following in your handlers:

    private function decorateViewModel(ServerRequestInterface $request, object $viewModel): object
    {
        return match ($request->getHeaderLine('Accept')) {
            'text/html' => new HtmlViewModel($viewModel),
            'text/xml' => new RssViewModel($viewModel),
            'application/json' => new JsonViewModel($viewModel),
            default => new HtmlViewModel($viewModel),
        };

Allowing you to then:

    $response->getBody()->write($renderer->render($this->decorateViewModel($viewModel)));

(Clearly, you'd use something like willdurand/negotiation for the actual matching, but you get the gist).

The view model approach has another benefit: you can't forget _required_ data when you render. I can't tell you how many times I've discovered that the reason a page is broken is because of a mistyped array key, or just plain missing keys. Having actual typed view models helps developers ensure that they are providing all the information necessary to render a template.

I don't think we necessarily need to worry about having buy-in from the various template engines, either. The nice part about this is that all of this work — mapping view models to templates, calling the engine with the appropriate data — can all happen in _third-party libraries_ that _implement_ the FIG standard, but _proxy_ to the underlying engine. Those implementations can handle how to pass data from the model to the engine, or even have their own conventions that then _work with_ the engine. (As examples, they could pull data from any public properties of the view model; or they could pass the view model as a "model" or "view" template variable; or the renderer could make use of JsonSerializable or a `__toString()` method;  or the view model could be bound as "$this" in the template; etc. Application developers would choose the implementation that suits their application and/or development needs.)

This approach sidesteps the whole "template composes its renderer". View models can compose other services if they want, but the point is that they do not contain the information needed to render themselves; that's up to the renderer.

On top of that, it bypasses the whole "create a spec for referencing templates", which is very difficult to test, harder to enforce, and likely leaky (talking from experience here!).
 
So, I'll toss my hat in the "go with a view model" ring. I think the following is as simple as it gets and as flexible as it gets:

    interface Renderer
    {
        public function render(object $viewModel): string
    }

Now, going back a few emails, there was discussion about returning the rendered string, vs _streaming_.

I think it's interesting... but streamed content is _very_ rare in PHP, and very convoluted to achieve. When using PSR-15 and PSR-7, I'd argue that might become the realm of a specialized StreamInterface implementation. But if we were to deal with it in this proposal, I'd argue for a `stream(object $viewModel): void` or `stream(StreamInterface $stream, object $viewModel): void` method, vs the proposed render/capture, as it would make it more clear the _context_  for rendering (a stream).

--
he/him

Rasmus Schultz

unread,
Dec 28, 2023, 6:30:55 AM12/28/23
to PHP Framework Interoperability Group
Great post, Matthew, thanks for engaging :-)

I gotta say, you've almost sold me on the idea of standardizing on view-models. :-D

As said, I've been using that approach for many years, so I obviously like the idea.

My reservation, as explained, is you could build this view-model based renderer on top of
a more low-level render($template, $data): string interface - which tells me this approach is
more likely a "high level abstraction", and may not be the "lowest common denominator" we
would need in order to achieve interoperability across view engines.

How would you address that point?

There's also the simple mismatch with the current ecosystem to consider - the low-level
render($template, $data): string interface aligns better with the template engine world.

If you have just two properties, say, a page number and a list of results, do you really want
to build a view-model for that? I honestly might, but some people won't, and I would feel
very opinionated trying to push that onto an ecosystem that does things differently.

Either way, there's a fundamental arity mismatch between the render($object): string approach
and the render($template, $data): string approach used by most template engines, which
generally allow multiple name/value pairs - a difference you would need to "erase" and simply
choose a (possibly fixed) name for the view-model underneath the hood.

From a practical perspective, existing projects that already use a view-engine with a similar
API (so the large majority) would have a much easier time migrating to a similar interface -
with the render($model): string interface, you're essentially "forcing" everyone to introduce
view-models, which is probably a huge amount of refactoring, and will almost definitely kill
adoption of the standard.

Regarding my Template interface idea, and your comparison with ActiveRecord - yeah, I can
sort of see that point, and I am definitely *not* a fan of AR... but your argument seems to be
about composing the template engine in the Template instance? Why is that bad?

How is it better that your controller composes the template engine? which is what you'd be
proposing instead, right? (I don't see how either approach is better or worse - they're the same.)

In my experience, composition behind abstractions is good - it's how we keep from coupling
consumers to implementation details. (assuming good abstractions that don't leak details.)

You say composition affects debugging? how? in all my recent years, I've used dependency
injection by default, so I compose everything, all the time - I've never felt like this gets me into
trouble with debugging, if anything I feel like it helps make it very easy to see how control
gets transferred from one unit to another. But you haven't said what the issue is, so I wonder
if I'm missing something specific to the case of rendering templates?

Regarding the render($object): string approach, I would point out as well that, while this is
appealingly simple on the surface, it's also a bit incomplete, as it lacks the ability to render
different templates with the same view-model.

To address that, you would most likely need a second (optional) argument to specify the
name of the template you want, e.g. render(object $model, ?string $view): string

Here's how my own library addresses that:


If you think about it though, that kind of brings the conversation about full circle - back to
the idea of passing in some sort of logical template name... this time along with the view-model,
which means the logical name is specific to the view-model type - but still.

All this just brings me back to the conversation about logical template names - I had a look at
your implementation, and I don't fully understand what your engine is doing... as you said:

> It works, but you do end up with a leaky abstraction that may not work across all systems.
> Past the initial namespace, you can have a mixture of `::` and `/` to delineate templates in
> subpaths, and it gets messy. But it does mean that you largely do not need to care what
> the implementation is in order to render something.

I'm not sure I understand the concept of "namespace" here - this is some way to refer to path
aliases, which some view-engines allow you to configure? I've never used an engine with that
feature, so it's new to me.

but I guess I would argue for something with much simpler syntax and semantics - these would
be logical "template identifiers" only, an abstract representation of logical template names that
can be interpreted differently by various engines.

The syntax would not tied to absolute file paths or physical locations - the logical name would
uniquely identify a template within the context of a template engine, but it is just a name.

so if you were to "myshop/cart/checkout", one engine might resolve that as:

"/app/templates/myshop/cart/checkout.twig"

while a different engine might be configured with a path alias resolving "myshop" as
"vendor/myvendor/myshop", yielding:

"/app/vendor/myvendor/myshop/templates/cart/checkout.php"

The logical template name is a set of groupings and a logical template name only.

Just as the template engine has to figure out how to actually render proprietary template
syntax, it also needs to implement conventions, configuration, or other strategies, to map
the logical template name to an absolute path to the actual template file.

I think what I'm proposing is kind of similar to what you're doing, just without any syntax
beyond the slash to separate groupings in the logical template identifier - this doesn't leak
any details about how the engine might resolve the logical path, as far as I can figure?

Cheers and thanks for taking the time to discuss! :-)

Rasmus

Larry Garfield

unread,
Dec 28, 2023, 11:20:32 AM12/28/23
to PHP-FIG
My concern here is how it interacts with inheritance, give that value objects these days generally use both readonly and CPP. I'd expect the above to match to something like

readonly class ArticleView {
public function __construct(
public string $title,
public UserView $author,
public string $body,
public array $tags = [],
) {}
}

readonly class ArticleViewHtml extends ArticleView {]

readonly class ArticleViewRss extends ArticleView {}

etc.

The problem there is that if you want to add a constructor argument to one of the child classes, PHP itself now makes that really ugly to do. (This is a PHP limitation.) It also means an explosion of child classes even if you don't add properties; Adding a new output type to an existing system with 300 view models means adding 300 classes. That's not good.

If we instead did:

$engine->render($articleView, format: 'rss')

The logic inside render() to locate the template becomes a lot simpler. The degenerate case is probably something like (mostly copying from PSR-0):

public function render(object $viewModel, string $format = 'html'): string
{
$className = $viewModel::class;
$fileName = '/template/root/';
$namespace = '';
if ($lastNsPos = strrpos($className, '\\')) {
$namespace = substr($className, 0, $lastNsPos);
$className = substr($className, $lastNsPos + 1);
$fileName .= str_replace('\\', DIRECTORY_SEPARATOR, $namespace);
}
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.' . $format;

return $this->doRender($fileName, $viewModel);
}

(Which, assuming I didn't typo something, would turn App\Views\ArticleView into /template/root/App/Views/ArticleView.html. More robust engines would of course do something more interesting.)

Adding fallbacks there to make additional formats easier is fairly straightforward.

> The view model approach has another benefit: you can't forget
> _required_ data when you render. I can't tell you how many times I've
> discovered that the reason a page is broken is because of a mistyped
> array key, or just plain missing keys. Having actual typed view models
> helps developers ensure that they are providing all the information
> necessary to render a template.

Absolutely. Easily half the argument for "use a defined type for things" is "because I keep typoing otherwise and losing hours debugging it." :-) (And related benefits, of course.)
An additional wrinkle is nested templates. Both where the template engine itself nests things (a la Twig blocks et al), or where different parts of the page are rendered down to a string separately so they can be separately cached, rendered in parallel, etc. (Something Drupal did, badly.) At minimum that means a Stringable AlreadyRenderedString class, which some template engines already have, to avoid double escaping. There's probably other concerns there but I haven't had a chance to think all of them through yet.

--Larry Garfield

Rasmus Schultz

unread,
Jan 12, 2024, 11:35:06 AMJan 12
to PHP Framework Interoperability Group
> The problem there is that if you want to add a constructor argument to one of the child classes, PHP itself now makes that really ugly to do. (This is a PHP limitation.) It also means an explosion of child classes even if you don't add properties; Adding a new output type to an existing system with 300 view models means adding 300 classes. That's not good.

> If we instead did:

> $engine->render($articleView, format: 'rss')

for the record, this is precisely what we ended up doing, for exactly that reason - to avoid the potential view-model explosion.

if anyone is interested in trying it out, it's available here: https://github.com/mindplay-dk/kisstpl

it's been used at scale in a large modular codebase with many contributors - people liked it, it worked and held up very well over time.

I will emphasize this again though:

1. changing this library to internally compose an $engine and call $engine->render($path) would be extremely easy
2. the simpler $engine->render($path) approach aligns perfectly with most engines and provides all the interoperability we need

the $engine->render($path) approach is a more low-level approach than the $engine->render($viewModel, "format") approach.

I obviously prefer the view-model approach - but personal preferences and opinions doesn't really mean much here.

PSR interfaces are supposed to provide interoperability, and the fact is that the $engine->render($path) approach, being a near
perfect match for existing template engines, does that better.

if we went with the more opinionated view-model approach, I can practically guarantee, the PSR will see much less adoption,
because it won't fit existing engines very well, and it won't align well with "end user" developer expectations - they will basically
all need to individually implement something similar to my view-finder package above, because that's really like a more high-level
abstraction than the more low-level $engine->render($path) approach, which is easy to add on top.

I would compare this situation to the PSR cache situation - and the outcome would very likely be the same. If we standardize on
the more high-level $engine->render($viewModel, "path") approach, as the original PSR-6 caching interface did, we would almost
definitely end up with a second "Simple Template Renderer" PSR later on, like how PSR-16 simplified caching.

if you're really adamant about pushing the view-model approach on the community, I would actually suggest you consider
including *both* interfaces in the PSR: the low-level TemplateRenderer and high-level ViewModelRenderer.

I could definitely see benefits to standardizing both.

but standardizing on just the ViewModelRenderer would be almost poignant - there is my package and maybe one or two others
using that approach (?) so maybe it has a bit of value in terms of interoperability, but for the majority of template engines, it would
just be a hassle to implement, whereas TemplateRenderer would be an easy slam dunk for practically every engine out there.

another case for two interfaces would be composability - since most template engines support something like TemplateRenderer
already, those implementations could focus on just the "template name to path" convention, while a ViewModelRenderer implementation
could compose a TemplateRenderer implementation, focusing on just the "view model and format to template name" convention.

I actually don't see these as competing ideas, but potentially as complimentary ideas.

in my opinion, this approach makes for better architecture, better separation of concerns. ("do one thing and do it well".)

having two interfaces with a shared concept of "template names" might make a lot of sense here?

community wise, I think that putting both ideas out there and letting nature decide would be a lot less biased than trying to pick one?

Rasmus

Larry Garfield

unread,
Jan 12, 2024, 12:05:56 PMJan 12
to PHP-FIG
I fully agree about the adoption challenge of a view-model-centric approach, and have said as much repeatedly. :-) A dual-design is an option, but it doesn't resolve the primary blocker of a path-based approach:

$engine->render('models/user.twig', $data);
$engine->render('models/user.latte', $data);
$engine->render('models/user.blade', $data);
$engine->render('models/user.smarty', $data);

Absent some good way to standardize the path string in an engine-independent way, I don't see how that can ever be a meaningful abstraction. Just accepting TemplateInterface in my class doesn't make it portable if I still have to specify ".twig" in the path. That is the problem that has to be solved, to the satisfaction of the big players, before that approach is viable at all.

So far, the only viable approach I've heard is to not do that and instead use view models, so the type system becomes the abstraction. Which it seems the consensus is "that would be really cool, but nobody actually does that so transitioning would be awful."

So I still don't see a viable way forward, much as I wish I did.

What we really need is for someone from Twig or Blade to weigh in. Unfortunately, that seems unlikely.

--Larry Garfield

Rasmus Schultz

unread,
Jan 12, 2024, 3:04:25 PMJan 12
to php...@googlegroups.com
what's wrong with this?

$engine->render('models/user', $data);

every engine takes a logical template name, and applies whatever convention it uses - which for
most engines would mean adding the appropriate root path and file extension, while other engines
might use the first part of the logical name to map to one of several registered root paths.

> the only viable approach I've heard is to not do that and instead use view models, so the
> type system becomes the abstraction

this doesn't really change anything though - under the hood, that view-model name still needs to
be resolved to an engine-specific absolute template path with a file extension.

so you still have the exact same problem - now it's just one layer below the thing you're looking at.

the ViewModelRenderer needs to do everything a TemplateRenderer would need to do, and more.

I don't think there's any way around that - but that's probably okay? depending on what you think
this kind of abstraction buys you in the first place? :-)

for me, it would be something like: I can ship a login service module, and this module can send a
password recovery email - that service can call $engine->render('login-service/lost-password', $data)
and by injecting a different $engine you can choose the template syntax you'd like to use in your
project for that service, with the path/filename conventions appropriate for that engine.

in terms of interoperability, what else were you hoping for? :-)

Rasmus


--
You received this message because you are subscribed to a topic in the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/php-fig/X4e1z5IaG9E/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/php-fig/4dcd1e45-239a-45ce-8bb8-8332406bd4d5%40app.fastmail.com.

Larry Garfield

unread,
Jan 12, 2024, 3:34:49 PMJan 12
to PHP-FIG
Agreement from the major template engines to omit the extension in their code, and standardize on what / means. :-) Note also that some (eg, Twig) commonly include a format in the file name that needs to be thought through, as well. (IMO, moved to a named arg.)

That's a solvable problem. But so far, no one has done the work to try and get the right parties in the room.

--Larry Garfield

Rasmus Schultz

unread,
Jan 12, 2024, 4:29:39 PMJan 12
to php...@googlegroups.com
> Agreement from the major template engines to omit the extension in their code, and standardize on what / means. :-) 

why?

I would never expect that.

the interface would most likely serve as an adapter - just implements whatever is needed to convert a logical template name to whatever the engine uses.

I don't see any reason the template engine's own API would need to change.

template engines could directly implement the interface, or provide an adapter, built-in or as a separate package - for that matter, any third party could write an adapter if they need one, it wouldn't be difficult.

I don't think you need anybody's blessing, really? template engine APIs are already similar enough that this is doable in principle without any involvement by template engine vendors.

if the community wants this standard, that's actually all the buy-in you need. :-)

Rasmus


--
You received this message because you are subscribed to a topic in the Google Groups "PHP Framework Interoperability Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/php-fig/X4e1z5IaG9E/unsubscribe.
To unsubscribe from this group and all its topics, send an email to php-fig+u...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages