Can twig macros return values?
Asked Answered
I

4

6

I'm trying to write a template in Twig. Inside it I would like it to perform some manipulations on the string data that comes from the controller. In particular, there's a common manipulation (convert from underscore_case to CamelCase) that I'd like to bring out into a separate function. Then later I can use it repeatedly like {% set x = magic(a) ~ magic(b) %}. However I cannot find how to make such a reusable function inside the template itself. There are macros, but those don't seem to be able to return values. Filters are another option that seem to fit the bill, but I can only define those on PHP side.

Can this be done? Or should I do all the advanced string manipulation controller-side? It kinda feels like I'm pulling parts of display logic in there; things that should be in the view.

Internationalist answered 12/9, 2018 at 13:13 Comment(0)
M
6

Twig is for outputting data. If you need to "transform" the data you need to do that before you send it to twig or you need to extend twig

Ideally, all the data you send to twig is just variables and arrays that needs the least amount of manipulation on their own.

When you're actually "in" twig, the data processing can be assumed to be "done" and only needs to be outputted in the appropriate places with minimal logic to decide user interface styles.

So revisit your logic and prepare your data better before sending it to twig.

An example for extending a toolkit class that contains our magic methods to do real wizardry.

class CustomToolkit 
{
    public function magic_a($a) 
    {
        return strtolower($a);    }

    public function magic_b($b) 
    {
        return camel_case($b);
    }

    public function magic_tidle($a, $b) 
    {
        return $this->magic_a($a) ~ $this->magic_b($b);
    }
}

Then you add this to your twig instance. I added here a complete instantiation loop. if you have a service provider you can just grab the instance from there and add it to that one.

$twig = new Twig_Environment(new Twig_Loader_Array([
                                                      'html' => $contents
                                                   ]),[
                                                      'auto_reload' => true,
                                                      'debug' => false,
                                                   ]);
$twig->addExtension('toolkit', new CustomToolkit ());
echo $twig->render('html', $values);

Then in your twig code you should be able to do something along the lines of

{% set x = toolkit.magic_tidle("value","value_b") %} 
Mediocre answered 12/9, 2018 at 13:29 Comment(7)
Well, I always considered such transformations part of the output process. For example, HTML-encoding is one transform that is usually applied at output. Truth be told, what I need is in essence a Twig transform - it's just that it's only needed in one view, so I'd like to keep it local.Internationalist
just extend the twig loader for that instance then with a custom class that provides you with the tools you need so you can grab hold of those.Mediocre
How do I do that? I'll search the documentation now, but if you already know where it is documented, a link would be appreciated.Internationalist
@Internationalist I added an example. Have you looked at the link I had in my answer too? twig.symfony.com/doc/2.x/advanced.htmlMediocre
OK, figured it out. :) In my case, I used the Twig_Filter class mentioned in your link, and adding it to the view was done with $this->container->get('twig')->addFilter($filter); - the part about getting the Twig environment I copy-pasted from the ControllerTrait::render method. Sorry, I'm new to Symfony/Twig. :) Hope that's the normal way of doing things.Internationalist
@Internationalist I work with octobercms and twig, that's how I learned it, which is based upon laravel, which is based upon symfony. But it's always fun to learn new things :-) In october you'd put it in your boot method and then $twig = App::make("twig.environment"); $twig->addFilter($filter); Different methods, different service providers, but all boils down to the same in the end.Mediocre
True, although I do like the | character inside twig when using Twig_SimpleFilter, as it clearly indicates the data will be transformedSubaxillary
F
6

You are right, macros do not have return values and you cannot really make them have any. All they do is outputting strings.

Still, you are able to capture string output using set: https://twig.symfony.com/doc/2.x/tags/set.html
The syntax looks similar to this:

{% set var -%}
    {{- call.macro() -}}
{%- endset %}

The output of the macro call is then stored inside var. You may want to strip the whitespace though.

But then, consider rethinking what you are doing. Is this still presentation logic, or is your controller simply "too lazy" to transform the strings prior to passing them to twig? If it's really presentation logic, simply adding a twig filter by extending twig surely is worth the hassle. Not only because your code becomes testable.

Flown answered 12/9, 2018 at 13:31 Comment(1)
See my comment on Tschallacka's answer. I considered output transforms part of the output process, similar to HTML-encoding. I can, of course, move them to the controller. It just felt wrong.Internationalist
B
1

Can this be done?

Yes! However, Twig templates are not the ideal places to run logic. At least, we should do our best to avoid it. Instead, Controller should return what Twig template needs. Logic should run in Service, Utility, Helper (you name it) etc. and Controller returns it to Twig. Twig then just displays it.

Can twig macros return values?

Yes! Look at this example. It accepts parameters and returns (not a real "return" thing but you get the idea) something back.

Example:

Assuming that the data you are trying to manipulate is something simple.

use Doctrine\Common\Inflector\Inflector;

Controller
{
   action() {
      $data = Inflector::camelize('hello_world'); // This will become helloWorld

      return ....;
   }
}

Look into Inflector class. It has useful stuff.

Bornite answered 12/9, 2018 at 13:27 Comment(7)
No, in that example the macro just outputs its stuff. I cannot stick it in another variable instead.Internationalist
Sure you can e.g. {% set myvar %}{{ mymacro.foo() }}{% endset %}Subaxillary
Yes, that's correct. I was updating the answer while you were writing the message to add not a real "return" thing but you get the idea :)Bornite
OK, I guess that could work, though it loses a lot of the elegance. Adding it to the controller instead doesn't look so bad compared to this.Internationalist
Remember, Controller should not run any logic either. Logic etc don't belong to Controllers. Controllers should just return ..... You can add your manipulation logic somewhere (a class) and call it in Controller then return it to Twig.Bornite
@Internationalist Check example I added please.Bornite
Imho I would just chain the Inflector class in my twig extension with that example thoughSubaxillary
C
0

It's actually doable with Twig without having to write any PHP extensions, like this:

{% macro calculateSales(orders) %}
    {% set result = 0.0 %}
    {% for order in orders %}
        {% set result = result + order['price'] * order['quantity'] %}
    {% endfor %}
    {{ result }}
{% endmacro %}

{% set total = _self.calculateSales(orders) | spaceless %}

{{ total * 0.08 }}

More here in the article Making Twig Macros Work Like Functions

Concinnate answered 18/7, 2023 at 3:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.