> 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

On Thursday, December 28, 2023 at 5:20:32 PM UTC+1 Larry Garfield wrote:

On Wed, Dec 27, 2023, at 3:29 PM, Matthew Weier O'Phinney wrote: 



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


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

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

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 

-- 
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/0d6e8c76-ff19-4cae-a6c6-f75af45b53d8n%40googlegroups.com.

Reply via email to