hashtagerrors
hashtagerrors

Reputation: 239

How to set a variable name with dynamic variables?

I am trying to set variables with dynamic names. The code I am using is:

{% for i in 0..2 %}
    {% set foo~i    = 'array'.'~i~'.'getfoo' %}
    {% set bar~i    = 'array'.'~i~'.'getbar' %}
{% endfor %}

The variables I want are:
foo0
bar0
foo1
bar1
foo2
bar2

But I get this error Unexpected token "operator" of value "~" ("end of statement block" expected)

Also I don't want these variables as array.

Upvotes: 2

Views: 602

Answers (1)

Matias Kinnunen
Matias Kinnunen

Reputation: 8540

Like @DarkBee mentioned, you can't do this in vanilla Twig. But you can create quite a simple extension – notice that $context needs to be passed by reference:

class MyTwigExtension extends Twig_Extension {
    public function getFunctions() {
        return [
            new Twig_Function('set', [$this, 'set'], ['needs_context' => true]),
        ];
    }

    public function set(&$context, $name, $value) {
        $context[$name] = $value;
    }
}
$twig->addExtension(new MyTwigExtension());

Then in Twig you can do:

{{ dump() }}
{% do set('foo' ~ 1, 'bar') %}
{{ dump() }}

The above will print:

array(0) {
}

array(1) {
  ["foo1"]=>
  string(3) "bar"
}

But note that a for loop has its own context. So if you do this:

{% set foo = 'bar' %}

Before loop:
{{ dump() }}

{% for i in 0..2 %}
    {%- do set('foo' ~ i, 'iteration ' ~ i) %}
    {%- if loop.last %}
        {{- 'Inside loop (last iteration):\n' }}
        {{- loop.last ? dump() }}
    {% endif %}
{% endfor %}

After loop:
{{ dump() }}

You get this – notice the _parent array which represents the "parent" context outside of the loop:

Before loop:
array(1) {
  ["foo"]=>
  string(3) "bar"
}


Inside loop (last iteration):
array(9) {
  ["foo"]=>
  string(3) "bar"
  ["_parent"]=>
  array(1) {
    ["foo"]=>
    string(3) "bar"
  }
  ["_seq"]=>
  array(3) {
    [0]=>
    int(0)
    [1]=>
    int(1)
    [2]=>
    int(2)
  }
  ["loop"]=>
  array(8) {
    ["parent"]=>
    array(1) {
      ["foo"]=>
      string(3) "bar"
    }
    ["index0"]=>
    int(2)
    ["index"]=>
    int(3)
    ["first"]=>
    bool(false)
    ["revindex0"]=>
    int(0)
    ["revindex"]=>
    int(1)
    ["length"]=>
    int(3)
    ["last"]=>
    bool(true)
  }
  ["i"]=>
  int(2)
  ["_key"]=>
  int(2)
  ["foo0"]=>
  string(11) "iteration 0"
  ["foo1"]=>
  string(11) "iteration 1"
  ["foo2"]=>
  string(11) "iteration 2"
}


After loop:
array(1) {
  ["foo"]=>
  string(3) "bar"
}

You can overcome this limitation in three ways. First is to initialize the variables before the for loop (notice that foo0 is left as null because the loop starts at 1, and that foo3 won't be in the global context because it hasn't been initialized):

{% set foo0 = null %}
{% set foo1 = null %}
{% set foo2 = null %}

{% for i in 1..3 %}
    {% do set('foo' ~ i, 'iteration ' ~ i) %}
{% endfor %}

{{ dump() }}

The above will print:

array(3) {
  ["foo0"]=>
  NULL
  ["foo1"]=>
  string(11) "iteration 1"
  ["foo2"]=>
  string(11) "iteration 2"
}

The second way is to modify the extension's set method to check whether $context contains a key _parent:

public function set(&$context, $name, $value) {
    $context[$name] = $value;

    if (array_key_exists('_parent', $context)) {
        $this->set($context['_parent'], $name, $value);
    }
}

Then even nested for loops aren't a problem:

{% for i in 1..2 %}
    {% for j in 3..4 %}
        {% do set('foo' ~ i ~ j, i ~ ' and ' ~ j) %}
    {% endfor %}
{% endfor %}

{{ dump() }}

The above will print:

array(4) {
  ["foo13"]=>
  string(7) "1 and 3"
  ["foo14"]=>
  string(7) "1 and 4"
  ["foo23"]=>
  string(7) "2 and 3"
  ["foo24"]=>
  string(7) "2 and 4"
}

The third way is to keep the extension's set method intact and create a new method, e.g. set_global:

class MyTwigExtension extends Twig_Extension {
    public function getFunctions() {
        return [
            new Twig_Function('set',        [$this, 'set'],        ['needs_context' => true]),
            new Twig_Function('set_global', [$this, 'set_global'], ['needs_context' => true]),
        ];
    }

    public function set(&$context, $name, $value) {
        $context[$name] = $value;
    }

    public function set_global(&$context, $name, $value) {
        $context[$name] = $value;

        if (array_key_exists('_parent', $context)) {
            return $this->set_global($context['_parent'], $name, $value);
        }
    }
}
$twig->addExtension(new MyTwigExtension());

Then you can use set to set variables in the current context (e.g. in the context of a for loop) or set_global to set "global" variables (in the context of the file). You can use both methods inside for loops to set new values to already initialized variables.

Upvotes: 1

Related Questions