-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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 tag to declare variable types #4165
Comments
Would you mind linking
with #3928 so we can see, when this is unblocked by this issue? :) |
If I may suggest: instead of deciding the possible var type values, why not accept any valid PHPDoc type. Together with PHPStan's PHPDoc Parser we can parse them into PhpDocNode's that then can be transformed to PHPStan types. From there, it can be used to build compile time assertions. Doing it from scratch feels like reinventing the wheel 😅 We could still only accept a subset from the possible types, and let others improve it over time via PR's. |
Give it enough time and Twig will end up being PHTML, the format it was supposed to replace. |
Thanks, that's a good suggestion. I was mindful of not reinventing the wheel and hoped that we could re-use parts of PHPStan to parse and evaluate the types. Should we perhaps go all the way and support any PHPStan type? I'll add a sentence in italics to that section for now. I'll add support for nullable types, which I forgot. But the more gets added, the more sense it makes to just go all the way |
I'm not sure if this would be the best place to implement this, but a lot of times I have a flag in a template to allow certain markup to be turned on or off, but need to retain previous functionality for other templates that are already including it. I usually have to add a |
{% var variable Type %} I think that the order should be the same as with inline PHPDocs: {% var Type variable %} |
I agree with @ruudk on the order. It would also match how class properties and argument types are defined in PHP. Also, going along with that and clarifying what I was proposing in #4165 (comment), this is what it would look like to specify a default value: {% var Type variable = 'default_value' %}
{% var bool flag = false %} |
Agreed. I've updated the OP. (While I prefer the original order, we should follow existing conventions.) We could also consider an optional description. phpDocumentor supports this, but PHPStan doesn't seem to.
Then again, this makes the tag less readable, plus there's nothing Twig itself would do with this. The alternative is to just add a comment (before or afterwards) for humans:
|
What's the point of providing a description there? Same can be done with Twig: {# I need to explain this somehow #}
{% var 'bool' foo %} PS: The Type should be a string (ConstantStringExpression). The variable could be a NameExpression or ConstantStringExpression. |
I personally actually disagree on principle: declaring the type and any value should not be mixed. This does bring up two interesting questions, though. First, should we add Second, what do we do about variables defined using |
Exactly. I was just thinking out loud.
Why should the type be quoted? It's not an expression (although I confess not to know the definition of "expression"). It also looks really weird to me and is inconsistent with PHPStan. Do you think the implementation could be tricky if the type isn't quoted? Update: the implementation does indeed run into issues. Therefore, I've amended the spec to quote types. |
@drjayvee can you explain more? |
@willrowe, absolutely! I should have done that straight away. But first off, I want to point out that this is just my personal opinion. I'd love to hear what other people think. One reason I dislike your proposal is that Twig already has three ways of setting values:
I feel like adding a fourth way leads to bloat. (Or worse, conflicts.) Macros arguments already have support for default values. (In fact, they're never even undefined, since they get an implicit default of The {% set foo = foo ?? 'bar' %}
{# or, in case null must be kept as-is, more strictly: #}
{% set foo = foo is defined ? foo : 'bar' %} The second problem is that it feels like mixing concerns. I'm envisioning the However, I'm not super convinced by my own argument here, since both PHP and Typescript (as just two examples) do let you declare types and default values in one go in many places (e.g., function default values and class properties). A third problem is that I hope I explained clearly. I'm sincerely curious as to you what you think now you know my reasoning. |
What does it mean? What is |
@drjayvee those are all great points and illustrate how you see this feature being utilized. Where we differ is that I think about this as more of a dockblock/argument list at the top of the template file (similar to the Keeping the above in mind:
I see this as augmenting the template context specifically, so if a
Since I am thinking of this as a single definition at the top, it would be different than
I think macros are separate from what I am talking about.
I don't see it as an additional way to set variable values within the template if it were to only affect the context as it comes into the template. It would be overridden by any values provided by the user.
I'm not talking about macros, just template file context.
As I said in my original comment, I already do this, but am looking for a way to easily combine the documentation of variables within a template file and a default value.
This is where we are thinking about the tag differently. It seems like adding the ability specify a type to the existing
I think it would be much more powerful to have the variable type information available after compiling, since that would allow introspection of what variables a template file expects to be passed as context, among other things. Compiling the types could also possibly allow Twig to leverage built-in PHP type checks so that checks don't necessarily need to be run manually at runtime, but would rather be implicitly checked. All this being said, I think it comes down to whether it would be used as a way to define variable types once for the whole file, or inline. |
This tag declares that variable The PHP equivalent for a function argument would be function bar(?\Name\Space\ClassName $baz) {} |
@willrowe thanks for your detailed response. It does indeed appear we have slightly different use cases in mind. Documenting / specifying the template's context variables similar to function arguments is an explicit goal of this proposal. The current proposal is more generic: you can use Consider the following example: {% if user.isLoggedIn() %}
{% set userName = user.fullName %} {# Class property is ?string but guaranteed to be string if user is logged in #}
{% else %}
{% set userName = 'Anonymous' %}
{% endif %}
{% var string userName %} The That being said, I very much would like the community to weigh in here. Speaking of: @fabpot, you mentioned you wanted to involve some interested parties, no? I'd also like to know your thoughts on the proposal so far. |
@drjayvee thanks. How do you declare to the twig compiler that this class exist? Said differently, at compile-time, how is the compiler expected to check that this type exist? |
Simply by using Do you see any other problem? Or am I overlooking something here? |
@drjayvee you are explaining how the php implementation would work. But how would the other implementations work? More generally, from a specification point of view, how would |
@drjayvee also it is fundamental to establish how type checking would work? Let's say the a variable
What would be the algorithm that compilers are supposed to implement to check that As I said earlier in this thread in an ironic manner, I'm concerned that twig would become a proper typed programming language, which means that it would also come with a complexity in both specification and implementation that the TwigPHP team may not be ready to face. I recommend that you have a look at how typed languages are specified and how their compilers are implemented before making a decision about this feature: this is very very complex topic. |
I think there are two different things we are talking about here in regards to a template's context variables:
I think it is important to distinguish between the two. Using your example to expand on what I mentioned in my last comment, I think it would be more clear to augment the existing {% set string userName = (
user.isLoggedIn()
?
user.fullName
:
'Anonymous'
) %} If that variable could be passed to the template instead, this could be done: {% var string userName = 'Anonymous' %} |
At compile time, Twig checks whether the type is valid. For classes, that would simply be done using For runtime (i.e., in the compiled template's PHP code), a check is added to assert the type. For example: {% var int number %}
{% var \User user %}
{{ user.name }} has {{ number }} Would compile to something like the following, if (and only if) if (!is_int($context['int'])) {
trigger_error('Invalid type for variable "int"'); // or throw an Exception
}
if (! $context['user'] instanceof \User) {
trigger_error('Invalid type for variable "user"'); // idem dito
} Do you see any problem here so far? Or significant limitations which render this whole feature moot?
That's an interesting question. I'd hope to leverage PHPStan to do those chained checks. At the very least, though, we could check whether However, even if properties/methods aren't checked at all, there's still value. Consider this example: {% var \Foo\Bar foo %}
{% set baz = foo.bar.getBaz() %}
{% var string baz %} At compile time, we can ensure that the type is valid. The At runtime, we can trigger warnings/errors or throw Exceptions if The goal of |
Oh, do I understand correctly that you'd want the default value in In that case, I think this would get too complicated, both in the implementation, as well as for users to understand.
I guess I wouldn't be opposed to adding optional types to However, {% set string foo, int bar = 'foo', 1337 %} {# should work fine #}
{% set string baz %} {# pointless, as captured chunks will always be guaranteed to be string (right?) #}
Baz
{% endset %} Moreover, I guess that could all work. But you'd need typing support in It's an interesting idea for sure. My gut still says it's better separate those concerns. But that's just one opinion. |
@drjayvee, what about other compiler implementations like twig.js or Twing? What would be the logic that they need to implement? |
@ericmorand what do you currently do for |
@willrowe since there was no specification, we made an arbitrary call: https://twing.nightlycommit.com/specifics.html#constant But constant is an easy case to handle since the value is picked from the data passed to the context. Types are another story and need a proper specification to be supported by non-PHP implementation. |
@ericmorand do you have a specific question about how it should function when a template is rendered? You keep asking about the specification and how it should be implemented outside of PHP, but it doesn't seem like that is something to be decided here in this issue or even in this project. You may need to decide for yourself how you are going to implement it in your own project so it matches TwigPHP in a way that is consistent with how you have implemented all the other features. |
I honestly wasn't aware of these! It's interesting that there are already two. Not to be disrespectful, but I wouldn't want to limit the capability or complicate the spec to cater to implementations other than TwigPHP, especially ones in other languages. (Given the difficulties described here, this might already be the stance of the Twig maintainers.) To answer your question directly: the easiest path is to simply ignore Twing's documentation already reads:
|
I can tell you that this assumption is wrong.
|
That's the very problem that lead me to investigate static analysis for twig. I had renamed a variable (in a macro) and forgot to change one of the uses. It broke the whole page in production. This is no longer possible with our extension. More generally though, we could reconsider the utility for |
In PR #4235, the documentation for
@fabpot added the following in his review (merci beaucoup pour ca!)
I think this is a good idea, but I have some reservations. Let me also think out loud here. It's weird that Twig core specifies should/what must be supported, while explicitly leaving validation and support for more types to said extensions. Then again, it would be great if the basics are specified to give the tag more weight and possibly direction. I'd propose While I like Next, while I like What's more, specifying Lastly, I would also really like support for nullable by using the I predict we'll see lots of different opinions. Which isn't to say we shouldn't have it and stick to just agnosticism. Should we consider this a separate issue? I guess we should either try to merge the tag with a basic list of types, or decide to remain completely agnostic. |
Documenting a default subset would mean that extensions wanting to validate type would be interoperable on the meaning of this default subset, making things more useful. See for instance |
Regarding get_attribute optimization: I would hate to work with mixed just because someone has an " Do I get it right, that any variable is expected to be nullable? So there is no need to write The pagination DTO is just something to make it less abstract. Insert any case you have in your mind, where a template requires certain data to be used. |
I would like the ability to explicitly mark variables as accepting a null value or completely nullable. To give some examples, I currently document variables that are optional and are not required to be passed to the template context like so: {% types {
optional_variable: '?boolean',
} %} Usually, I will then set that variable to a default value if it has not been passed. The {% types {
optional_variable: 'string|null',
} %} In this case, if some text should be displayed in an |
Can we say that a variable is optional if there is a |
@fabpot that's a great question. I'm only thinking in terms of what is passed in the template context. |
If you think of the context as an array, in PHP you would type optional array keys as follow:
See https://phpstan.org/writing-php-code/phpdoc-types#array-shapes It would be great if we could write: {% types {
key: 'string',
optionalKey?: 'string'
} %} Also, if we want to describe what are valid values for types, I would say: any type you would write in PHPDoc. See https://phpstan.org/writing-php-code/phpdoc-types Let's keep it simple and see what analyzers do with it. We can standardize things depending on use cases in the wild. After things are proven to be useful. |
@ruudk that's an interesting approach. I'm not picky about the syntax, I would just like a way to denote it that is valid and doesn't conflict with anything else. |
I'm thinking too the base types of twig should be "documented" to encourage compatibility. For the rest, i kinda feel this could be solved by usage pretty quickly, as users will do what they need, tools too, and thus can be standardized in a future step. |
Optional variables
My idea with the I would really encourage everyone (who's interested in
I prefer ruud's proposal (i.e., (Edit: I just tried adding the implementation for (Extensions can trigger a warning if an optional variable is used without Do we all agree that we do want support for optional variables in Here's an example: {% types {
requiredInt: 'int',
optionalInt?: 'int',
requiredNullableInt: '?int',
optionalNullableInt?: '?int',
} %} Types So do you prefer matching PHP's concrete types (e.g., 'string, array`), or Fabian's more high-level ones (e.g., 'stringable, iterable')? Again, extensions can add types and validate types as they see fit. I'm okay with extension developers figuring out interoperability amongst each other. Most likely, extensions will settle on PHPStan/Psalm. |
I like the For the basic types, I would say that the concrete PHP types need to be part of the list, but this list might include other types. For instance, |
And that's perfectly valid according to PHPStan's documentation |
That's not a valid type in PHPDoc. In PHPDoc you write So it would become: {% types {
requiredInt: 'int',
optionalInt?: 'int',
requiredNullableInt: 'int|null',
optionalNullableInt?: 'int|null',
} %} I think to support the Twig also supports shorthands like |
To give us more time to experiment, maybe we could mark the new tag as being "experimental" like what we do on Symfony. That will let us change the details after we gain enough experience using it. |
Right, but it is valid PHP. 🤕 I actually kind of prefer
Nope. My current implementation doesn't use that method because it supports way more than Adding support for
Sure, I'm totally fine with that. |
That's great 👏 |
This also adds support for optional keys as discussed in twigphp#4165. I really don't like using a static method, but I couldn't figure out how else to call the method in tests. See the notes on the method itself. Any help is appreciated!
And it's merged! Thank you all for contributing. Do we still want to continue discussing the base types that should be supported? Do we continue in a new issue and close this one? Or do we wait and see what the community does with this tag? I still think it would be valuable to have the basics covered and part of the official spec, but I'd prefer continuing this discussion in a new issue. Lastly, I wanted to ask you, @Haehnchen, whether you're going to add support for |
Thank you all for your great discussion here. Let’s open another issue for the basic types. |
I'll close this issue. See you in #4256! |
See #4003 and #4009 for some context. In short, since the PHP language itself as well as its community are moving to (optional) stricter typing, it makes sense for Twig to support this as well by adding a
types
tag.@fabpot said he'd like to bootstrap a spec. This is the first draft. Everything is up for debate, obviously. In addition, I've put in italics parts of the spec that I have doubts about myself.Given the discussions below (thanks all for contributing), I think the spec is now crystallizing. Consider it a beta.
Syntax
The
types
tag contains a mapping of variable name to constant type string (no expression or interpolation):The reason for quoting the type is that several characters (e.g.,
\
) are currently not supported by the Lexer. It also allows analyzer implementations to freely specify the syntax.The variable name must not be quoted. This is to ensure that only valid names can be used.
Types
The syntax and content of the types are not part of this specification. This provides complete freedom to extensions that inspect and analyze the tags. It also keeps the implementation in Twig very simple and robust.
(In practice, it's likely that implementations will rely on widely-used specifications like PHPDoc / PHPStan types.)
Examples
Here are two examples:
Behavior
This specification does require any behavior other than validating the syntax for the tag itself. The tag does not lead to generated PHP code.
Usage
This section describes possible implementations to demonstrate the utility of this addition.
It is up to extensions developed by the community to implement any of this.
Development time
The types can be useful during development by enabling IDEs to auto-complete code, issue warnings, and more.
This is already implemented by the Symfony Support plugin for PHPStorm when using comments to declare variable types (e.g.,
{# @var id int #}
).These tags also provide precious documentation about the variables expected in a template's context.
Compile time
During compilation, a Twig extension can:
Run time
During run time, behavior may depend on
strict_variables
environment option:Possible extensions
twig_get_attribute()
to improve performance. (see Prevent calling twig_get_attribute by generating the guessed PHP code instead #3928)The text was updated successfully, but these errors were encountered: