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

Allow Chain Rendering #22

Open
XedinUnknown opened this issue Mar 12, 2020 · 6 comments
Open

Allow Chain Rendering #22

XedinUnknown opened this issue Mar 12, 2020 · 6 comments
Assignees
Milestone

Comments

@XedinUnknown
Copy link
Member

The Problem

Sometimes, not all of a template's placeholders can be satisfied by just one context. In cases where there is a single consumer, multiple contexts can be merged:

$greeting = new StringTemplate('Hello, {name} {surname}!');
$ctx1 = ['name' => 'Bob'];
$ctx2 = ['surname' => 'Page'];
echo $greeting->render(array_merge($ctx1, $ctx1)); // Hello, Bob Page!

This is harder to do with non-array contextx, such as ContainerInterface. Furthermore, and most importantly, there isn't always a single consumer which can satisfy all of the tokens. For example, one part of the software may have knowledge of some of the context, while another part would have knowledge of the rest of the context. In these cases, the rendered result must be wrapped into another template.

// Somewhere
$result = $template->render(['name' => 'Bob']);
return new StringTemplate($result);

// Somewhere else
$result = $template->render(['surname' => 'Page']);
echo $result; // Hello, Bob Page!

Needless to say, this is quite inconvenient. It requires explicit knowledge of another template class in the first part, thus coupling it to the implementation. It can also result in coupling to some implementation details, such as what the token format is. This is not something that the consumer should know: they are consuming a TemplateInterface. If a factory is used for de-coupling, then that factory must first be injected. At the same time there's no guarantee that the details such as the token format will persist between factories.

Suggested Approach

Add a renderToTemplate($ctx): TemplateInterface method to TemplateInterface. It would work the same way as render(), except it would return another instance of the same template implementation which represents the rendered result. This allows preserving implementation details, and avoiding coupling/factory injection.

$greeting = new StringTemplate('Hello, {name} {surname}!');
$ctx1 = ['name' => 'Bob'];
$ctx2 = ['surname' => 'Page'];
echo $greeting->renderToTemplate($ctx1)->render($ctx2);  // Hello, Bob Page!
@mecha
Copy link
Member

mecha commented Mar 12, 2020

This "problem", let's call it that, is not a result of a lacking API in an interface, but one that emerges from how consumers use implementations of said interface. As such, adding a method to the interface is a poor solution IMO. A consumer problem should ideally be solved by consumers.

Furthermore, the suggested solution would cause templates to be rendered multiple times, which is inefficient.


How about just decorating the template?

Consider an implementation TemplateDecorator that accepts a TemplateInterface and a context during construction. On render, it merges the render-time context with its construction-time context.

// Somewhere
$template2 = new TemplateDecorator($template1, ['name' => 'Bob']);

// Somewhere else
$result = $template2->render(['surname' => 'Page']);
echo $result; // Hello, Bob Page!

@XedinUnknown
Copy link
Member Author

If multiple contexts need to be provided at different times, it will get rendered multiple times either way. Merging contexts is good, but not really a solution to the problem

The problem with your solution is that it requires knowledge of a template decorator class, which has the same issue as another template.

This is indeed a problem of consumers. However, a convenient solution would need to be re-usable.

@mecha
Copy link
Member

mecha commented Mar 12, 2020

Well, if the first consumer needs to render, then obviously it doesn't make sense for it to have to use a decorator class, and should render() instead. So I guess it depends on what exactly you're trying to solve.

Although I would question why exactly a template, that doesn't have the full context, needs to render in the first place. Maybe it doesn't and you're trying to solve a leaf problem, when the root problem is higher up.

@XedinUnknown
Copy link
Member Author

So, you're saying that the ability to render a template into something once again renderable is not useful? The render result may contain other placeholders, for example.

@XedinUnknown XedinUnknown added this to the 0.4 milestone Mar 12, 2020
@XedinUnknown XedinUnknown self-assigned this Mar 12, 2020
@XedinUnknown
Copy link
Member Author

XedinUnknown commented Mar 12, 2020

Another approach: allow default context, and allow adding to it.

$template = (new StringTemplate('Hello, {name} {surname}!'))
	->withContext(['name' => 'Bob', 'surname' => 'Zyorunkle']);

echo $template->render(['surname' => 'Page']); // Hello, Bob Page!

@mecha
Copy link
Member

mecha commented Mar 14, 2020

So, you're saying that the ability to render a template into something once again renderable is not useful? The render result may contain other placeholders, for example.

I'm not saying that at all. If the result of rendering is another template, then it could make sense. But the scenario for which you opened this issue, literally the first line, was this:

Sometimes, not all of a template's placeholders can be satisfied by just one context.

I simply think there's a better way to deal with situations like these than rendering a template multiple times.


Wouldn't adding withContext() yield the same effect as decoration? The only difference I can see is that one obeys SRP and the other clutters the template interface.

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

No branches or pull requests

2 participants