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:

https://github.com/mindplay-dk/kisstpl/blob/7d59e10da48b2a86f14244c490231a56f78c2611/src/Renderer.php#L27-L41

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

On Wednesday, December 27, 2023 at 10:29:58 PM UTC+1 Matthew Weier 
O'Phinney wrote:

On Thu, Dec 21, 2023 at 10:23 AM Larry Garfield <la...@garfieldtech.com> 
wrote:

On Thu, Dec 21, 2023, at 11:27 AM, Rasmus Schultz wrote:
> 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.

All of the above is correct, and is the main reason this proposal has so 
far gone nowhere. :-)


<snip>
 

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

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.


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:

- https://docs.mezzio.dev/mezzio/v3/features/template/interface/#namespaces

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

-- 
Matthew Weier O'Phinney
mweiero...@gmail.com
https://mwop.net/
he/him

-- 
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+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/php-fig/bd2d63b8-e862-4fb6-8ce0-c71d51f5279bn%40googlegroups.com.

Reply via email to