69

Is it possible to retrieve all variables inside a Twig template with PHP?

Example someTemplate.twig.php:

Hello {{ name }}, 
your new email is {{ email }}

Now I want to do something like this:

$template = $twig->loadTemplate('someTemplate');
$variables = $template->getVariables();

$variables should now contain "name" and "email".

The reason I want to do this is that I am working on a CMS system where my twig templates and variables are dynamically set by my users and they also fill the variables through an API.

I want to set default values to not-set variables and therefore I need a list of all variables that exist inside the template…

j0k
  • 22,600
  • 28
  • 79
  • 90
Stefan Neubig
  • 958
  • 1
  • 7
  • 8
  • This is possible, see the answer further down: https://stackoverflow.com/a/45564150/635522 – Bananaapple Nov 07 '17 at 10:21
  • 1
    I think somebody should explore another approach.. Proxying the variables passed to the template, and every time they are accessed (with the __get magic) log it in the proxy and eventually if the returned variable is an object then recurse the proxy. It wouldn't get all possible variables but at least the ones being accessed in a given context – Tofandel Nov 24 '20 at 10:32

16 Answers16

93

This is useful I find to get all the top-level keys available in the current context:

<ol>
    {% for key, value in _context  %}
      <li>{{ key }}</li>
    {% endfor %}
</ol>

Thanks to https://www.drupal.org/node/1906780

duncan
  • 31,401
  • 13
  • 78
  • 99
  • Oh nice, I didn't see that. This is not exclusive to Drupal however. It is a global feature of Twig. – Zephyr Dec 22 '15 at 11:11
  • 2
    Also allows for doing things like `{% set variable = 'foo' %} {{ variable is not empty ? attribute(_context, variable) : '' }}` – Will B. Mar 29 '16 at 13:39
  • 11
    Or directly use `{{ dump(_context|keys) }}` – VDarricau Dec 07 '16 at 16:42
  • 1
    For anyone like me who was going out of his mind because `{{ dump(var) }}` kept breaking their site due to memory limits, this suggestion is incredibly helpful. I was just trying to find the proper parameter within a variable, but my server couldn't handle `dump`. But by simply listing keys, I found what I needed in a literal minute. Thanks so much! – Tom Bisciglia Jan 15 '17 at 11:12
  • 1
    @albanx I don't think this actually answers the question. It's useful to know, but OP is after everything that a template "needs", not everything a template has. – Waddles Mar 24 '17 at 00:52
  • 2
    Very useful! Thanks for posting this! – Jason Glisson Jan 19 '18 at 19:45
66

UPDATE 2019

Although {{ dump() }} does work, in some circumstances it may result in a "memory exhausted" error from PHP if it generates too much information (for example, due to recursion). In this case, try {{ dump(_context|keys) }} to get a list of the defined variables by name without dumping their contents.

UPDATE 2017

It is possible by using {{ dump() }} filter. Thanks for pointing that out in the comments!


OUTDATED

It is not possible.

You can look for these variable in twig templates and add |default('your_value') filter to them. It will check if variable is defined and is not empty, and if no - will replace it with your value.

Will Martin
  • 4,142
  • 1
  • 27
  • 38
Vitalii Zurian
  • 17,858
  • 4
  • 64
  • 81
  • Do you mean by parsing the template myself and attach the string `|default('your_value')` to every variable? Or does Twig provide a function to setup the default filter automatically to every variable? – Stefan Neubig Oct 09 '12 at 11:39
  • Unfortunately, you have to parse templates manually. Or you could redefine Twig's default parsing behavior - that would be possible in Symfony2 – Vitalii Zurian Oct 09 '12 at 11:41
  • @StefanNeubig Thanks for accepting. So, how did you solve it? By parsing templates or by redefining twig methods? – Vitalii Zurian Oct 10 '12 at 07:33
  • 1
    I currently parse the template variables with the following regex: `preg_match_all("/{{ *\\w+(\\|\\w+\\(\\'\\w+\\'\\))? *}}/", $template, $matches);` – Stefan Neubig Oct 14 '12 at 12:08
  • 3
    It *is* possible, as described in duncan's answer. – TheThirdMan Dec 14 '16 at 13:58
  • It is possible by simply doing `{{ dump() }}` as described here https://stackoverflow.com/a/45564150/635522 – Bananaapple Nov 07 '17 at 10:21
  • I created a class called Twig2Schema to infer variables from a twig template in PHP through AST walking. https://gist.github.com/cwilby/2847624d6533585311e11cdb463e06f5 – Cameron Wilby Aug 23 '19 at 00:25
  • This technique will only dump keys for non-computed values. For example, you might have an ID at `node.field_main_image.target_id`. You can access the entity at `node.field_main_image.entity`, but the `entity` key will not appear in lists of keys because it is a computed value. – Liam Morland Jun 02 '22 at 19:37
