-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Add support for functions/filters/tests stubs #4304
base: 3.x
Are you sure you want to change the base?
Conversation
I think this could work to solve the linked issue. But, let me add some comments. Imagine that your bundle template uses (1) If it's available, the correct Twig function is used The (2) problem is solved by this PR, but there are some sub-cases when defining a stub: (2.1) You define an empty/simple stub (e.g. My use-case would be to always use (2.3). This way:
So, if for other folks the use case is always (2.3) or similar, we could do something like Symfony's
|
We need the function because we need its signature. But I like this idea. I'm not sure it covers that many use cases though. |
In its current form, it looks like that this is just syntactic sugar over what undefined callable callbacks can do already (the implementation could correspond to registering an undefined callable callback that return I'm not sure we need this shortcut implementation as a separate API in the Environment itself. |
Maybe this is just a recipe in the documentation then? |
For the importmap scenario i wonder if something like this could do the work, without the negative impact {% if function('importmap') is defined %}
{{ importmap('app', ....) }}
{% endif %} or maybe {% if 'importmap' is function %}
{{ importmap('app', ....) }}
{% endif %} As you said @javiereguiluz, either
As i see it, stubs could be really usefull in functional tests, but i imagine this is not the same need |
No, it's not possible as the function must be defined at compilation time. |
7e69f99
to
e0feaf1
Compare
I've pushed Javier's suggestion: Is it worth it? Would a recipe in the docs be enough? |
I like this, and I think this would work in most cases. Thanks. But, looking at some real example, I think we might need something more. E.g. I have this in the Twig template: <form class="d-none" method="post" id="delete-form">
<input type="hidden" name="token" value="{{ ea_csrf_token('ea-delete') }}" />
</form> Thanks to this PR, I'd change
But, this could be confusing because the CSRF token is like an indirect feature not related to what the user is doing at this moment. So, maybe the stub should allow to define an optional error message to display to the user? public function registerFunctionStub(string $name, string $package, ?string $customErrorMessage): void And I'd use it like this: registerFunctionStub(
'csrf_token',
'symfony/security-csrf',
'When using the delete action on any backend page, EasyAdmin generates a CSRF token to protect your application. This uses the "csrf_token()" Twig function provided by the "symfony/security-csrf" package. So, you must install that package in your app by running "composer require symfony/security-csrf"'
); |
Just a random thought. This feature is needed by a small niche of apps like backend bundles that don't have control over the app where they are installed. So, maybe we could simplify this by creating a So, my original Twig template: <form class="d-none" method="post" id="delete-form">
<input type="hidden" name="token" value="{{ ea_csrf_token('ea-delete') }}" />
</form> Would become: <form class="d-none" method="post" id="delete-form">
{% try %}
<input type="hidden" name="token" value="{{ csrf_token('ea-delete') }}" />
{% catch %}
When using the delete action on any backend page, EasyAdmin generates a CSRF token to protect your application.
This uses the "csrf_token()" Twig function provided by the "symfony/security-csrf" package. So, you must install
that package in your app by running "composer require symfony/security-csrf"
{% endtry %}
</form> |
I may not have understood all this correctly, so please correct me if i missread something :) DX (before)I took an open project (no security, no easyadmin) and added
Here, i understand the situation, the "problem" and the solution. DX (after) ?With this stub registration, if easyadmin is installed in the project, i'll read this:
It would feel very confusing to me, as nothing here is related to what i did 🤷♂️ |
@smnandre for this But here's a more realistic use-case. We added support for AssetMapper in EA very early because AssetMapper is great. BUT, many people still use WebpackEncore or nothing at all. So, we have to use this in the template: {% block head_javascript %}
{# ... #}
{% block importmap %}
{{ ea_importmap(assets|map(asset => asset.value)) }}
{% endblock %}
{% endblock head_javascript %} We cannot use The ideal solution is what you proposed earlier: {% if function('importmap') is defined %}
{{ importmap('app', ....) }}
{% endif %} or {% if 'importmap' is function %}
{{ importmap('app', ....) }}
{% endif %} but apparently is not possible technically speaking. |
Thank you for the example ! I imagine you have something similar for webpack ? So the two of them must be "detected" ? Regardless of the stubs.... i may have something that could help you In your bundle recipe, do you know you can target / differenciate assetmapper and webpack users ? Exemple with the StimulusBundle recipe {
"file": "assets/bootstrap.js",
"content": "import { startStimulusApp } from '@symfony/stimulus-bridge';\n\n// Registers Stimulus controllers from controllers.json and in the controllers/ directory\nexport const app = startStimulusApp(require.context(\n '@symfony/stimulus-bridge/lazy-controller-loader!./controllers',\n true,\n /\\.[jt]sx?$/\n));",
"position": "top",
"requires": "symfony/webpack-encore-bundle"
},
{
"file": "assets/bootstrap.js",
"content": "import { startStimulusApp } from '@symfony/stimulus-bundle';\n\nconst app = startStimulusApp();",
"position": "top",
"requires": "symfony/asset-mapper"
}
This is how the bridge/bundle is installed with two ways, the "good one" being selected wether the packages are installed and the precence of key files :) So maybe you could do something similar to change/comment a file (ex: a twig path?) or a line of your configuration ? Not a silver bullet but it's always good to know (if you did not already 😅 ) |
As we need to execute the test at compilation time, here are two proposals: {% if when compiling 'importmap' is function %}
{{ importmap('app') }}
{% else %}
importmap does not exist
{% endif %} I've implemented this one and it works well. Basically, it only executes the branch for which the condition is or: ## if 'importmap' is function ##
{{ importmap('app') }}
## else ##
importmap does not exist
## endif ## |
👏 I love both syntaxes! Also, this could open the door for nice things .. what are the limits / constraints ? |
The proposed syntaxes look a bit rough to me. Even if we're talking about an advanced feature here, we should keep Twig somewhat friendly to designers. Another random proposal: introduce {% if function_exists('importmap') %}
{{ importmap('app', ....) }}
{% endif %}
{% if filter_exists('u') %}
{{ user.biography|u.truncate(120, '…', false) }}
{% else %}
{{ user.biography[:120] }}
{% endif %}
{% set is_foo = test_exists('foo') ? (variable is foo) : false %} And don't throw exceptions or output any messages to users:
|
The main "problem" is that the test expression (whatever the syntax) must be evaluated at compile time to avoid parsing the branch that won't work. So, we need a way to signal that to the parser. |
Another syntax (whatever we end up with) is probably the way to go (like a pre-processor). |
I thought the (1) Original template code: {% if function_exists('importmap') %}
{{ importmap('app', ....) }}
{% endif %}
{% if filter_exists('u') %}
{{ user.biography|u.truncate(120, '…', false) }}
{% else %}
{{ user.biography[:120] }}
{% endif %}
{% set is_foo = test_exists('foo') ? (variable is foo) : false %} (2) A new compiler pass that evaluates the functions at compiling time and replaces them by Let's consider that {% if true %}
{{ importmap('app', ....) }}
{% endif %}
{% if false %}
{{ user.biography|u.truncate(120, '…', false) }}
{% else %}
{{ user.biography[:120] }}
{% endif %}
{% set is_foo = false ? (variable is foo) : false %} About this concern --> "So, we need a way to signal that to the parser." Just using these (3) A DCE (dead code elimination) compiler pass simplifies the code like this: {{ importmap('app', ....) }}
{{ user.biography[:120] }}
{% set is_foo = false %} |
@javiereguiluz the issue is not the |
Quick PoC for #3655
@javiereguiluz Would that help?
Usage: