How can I use break or continue within for loop in Twig template?

PhpSymfonyFor LoopTwigBreak

Php Problem Overview


I try to use a simple loop, in my real code this loop is more complex, and I need to break this iteration like:

{% for post in posts %}
    {% if post.id == 10 %}
        {# break #}
    {% endif %}
    <h2>{{ post.heading }}</h2>
{% endfor %}

How can I use behavior of break or continue of PHP control structures in Twig?

Php Solutions


Solution 1 - Php

This can be nearly done by setting a new variable as a flag to break iterating:

{% set break = false %}
{% for post in posts if not break %}
    <h2>{{ post.heading }}</h2>
    {% if post.id == 10 %}
        {% set break = true %}
    {% endif %}
{% endfor %}

An uglier, but working example for continue:

{% set continue = false %}
{% for post in posts %}
    {% if post.id == 10 %}
        {% set continue = true %}
    {% endif %}
    {% if not continue %}
        <h2>{{ post.heading }}</h2>
    {% endif %}
    {% if continue %}
        {% set continue = false %}
    {% endif %}
{% endfor %}

> But there is no performance profit, only similar behaviour to the built-in break and continue statements like in flat PHP.

Solution 2 - Php

From docs TWIG 2.x docs:

> Unlike in PHP, it's not possible to break or continue in a loop.

But still:

> You can however filter the sequence during iteration which allows you to skip items.

Example 1 (for huge lists you can filter posts using slice, slice(start, length)):

{% for post in posts|slice(0,10) %}
    <h2>{{ post.heading }}</h2>
{% endfor %}

Example 2 works TWIG 3.0 as well:

{% for post in posts if post.id < 10 %}
    <h2>{{ post.heading }}</h2>
{% endfor %}

You can even use own TWIG filters for more complexed conditions, like:

{% for post in posts|onlySuperPosts %}
    <h2>{{ post.heading }}</h2>
{% endfor %}

Solution 3 - Php

A way to be able to use {% break %} or {% continue %} is to write TokenParsers for them.

I did it for the {% break %} token in the code below. You can, without much modifications, do the same thing for the {% continue %}.

  • AppBundle\Twig\AppExtension.php:

      namespace AppBundle\Twig;
    
      class AppExtension extends \Twig_Extension
      {
          function getTokenParsers() {
              return array(
                  new BreakToken(),
              );
          }
          
          public function getName()
          {
              return 'app_extension';
          }
      }
    
  • AppBundle\Twig\BreakToken.php:

      namespace AppBundle\Twig;
    
      class BreakToken extends \Twig_TokenParser
      {
          public function parse(\Twig_Token $token)
          {
              $stream = $this->parser->getStream();
              $stream->expect(\Twig_Token::BLOCK_END_TYPE);
    
              // Trick to check if we are currently in a loop.
              $currentForLoop = 0;
    
              for ($i = 1; true; $i++) {
                  try {
                      // if we look before the beginning of the stream
                      // the stream will throw a \Twig_Error_Syntax
                      $token = $stream->look(-$i);
                  } catch (\Twig_Error_Syntax $e) {
                      break;
                  }
                  
                  if ($token->test(\Twig_Token::NAME_TYPE, 'for')) {
                      $currentForLoop++;
                  } else if ($token->test(\Twig_Token::NAME_TYPE, 'endfor')) {
                      $currentForLoop--;
                  }
              }
              
    
              if ($currentForLoop < 1) {
                  throw new \Twig_Error_Syntax(
                      'Break tag is only allowed in \'for\' loops.',
                      $stream->getCurrent()->getLine(),
                      $stream->getSourceContext()->getName()
                  );
              }
    
              return new BreakNode();
          }
    
          public function getTag()
          {
              return 'break';
          }
      }
    
  • AppBundle\Twig\BreakNode.php:

      namespace AppBundle\Twig;
    
      class BreakNode extends \Twig_Node
      {
          public function compile(\Twig_Compiler $compiler)
          {
              $compiler
                  ->write("break;\n")
              ;
          }
      }
    

Then you can simply use {% break %} to get out of loops like this:

{% for post in posts %}
    {% if post.id == 10 %}
        {% break %}
    {% endif %}
    <h2>{{ post.heading }}</h2>
{% endfor %}

To go even further, you may write token parsers for {% continue X %} and {% break X %} (where X is an integer >= 1) to get out/continue multiple loops like in PHP.

Solution 4 - Php

From @NHG comment — works perfectly

{% for post in posts|slice(0,10) %}

Solution 5 - Php

I have found a good work-around for continue (love the break sample above). Here I do not want to list "agency". In PHP I'd "continue" but in twig, I came up with alternative:

{% for basename, perms in permsByBasenames %} 
    {% if basename == 'agency' %}
        {# do nothing #}
    {% else %}
        <a class="scrollLink" onclick='scrollToSpot("#{{ basename }}")'>{{ basename }}</a>
    {% endif %}
{% endfor %}

OR I simply skip it if it doesn't meet my criteria:

{% for tr in time_reports %}
    {% if not tr.isApproved %}
        .....
    {% endif %}
{% endfor %}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionVictor BocharskyView Question on Stackoverflow
Solution 1 - PhpVictor BocharskyView Answer on Stackoverflow
Solution 2 - PhpNHGView Answer on Stackoverflow
Solution 3 - PhpJules LamurView Answer on Stackoverflow
Solution 4 - PhpBasitView Answer on Stackoverflow
Solution 5 - PhppaidforbychristView Answer on Stackoverflow