23

The way I do it is

<script>console.log({{ _context | json_encode | raw }});</script>

And then I just check my console using DevTools

Luke Madhanga
  • 6,871
  • 2
  • 43
  • 47
  • 1
    This works when you otherwise would need to iterate over object properties, great solution! – zwolin Apr 15 '20 at 08:38
  • 1
    By far, The very best solution is this one, i've been looking for this line of code for hours! For those struggling with ´JSON.parse()´ as I was, this is the best option. Besides your IDE will raise an error, Because javascript does not parses the Twig notation, you can add it on any HTML file, and run. You will have a great visibility of you layer of data inside the console on Dev Tools. Also, there is no need to encapsulate `{{...}}` on cotes,it will give you also an error (` SyntaxError: missing ) after argument list`). Thanks @madhan.ga – Diego Mello May 27 '21 at 14:43
16

Answer added at 2015

In the past it wasn't possible. But since version 1.5 dump() function has added. So you can get all variables from current context calling dump() without any parameters:

<pre>
    {{ dump(user) }}
</pre>

However, you must add the Twig_Extension_Debug extension explicitly when creating your Twig environment because dump() isn't available by default:

$twig = new Twig_Environment($loader, array(
    'debug' => true,
    // ...
));
$twig->addExtension(new Twig_Extension_Debug());

If you have been using something like Symfony, Silex, etc, dump() is available by default.

EDIT:

One can also reference all variables passed to a template (outside the context of dump()), using the global variable _context. This is what you were looking for. It is an array associating all variable names to their values.

You can find some additional info in the Twig documentation.

For this specific question however, it would probably be best to gather all of these custom variables you are speaking of, under a same umbrella variable, so that retrieving them would not be a headache. I would be an array called custom_variables or whatever.

felipsmartins
  • 13,269
  • 4
  • 48
  • 56
14

Here is the best way and easiest way to dump all variables :

{{ dump () }}

Source : https://www.drupal.org/docs/8/theming/twig/discovering-and-inspecting-variables-in-twig-templates

ReaperSoon
  • 765
  • 7
  • 23
  • Thanks for the link! Now: https://www.drupal.org/docs/theming-drupal/twig-in-drupal/discovering-and-inspecting-variables-in-twig-templates – Jono Aug 31 '22 at 14:05
10

If you need all Twig elements inside of a text, just use:

preg_match_all('/\{\%\s*(.*)\s*\%\}|\{\{(?!%)\s*((?:[^\s])*)\s*(?<!%)\}\}/i', $text, $matches);

I had a issue where the WSIWYG editor placed HTML tags inside of Twig variables. I filter them with:

public function cleanHTML($text)
{
    preg_match_all('/\{\%\s*(.*)\s*\%\}|\{\{(?!%)\s*((?:[^\s])*)\s*(?<!%)\}\}/i', $text, $matches);

    if (isset($matches[0]) && count($matches[0])) {
        foreach ($matches[0] as $match) {
            $clean_match = strip_tags($match);

            $text = str_replace($match, $clean_match, $text);
        }
    }

    return $text;
}

UPDATE

Use this expression to find all {{ }} and {% %}

preg_match_all('/\{\%\s*([^\%\}]*)\s*\%\}|\{\{\s*([^\}\}]*)\s*\}\}/i', $text, $matches);
19Gerhard85
  • 183
  • 1
  • 12
5

I think 19Gerhard85's answer is pretty good, although it might need some tweaking because it matched some empty strings for me. I like using existing functions where possible and this is an approach mostly using twig's functions. You need access to your application's twig environment.

/**
 * @param $twigTemplateName
 * @return array
 */
