twigphp / Twig

Twig, the flexible, fast, and secure template language for PHP
https://twig.symfony.com/
BSD 3-Clause "New" or "Revised" License
8.14k stars 1.25k forks source link

First-class lambdas #4005

Open Bilge opened 6 months ago

Bilge commented 6 months ago

It is noted that when calling functions/macros specifically, one can use lambdas as discussed here. However, it is not possible to assign a lambda to a variable or a hash literal. Therefore, it is not possible to pass a lambda from one template to another. However, I would like to propose first-class lambda support is possible in Twig.

To establish why this may be necessary, consider a template that contains a loop. We want to replace some content within the loop from another template. Since it is not possible to pass lambdas, we cannot do this.

{# app_list.twig #}
{% for app in apps %}
{% block score %}{{ (app.score * 10)|number_format(2) }}{% endblock %}
{% endfor %}
{# my.twig #}
{% extends 'app_list.twig' %}

{# This makes no sense because `app` is either undefined in this context or refers to something else;
it does not refer to each entry in `apps` as in app_list.twig. #}
{% block score %}{{ (app.score * 100)|number_format(0) }}{% endblock %}

First-class lambdas would be one way to solve this problem.

{# app_list.twig #}
{% for app in apps %}
{{ scorer(app)|default((app.score * 10)|number_format(2)) }}
{% endfor %}
{# my.twig #}
{{ include('app_list.twig', {
  scorer: app => (app.score * 100)|number_format(0),
}) }}

Note that we had to change our reuse strategy from inheritance (extends) to include, since there doesn't seem to be a logical way to pass a lambda to a block.

Whilst one could make the case for supplying app.score already formatted from the backend, this will not always be possible or ideal architecture, since the knowledge of which formatting to use may depend on which specific combination of templates is used, which is only known by the front-end. It should also be considered impractical to create twig extension functions for every possible permutation of score formatting.

stof commented 5 months ago

this would require removing the validation of known functions at compile time by replacing any unknown function with a runtime call to a lambda, because there would be no way to know at compile time whether scorer(app) is meant to be a call to a lambda or to a Twig function. And this also leads to questions about precedence between variable names containing lambdas (known only at runtime) and Twig function names in case you have both names. Reusing the scorer() syntax to call lambdas means that we don't have separate naming spaces anymore for functions and variables and we need to define priority. And any priority would be confusing IMO:

fabpot commented 1 week ago

I agree with stof. But calling scorer.__invoke() would work, right?

Related to #3848