124

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?

Victor Bocharsky
  • 11,930
  • 13
  • 58
  • 91

5 Answers5

172

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.

Helenesh
  • 3,999
  • 2
  • 21
  • 36
Victor Bocharsky
  • 11,930
  • 13
  • 58
  • 91
  • 1
    It is useful. In my case I just need to show / get the first result. Is there a way in Twig to get just the first value? This is only for better performance purposes. – Pathros Feb 11 '16 at 15:27
  • 1
    @pathros In order to get the first value, use the `first` twig filter: http://twig.sensiolabs.org/doc/filters/first.html – Victor Bocharsky Feb 11 '16 at 20:21
  • 1
    Love the note. Been trying my last 10mins finding something that not's really helpful :D – Tree Nguyen Sep 11 '17 at 01:08
  • 3
    It's worth noting that this will **not** break the code execution, anything below `set break = true` will be executed unless you put it in an `else` statement. See https://twigfiddle.com/euio5w – Gus Nov 03 '17 at 12:27
  • 2
    @Gus Yep, that's why I was meaning to put that if statement with `set break = true` in the very _end_. But yeah, it depends on your code, so thanks for mentioning it to clarify – Victor Bocharsky Nov 04 '17 at 19:46
  • But wouldn't it be easier to do this: (I don't know how to format it on this page) `{% for post in posts %} {% if post.id != 10 %}

    {{ post.heading }}

    {% endif %} {% endfor %}`
    – paidforbychrist May 15 '20 at 19:00
  • 3
    I use twig 3.x and had an issue with this method `if not break` I've got this error `Uncaught Twig\Error\SyntaxError: Unexpected token "name" of value "if"` – ZenithS May 18 '22 at 03:56
  • `Symfony: Twig conditional 'for' syntax is deprecated as of Twig 2.10` – Jonathan Feb 13 '23 at 07:14
144

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 %}
NHG
  • 5,807
  • 6
  • 34
  • 45
  • 28
    Moreover if you wanna achive break loop after 10 iteration you could use sth like that: `{% for post in posts|slice(0,10) %}` – NHG Feb 10 '14 at 09:20
  • 5
    OK, thanks, I probably missed `Unlike in PHP, it's not possible to break or continue in a loop.` when read the docs. But I think `break` and `continue` is a good features, which would need to add – Victor Bocharsky Feb 10 '14 at 09:35
  • You can't access loop variable in the loop statement! – Maximus May 15 '17 at 18:37
  • doesn't work. long list, `for` should be breakable after first hit. @VictorBocharsky 's answer is right – Vasilii Suricov Feb 20 '19 at 16:19
  • @VasiliiSuricov you can use `{% for post in posts|slice(0,10) %}` for huge lists. See my first comment. I've also updated my answer. – NHG Feb 20 '19 at 22:09
  • @NHG do you sure that needed element (because i need just one) will be in this slice? how you decided? i'll give you an example: list contains 999 elements, needed is on position 12. so first example wrong, 2nd and 3rd will check all 999 elements – Vasilii Suricov Feb 21 '19 at 10:53
  • @NHG, oh, sorry, i'm damn, `for` will run 999 times **anyway** – Vasilii Suricov Feb 21 '19 at 11:07
  • @VasiliiSuricov what's wrong with using arguments for `slice(start, length)`? In your example with 999 records: `slice(12, 1)` – NHG Feb 21 '19 at 13:56
  • @NHG wrng is that i dont know which one is right, i have to check. order in array can be different. >> you can use {% for post in posts|slice(0,10) %} not, u can't – Vasilii Suricov Feb 21 '19 at 14:18
  • @VasiliiSuricov please ask separated question for your specific case (I'll try to help) to avoid off-top discussion in comments section. – NHG Feb 21 '19 at 14:47
  • 4
    A heads up, while it's available in 2.0, Twig removed the {% for ... if ... %} statement in version 3.0. – Dave Lancea May 20 '21 at 15:43
  • Twig conditional 'for' syntax is deprecated as of Twig 2.10. – long Mar 10 '23 at 13:46
  • The replacement for the for... if... statement, could be used the filter "filter", something like this: {% for banner in banners|filter(b => b.isVisible) %} This works in twig 3.5 – Koronos Apr 06 '23 at 06:32
15

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.

Jules Lamur
  • 2,078
  • 1
  • 15
  • 25
  • 14
    That's just overkill. Twig loops should be supporting breaks and continues natively. – crafter Mar 04 '17 at 23:27
  • This is nice if you don't want to/can't use filters. – Daniel Dewhurst Mar 08 '18 at 12:17
  • 1
    The [`squirrelphp/twig-php-syntax` library](https://github.com/squirrelphp/twig-php-syntax) provides `{% break %}`, `{% break n %}` and `{% continue %}` tokens. – Matias Kinnunen Aug 07 '20 at 10:30
  • 1
    @mtsknn and the authors used and improved the code that I wrote for this answer! – Jules Lamur Aug 18 '20 at 09:27
  • @JulesLamur, you said "@mtsknn and the authors," but I'm not involved with that library. – Matias Kinnunen Aug 18 '20 at 09:54
  • @mtsknn sorry that's a technical misunderstanding, I meant it to be read like that: "The X library provides `{% break %}`, `{% break n %}` and `{% continue %}` tokens and the authors used and improved the code [...]". Your name in the comment was only here because I replied to you. – Jules Lamur Aug 18 '20 at 10:40
  • @JulesLamur, right, no worries. Now that I took a look at the library code ([`BreakOrContinueTokenParser.php`](https://github.com/squirrelphp/twig-php-syntax/blob/v1.5.1/src/TokenParser/BreakOrContinueTokenParser.php)), it's clearly based on your code. – Matias Kinnunen Aug 18 '20 at 12:07
  • A bit late to the party, but that's a brilliant answer, and the mentioned library from squirrelphp/twig-php-syntax is an awesome feat! Thank you JulesLamur and @MatiasKinnunen for your information – Florian Müller Apr 26 '23 at 14:11
8

From @NHG comment — works perfectly

{% for post in posts|slice(0,10) %}
Basit
  • 16,316
  • 31
  • 93
  • 154
6

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 %}
paidforbychrist
  • 359
  • 4
  • 9