public function getRequiredKeys($twigTemplateName)
{
    $twig = $this->twig;
    $source = $twig->getLoader()->getSource($twigTemplateName);
    $tokens = $twig->tokenize($source);
    $parsed = $twig->getParser()->parse($tokens);
    $collected = [];
    $this->collectNodes($parsed, $collected);

    return array_keys($collected);
}

And the only custom part of it is the recursive function to collect only certain types of nodes:

/**
 * @param \Twig_Node[] $nodes
 * @param array $collected
 */
private function collectNodes($nodes, array &$collected)
{
    foreach ($nodes as $node) {
        $childNodes = $node->getIterator()->getArrayCopy();
        if (!empty($childNodes)) {
            $this->collectNodes($childNodes, $collected); // recursion
        } elseif ($node instanceof \Twig_Node_Expression_Name) {
            $name = $node->getAttribute('name');
            $collected[$name] = $node; // ensure unique values
        }
    }
}
mickadoo
  • 3,337
  • 1
  • 25
  • 38
  • I wouldn't say it's comprehensive. I spoke to the some of the contributors on Twig about it and they told me my answer doesn't cover all cases. It worked well enough for me, but just keep that in mind. – mickadoo Mar 24 '17 at 19:38
5

After using duncan's answer for quite some time I finally found the "right" way to dump all twig variables of a template:

{% dump %}

That's it. All the variables available in the template will be dumped and in the dump section of the profiler, not in the middle of your html like with {{ dump() }}.

if you put the contents of dump() into a variable:

{% set d = dump() %}

you 'll get all the variables but in "dump ready" html so it would be a pain to parse it.

Hope that helps.

cezar
  • 11,616
  • 6
  • 48
  • 84
Mawcel
  • 1,967
  • 15
  • 22
4
$loader1 = new Twig_Loader_Array([
    'blub.html' => '{{ twig.template.code }}',
]);
$twig = new Twig_Environment($loader1);
$tokens = $twig->tokenize($loader1->getSource('blub.html'));
$nodes = $twig->getParser()->parse($tokens);

var_dump($this->getTwigVariableNames($nodes));


function getTwigVariableNames($nodes): array
{
    $variables = [];
    foreach ($nodes as $node) {
        if ($node instanceof \Twig_Node_Expression_Name) {
            $name = $node->getAttribute('name');
            $variables[$name] = $name;
        } elseif ($node instanceof \Twig_Node_Expression_Constant && $nodes instanceof \Twig_Node_Expression_GetAttr) {
            $value = $node->getAttribute('value');
            if (!empty($value) && is_string($value)) {
                $variables[$value] = $value;
            }
        } elseif ($node instanceof \Twig_Node_Expression_GetAttr) {
            $path = implode('.', $this->getTwigVariableNames($node));
            if (!empty($path)) {
                $variables[$path] = $path;
            }
        } elseif ($node instanceof \Twig_Node) {
            $variables += $this->getTwigVariableNames($node);
        }
    }
    return $variables;
}

have fun :-)

Lars
  • 49
  • 1
2

You have to parse the template, and walk through the AST it returns:

$loaded = $twig->getLoader()->getSource($template);
var_dump(extractVars($twig->parse($twig->tokenize($loaded))));

function extractVars($node)
{
    if (!$node instanceof Traversable) return array();

    $vars = array();
    foreach ($node as $cur)
    {
        if (get_class($cur) != 'Twig_Node_Expression_Name')
        {
            $vars = array_merge($vars, call_user_func(__FUNCTION__, $cur));
        }
        else if ($cur->getAttribute('always_defined') == false)
        {
            // List only predefined variables expected by template, 
            // filtering out `v` and leaving `arr` from `{% for v in arr%}`
            $vars[] = $cur->getAttribute('name');
        }
    }

    return $vars;
}
terales
  • 3,116
  • 23
  • 33
devicenull
  • 484
  • 1
  • 4
  • 14
2

I built a Twig2Schema class to infer variables from a Twig AST. To get the variables in a document, you need to recursively "walk" through the Twig AST and have rules in place when you encounter certain types of language nodes.

This class extracts variable names from Nodes if they are not always defined, and also grabs variables from the value used in ForLoopNodes and IfStatements.

To use it, you can either call infer for the whole template, or a subset of the tree using inferFromAst.

<?php

