Missing comma before tag arguments - why Latte wants a comma
- David Grudl
- founder | 8328
If you upgraded to nette/application 3.3 and got the message
Missing comma before tag arguments, this post is for you. If you
just want to fix your templates quickly, you can stop reading here: a script adds
the missing commas for you , and the instructions are at the end of this
post. For everyone else, I'll explain what changed, why, and where it's all
heading.
What happened
The {control} tag now reports a deprecation when the comma
between the component name and its arguments is missing:
{control poll 1, 'foo'} {* deprecated *}
{control poll, 1, 'foo'} {* correct *}
The template compiles exactly as before, Latte just warns you about an outdated way of writing it. Why outdated? Because without the comma, Latte has to guess where the component name ends and the arguments begin. Usually it guesses right. But when it gets it wrong, it often doesn't say a word:
{control cart:small -1} {* method 'render' . 'small' - 1, so a TypeError instead of renderSmall(-1) *}
{control poll:type [1]} {* method named by the constant type[1] instead of renderType([1]) *}
{control $comp ($x)} {* calls $comp($x) and renders the component without arguments *}
Links are even more treacherous:
{link change! -1} {* works: signal change! with the argument -1 *}
{link change! +1} {* link to the action change with the argument false *}
In the second case the link loses its exclamation mark, so it no longer
points to a signal, and !+1 becomes the argument
false. No error, no warning, the link is generated as usual and
simply leads somewhere else. And why does minus work while plus doesn't? Nobody
can be expected to remember that.
{include} fares the same way:
{include $file (id: 1)} {* calls $file(id: 1) as a function *}
With the comma, all of these cases work exactly as you'd expect:
{control cart:small, -1}
{control poll:type, [1]}
{control $comp, ($x)}
{link change!, +1}
{include $file, id: 1}
Why not fix it in the parser
You might ask why Latte doesn't simply see that the name ends at the space.
The thing is, expressions in Latte behave just like in PHP: whitespace inside an
expression means nothing. $a -1 is the same as $a - 1,
and $f ($x) is a function call just like $f($x). The
method after the colon and a component name given as a variable are full
expressions, so they pull in whatever follows them, and a space won't stop them.
Telling the cases apart by whitespace would mean that $f ($x) means
something different in Latte than in PHP, which would be even more
confusing.
For a simple name like poll or change!, Latte does
try: after the name, it looks for characters an argument can start with. I kept
extending that list of characters: plus, the at sign, a decimal number starting
with a dot, the spread operator. Each time the tests passed, and each time
another case turned up right away. The list will never be complete, and every
addition just moves the place where things quietly break next time.
The only spelling that is always unambiguous is the comma.
And the comma isn't just the price of being unambiguous. It also makes templates easier to read, because you can see at a glance where the name ends and the arguments begin:
{input price size: 5} {* "price size" reads like a compound noun *}
{input price, size: 5}
<a n:href="this page: $page + 1"> {* "this page" reads as a single phrase *}
<a n:href="this, page: $page + 1">
{control poll $active ? 1 : 2} {* does $active still belong to the name? *}
{control poll, $active ? 1 : 2}
In a PHP function call, the parentheses do this job; in Latte, it's the comma.
Where it's heading
Latte introduces the comma gradually, tag by tag:
- It's required by new syntax that doesn't have to care
about existing templates. For example
{layout 'base.latte', title: 'Home'}, which passes variables to the layout (Latte 3.1.5); without the comma it fails to compile. - A deprecation is reported by
{form},{input}and{label}since nette/forms 3.3, and by{control}since nette/application 3.3. - It's still optional in
{link},{plink},n:href,{include},{import}and{embed}. As the examples above show, it's worth writing it there too.
But you don't have to wait for further deprecations. The script described below adds the comma to all of these tags at once, even where it's still optional. One run and your templates are sorted.
What to do about it
The quickest way is to let the script add the commas. Download it into the root of your project and do a dry run first:
curl -O https://gist.githubusercontent.com/dg/ec6ee5335db26443660eb9dd367b1bf6/raw/latte-comma.php
php latte-comma.php --dry-run app
It lists every tag it would change. If the result looks right, run it without
--dry-run and review the changes with git diff. It
fixes {control}, {form}, {input},
{label}, {link}, {plink},
n:href, {include}, {import},
{embed}, {define} and {sandbox}.
It inserts a comma only where the template compiles to exactly the same code
as before, so it can't change what your template does. Tags where a comma would
change the meaning are only listed for a manual check; they're most likely
broken already, like {link change! +1} above. If your templates use
your own tags, pass your Latte engine to the script with the
--bootstrap option; see the header of the script for details.
If you'd rather fix things by hand, that's easy too: the deprecation tells
you the exact spot, for example
Missing comma before tag arguments on line 12 at column 15. Either
way, the best habit is to write the comma everywhere, even where Latte doesn't
require it yet. The template is then unambiguous and none of the traps above can
catch it.