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

Modifier called like a function compiled to modifier name, not function callback #1100

Closed
rudiedirkx opened this issue Jan 29, 2025 · 17 comments

Comments

@rudiedirkx
Copy link

rudiedirkx commented Jan 29, 2025

Smarty 4.5.5

Is this a bug or a very strange feature? If I define a modifier:

$this->smarty->registerPlugin('modifier', 'kprint_r_out', trim(...));

and call it like:

{kprint_r_out($myVar)}

it will compile into

<?php echo kprint_r_out($_smarty_tpl->tpl_vars['myVar']->value);?>

even though kprint_r_out is only the name of the function, not the function itself (but kprint_r_out() does exist, otherwise it won't compile). The function (trim in this case) isn't used at all...

I like calling functions like function, but this is too weird. If it's intentional, it shouldn't be defined as a modifier with mandatory callback (that's not used).

Or am I abusing another Smarty functionality somehow?

@rudiedirkx
Copy link
Author

I also do like some functions being compiled into native functions (instead of going through the plugin system), but if I explicitly define a different callback, it should probably use that, not the function name.

@wisskid
Copy link
Member

wisskid commented Jan 29, 2025

Could you provide a full code example? From your report I have several questions.

  1. trim(...) would return a string, which would be the callback registered by smarty
  2. You seem to call dump instead of kprint_r_out in your template. I don't see dump being registered.

@wisskid wisskid added the waiting Waiting for answer label Jan 29, 2025
@rudiedirkx
Copy link
Author

dump was a typo. Sorry. There's no dump, only kprint_r_out and trim.

The register code is literally

$this->smarty->registerPlugin('modifier', 'kprint_r_out', trim(...));

with the ... to make it a callback. The result of trim(...) isn't a string, but a Closure. Apparently it's called 'first class callable': https://www.php.net/manual/en/functions.first_class_callable_syntax.php

But the callback doesn't even matter, that's my point. This register would do the same:

$this->smarty->registerPlugin('modifier', 'kprint_r_out', 'trim');

or this:

$this->smarty->registerPlugin('modifier', 'kprint_r_out', function() {
    // Smarty never gets here
    exit;
});

as long as kprint_r_out exists. Which is weird IMO, because kprint_r_out is just the name, right? The callability is checked during the registration (registerPlugin), but it's not used during compile (I don't know where that happens). It's compiled into the name of the function instead of the callback.

Maybe I just misunderstand Smarty syntax, but it's still a bug IMO.

I know functions:

{trans key='foo.bar'}

and modifiers:

{$title|html}

and apparently modifiers can be called like real functions!?:

{html($title)}

which I love, because it's much better syntax and supports variadic functions. And they compile into native functions, which is great for performance. Buut that should only happen if the callback is the same as the function name, like with

$this->smarty->registerPlugin('modifier', 'can', can(...));

(used like {if can('add-group-member', $group, $user)} etc)

not if they're different, like with my orig example:

$this->smarty->registerPlugin('modifier', 'kprint_r_out', trim(...));

That could be compiled into

<?php echo trim($_smarty_tpl->tpl_vars['myVar']->value);?>

because trim is the callback, but not into

<?php echo kprint_r_out($_smarty_tpl->tpl_vars['myVar']->value);?>

because kprint_r_out is just the name.

@rudiedirkx
Copy link
Author

All kinds of callbacks, and how to reflect their source: https://3v4l.org/D8hib

I should even be able to make a Smarty modifier like:

$manager = getAccessManager();
$this->smarty->registerPlugin('modifier', 'can', $manager->hasAccess(...));

And then call {if can($arg)} without object context, which calls $manager->hasAccess($arg). Valid PHP, and Smarty doesn't complain when registering it, only it's not compiled correctly.

@rudiedirkx
Copy link
Author

rudiedirkx commented Jan 29, 2025

only it's not compiled correctly

It might be compiled correctly for use as actual modifier though: {$myVar|can}. That might have a different compiler. That's why I ask. It seems odd that you can call a modifier like a real function: {can($myVar)}. Is that intentional?

@rudiedirkx
Copy link
Author

Super simple buggy way to use Reflection to compile those callbacks into code: https://3v4l.org/m0tP7 Sometimes it's possible to compile into native callable, but sometimes it has to go through the plugin systems.

@wisskid
Copy link
Member

wisskid commented Jan 29, 2025

Aaah got it now! I'll look into it.

@wisskid
Copy link
Member

wisskid commented Jan 29, 2025

I wrongfully assumed the three dots meant you left some irrelevant code out... 😣

@wisskid
Copy link
Member

wisskid commented Jan 29, 2025

@rudiedirkx In v4.5.5 I'm getting a Smarty Compiler: Syntax error in template "string:{kprint_r_out($myVar)}" on line 1 "{kprint_r_out($myVar)}" unknown function 'kprint_r_out'. I'm not seeing what you are (strange compilation) since I'm not getting this compiled.

In v5, your code runs fine BTW.

@rudiedirkx
Copy link
Author

I wrongfully assumed the three dots meant you left some irrelevant code out... 😣

It is very ambiguous syntax, I agree. That's how I used to write pseudo code too, before this was actual syntax.

@rudiedirkx
Copy link
Author

rudiedirkx commented Jan 30, 2025

@rudiedirkx In v4.5.5 I'm getting a Smarty Compiler: Syntax error in template "string:{kprint_r_out($myVar)}" on line 1 "{kprint_r_out($myVar)}" unknown function 'kprint_r_out'. I'm not seeing what you are (strange compilation) since I'm not getting this compiled.

Because that function doesn't exist in your test? Register a function (or modifier?) with name X and callback Y, compile a template with {$var = X($var)} or something, and see what it compiled into. I think it compiles into X($var) instead of Y($var). That's what I see in my almost vanilla setup.

In v5, your code runs fine BTW.

It runs fine in my setup too, but only because the function name and the actual function are the same. But I can't make a Smarty function foo that executes function bar when I call it like a function (and not a modifier).

@rudiedirkx
Copy link
Author

It's even weirder... Or I really don't know how Smarty works. If I register a modifier trim:

$smarty->registerPlugin('modifier', 'trim', 'trim');

I can call it like a function:

{trim($var)}

but if I call it something else:

$smarty->registerPlugin('modifier', 'my_trim', 'trim');

and I call it the same:

{my_trim($var)}

I get a compile error

unknown function 'my_trim'

Smarty does something very odd with real function names instead of the registered name and callback.

I'm making a demo repo.


And how is this weird fatal error possible!?

Fatal error: Uncaught --> Smarty Compiler: Syntax error in template "file:/var/www/tests/smarty-demo/views/trim1.tpl" on line 5 "'{trim1($myVar)}'" unknown function 'trim1' <-- thrown in /var/www/tests/smarty-demo/views/trim1.tpl on line 5

It's a SmartyCompilerException, but PHP doesn't say so, and what are those --> and <--? I've never seen that. Is that PHP 8.4 or some Smarty magic?

@rudiedirkx
Copy link
Author

Demo project: https://github.com/rudiedirkx/smarty-demo trim3.tpl output is very interesting.

I definitely don't know how Smarty works. Smarty function vs modifier vs functionlike? Smarty finds it acceptable to call a PHP function, but only if you register it as a modifier, but it doesn't actually call the modifier?? I don't get it.

@rudiedirkx
Copy link
Author

Please explain the difference (and how Smarty handles those) between

  • {myfunc foo=bar}
  • {$var|mymod}
  • {func_or_mod('bla')}

I've always thought the first two are Smarty syntax that's been around forever (smarty function and smarty modifier), and the third is 'newly' acceptable syntax to call a modifier. The first two work as expected, but the third behaves very strangely. What is that? When is Smarty okay with that? What does it run?

@rudiedirkx
Copy link
Author

Smarty 5 works as expected, and it compiles as expected too. All the calls go through the plugin system:

echo $_smarty_tpl->getSmarty()->getModifierCallback('trim3')($_smarty_tpl->getValue('myVar'));

And modifier trim3 ({$var|trim3}) is compiled into exactly the same as 'function' trim3 ({trim3($var)}) 👍

This is definitely a bug in Smarty 4. Even if it's a feature, the feature has a bug 😆 I guess I should upgrade to Smarty 5.

@wisskid
Copy link
Member

wisskid commented Jan 30, 2025

The thing is, in Smarty up until and including v4, you can call any callable PHP function directly from a template as well. I have removed this in Smarty v5, requiring developers to register any function they want to use. Since there is now now difference between the {blah($a)} syntax and the {$a|blah} syntax, I've merged the the compilation of these two together in v5. They are handled very differently in v4.

The bug (or the buggy feature) in v4 now seems to be that if you pass a closure as a modifier to registerPlugin, Smarty will sometimes use the callable function from the global scope with the exact name of the modifier instead. I think passing closures has never been tested before.

@wisskid
Copy link
Member

wisskid commented Feb 3, 2025

@rudiedirkx can you take a look at #1101 for me?

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