class Twig2Schema
{
    /**
     * @param \Twig\Environment $twig - A twig environment containing loaded templates
     * @param $twigTemplateName - The name of the template to infer variables from
     * @param $config - A configuration object for this function
     * @return array
     */
    public function infer(\Twig\Environment $twig, $twigTemplateName)
    {
        $source = $twig->getLoader()->getSourceContext($twigTemplateName);
        $tokens = $twig->tokenize($source);
        $ast = $twig->parse($tokens);
        return $this->inferFromAst($ast);
    }

    /**
     * @param \Twig\Node\ModuleNode $ast - An abstract syntax tree parsed from Twig
     * @return array - The variables used in the Twig template
     */
    public function inferFromAst(\Twig\Node\ModuleNode $ast)
    {
        $keys = $this->visit($ast);

        foreach ($keys as $key => $value) {
            if ($value['always_defined'] || $key === '_self') {
                unset($keys[$key]);
            }
        }

        return $keys;
    }

    /**
     * @param \Twig\Node\Node $ast - The tree to traverse and extract variables
     * @return array - The variables found in this tree
     */
    private function visit(\Twig\Node\Node $ast)
    {
        $vars = [];
        switch (get_class($ast)) {
            case \Twig\Node\Expression\AssignNameExpression::class:
            case \Twig\Node\Expression\NameExpression::class:
                $vars[$ast->getAttribute('name')] = [
                    'type' => get_class($ast),
                    'always_defined' => $ast->getAttribute('always_defined'),
                    'is_defined_test' => $ast->getAttribute('is_defined_test'),
                    'ignore_strict_check' => $ast->getAttribute('ignore_strict_check')
                ];
                break;
            case \Twig\Node\ForNode::class:
                foreach ($ast as $key => $node) {
                    switch ($key) {
                        case 'value_target':
                            $vars[$node->getAttribute('name')] = [
                                'for_loop_target' => true,
                                'always_defined' => $node->getAttribute('always_defined')
                            ];
                            break;
                        case 'body':
                            $vars = array_merge($vars, $this->visit($node));
                            break;
                        default:
                            break;
                    }
                }
                break;
            case \Twig\Node\IfNode::class:
                foreach ($ast->getNode('tests') as $key => $test) {
                    $vars = array_merge($vars, $this->visit($test));
                }
                foreach ($ast->getNode('else') as $key => $else) {
                    $vars = array_merge($vars, $this->visit($else));
                }
                break;
            default:
                if ($ast->count()) {
                    foreach ($ast as $key => $node) {
                        $vars = array_merge($vars, $this->visit($node));
                    }
                }
                break;
        }
        return $vars;
    }
}
Cameron Wilby
  • 2,222
  • 1
  • 25
  • 35
1

After I spent quite a night, trying all the above answers, I realized, for some unexpected reason, regexps did not work at all with my simple templates. They returned junk or partial information. So I decided to go by erasing all the content between tags instead of counting tags ^_^.

I mean, if a template is 'AAA {{BB}} CC {{DD}} {{BB}} SS', I just add '}}' in the beginning of the template and '{{ in the end.... and all the content between }} and {{ I'll just strip out, adding comma in between =>}}{{BB,}}{{DD,}}{{BB,}}{{. Then - just erase }} and {{.

It took me about 15 min to write and test.... but with regexps I've spent about 5 hrs with no success.

/**
 * deletes ALL the string contents between all the designated characters
 * @param $start - pattern start 
 * @param $end   - pattern end
 * @param $string - input string, 
 * @return mixed - string
 */
 function auxDeleteAllBetween($start, $end, $string) {
    // it helps to assembte comma dilimited strings
    $string = strtr($start. $string . $end, array($start => ','.$start, $end => chr(2)));
    $startPos  = 0;
    $endPos = strlen($string);
    while( $startPos !== false && $endPos !== false){
        $startPos = strpos($string, $start);
        $endPos = strpos($string, $end);
        if ($startPos === false || $endPos === false) {
            return $string;
        }
        $textToDelete = substr($string, $startPos, ($endPos + strlen($end)) - $startPos);
        $string = str_replace($textToDelete, '', $string);
    }
    return $string;
}

/**
 * This function is intended to replace
 * //preg_match_all('/\{\%\s*([^\%\}]*)\s*\%\}|\{\{\s*([^\}\}]*)\s*\}\}/i', 
 * which did not give intended results for some reason.
 *
 * @param $inputTpl
 * @return array
 */
private function auxGetAllTags($inputTpl){
   $inputTpl = strtr($inputTpl, array('}}' => ','.chr(1), '{{' => chr(2)));
   return explode(',',$this->auxDeleteAllBetween(chr(1),chr(2),$inputTpl));
}


$template = '<style>
td{border-bottom:1px solid #eee;}</style>
<p>Dear {{jedi}},<br>New {{padawan}} is waiting for your approval: </p>
<table border="0">
<tbody><tr><td><strong>Register as</strong></td><td>{{register_as}}, user-{{level}}</td></tr>
<tr><td><strong>Name</strong></td><td>{{first_name}} {{last_name}}</td></tr>...';

print_r($this->auxGetAllTags($template));

Hope it'll help somebody :)

duncan
  • 31,401
  • 13
  • 78
  • 99
Alexey Abraham
  • 379
  • 2
  • 13
1

If you look at twig compile process you can see that there is parameter called ignore_strict_check if it's true compile will replace missed variables with null but if false compile will throw run time error look at file twig/src/Node/Expression/NameExpression.php line 63 in symfony you can set this parameter via twig package configurations strict_variables: false

Tanogo
  • 11
  • 1
0

Create a Twig_Extension and add a function with the needs_context flag:

class MyTwigExtension extends Twig_Extension{
   public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('myTwigFunction', array($this, 'myTwigFunction'), array('needs_context' => true)),
        );
    }

    public function myTwigFunction($context)
    {
        var_dump($context);
        return '';
    }
}

The context will be passed as first parameter to your function, containing all variables.

On your Twig template you just have to call that function:

{{myTwigFunction()}}

If you need assistance on creating a Twig Extension, please refer to this documentation:

http://twig.sensiolabs.org/doc/2.x/advanced.html

0

This Question has a douplicate – there I found a useful and kind of more powerfull RegEX than above. This one, I've improved to match more precise:

\{\{(?!%)\s* # Starts with {{ not followed by % followed by 0 or more spaces
  ((?:(?!\.)[^\s])*?) # Match anything without a point or space in it
  (\|(?:(?!\.)[^\s])*)? # Match filter within variable
\s*(?<!%)\}\} # Ends with 0 or more spaces not followed by % ending with }}
| # Or
\{%\s* # Starts with {% followed by 0 or more spaces
  (?:\s(?!endfor)|(endif)|(else)(\w+))+ # Match the last word which can not be endfor, endif or else
\s*%\} # Ends with 0 or more spaces followed by %}
# Flags: i: case insensitive matching | x: Turn on free-spacing mode to ignore whitespace between regex tokens, and allow # comments.
Community
  • 1
  • 1
macwinnie
  • 66
  • 7
  • Is this code in this form, keeping the line breaks and comments? And what is the second part for? – sylbru Apr 20 '18 at 14:44
  • 1
    Answer to myself: it does work using: ```preg_match_all("/ ... # this ... # super long ... # regex /x", $str, $matches);``` (can't find a way to do linebreaks here, but you get the point) – sylbru Apr 20 '18 at 15:18
0

Have a actual solution for Twig 3.0:

function visit(\Twig\Node\Node $node, &$variables)
{
    // @see https://github.com/twigphp/Twig/issues/2340 for details about NodeCaptureInterface
    if ($node instanceof \Twig\Node\NodeCaptureInterface) {
        return;
    }

    if ($node instanceof \Twig\Node\Expression\NameExpression
        && false === $node->getAttribute('always_defined') // ignore scoped names as (key, value) in for loop 
    ) {
        $variables[$node->getAttribute('name')] = null;

        return;
    }

    foreach ($node as $child) {
        visit($child, $variables);
    }
}

// @var Twig\Environment $twig

// create template from string
$template = 'my template {{ var }}';
$source = new \Twig\Source($template, 'template');
// or get from loader
// $source = $twig->getLoader()->getSourceContext('template.twig.html');

// tokenize and par
$tokens = $twig->tokenize($source);
$nodes = $twig->parse($tokens);

$variables = [];
visit($nodes, $variables);

var_dump(array_keys($variables));