PHP Warning: “continue” targeting switch is equivalent to “break”. Did you mean to use “continue 2”?

A colleague of mine pointed me to this strange warning that started to appear in a server logs.
I’m not a PHP expert, but I use since twelve years (at least), and I know it carries on old behaviours due to a not so solid implementation of the language itself, and apparently, this is another one.
Here I’m going to discuss what the above warning referes to, but the important thing to note is that the above is a warning, and does not mean your code is going to stop working, on the other hand it advices that in future PHP releases, something may change and your code could break!

break and continue

In most sane languages, in particular those that derive from C, the two keywords do not have the same meaning:
  • break means “exit from the current loop immediatly”;
  • continue means “stop the current iteration and restart the loop from the very next one”.

Therefore, break means the loop must be terminated, continue means that the current iteration must be terminated, but the next one can continue.
The problem here arises from the fact that break can be used into other structures, most notably the switch one: even in such context, break means “exit from the switch right now”. In other words, you can think of break as “exit from the code block immediatly”, without any regard the code block is a loop or not.
In fact, C and derived languages have a switch implementation that is not the equivalent of an if-elsif-elsif... branching as it is often taught: the switch means “jump into the first match and go on from there”, where the break is used to exit the matching branch as soon as it finishes.
Raku, for example, provides a given when construct that is equivalent to a C switch with “automatic” break on each branch (and a lot more power).

Is switch a loop?

Of course switch is not a looping construct, however in PHP the switch is considered a looping block:

Note: In PHP the switch statement is considered a looping structure for the purposes of continue. continue behaves like break (when no arguments are passed) but will raise a warning as this is likely to be a mistake. If a switch is inside a loop, continue 2 will continue with the next iteration of the outer loop.


So, not only the switch is considered and implemented as a looping block, but PHP considers a break a mistake inside a switch. Why? Because in a looping structure you probably want to continue, not to break.
And this is why I don’t believe PHP is a sane language: reading the code you are going to find a keyword continue, that to my poor brain means, well, “continue”, that however is going to be implemented as a “break”.

But it does not ends here: if continue has a numeric argument, it will try to restart the loop outer of the specified number of levels. The innermost loop is at level 1, the outer loop is at level 2, and so on. Here’s why the system suggest you to write a continu 2, that will make the program flow to continue to the outer loop iteration.

An Example

Let’s start with a looping example: nest three level of foreach:

<?php
$array = array( 1, 2, 3, 4 );

foreach( $array as $big_outer ) {
    foreach( $array as $outer ) {
        foreach( $array as $inner ) {
            print "$big_outer $outer-$inner \n";
            continue 3;
        }
    }
}

?>


The above piece of code produces the following output, because at each iteration the loop starts over from the level 3, that is the outermost one:

% php test.php
1 1-1
2 1-1
3 1-1
4 1-1


If the continue 3 is replaced with a continue 2 the output changes, because only the innermost loop is interrupted:

% php test.php
1 1-1
1 2-1
1 3-1
1 4-1
2 1-1
2 2-1
2 3-1
2 4-1
3 1-1
3 2-1
3 3-1
3 4-1
4 1-1
4 2-1
4 3-1
4 4-1


Let’s see what happens with a switch within a loop:

<?php
$array = array( 1, 2, 3, 4 );

foreach( $array as $outer ) {
    print "Loop $outer \n";

    foreach( $array as $current ) {
        switch( $current ) {
        case 2: print "Two! \n";
            continue 2;

        case 4:
            print "Four! \n";
            continue 2;
        }
    }
}

?>



that produces the following output:

% php test.php
Loop 1
Two!
Four!
Loop 2
Two!
Four!
Loop 3
Two!
Four!
Loop 4
Two!
Four!



continue with an argument is prone to errors (?)

The continue keyword cannot accept a number of levels higher than those the control flow detects:

<?php
$array = array( 1, 2, 3, 4 );

foreach( $array as $outer ) {
    print "Loop $outer \n";

    foreach( $array as $current ) {
        switch( $current ) {
        case 2: print "Two! \n";
            continue 10;

        case 4:
            print "Four! \n";
            continue 10;
        }
    }
}

?>



produces the runtime error PHP Fatal error: Cannot 'continue' 10 levels.
This could be a problem because, when you refactor the code, you need to inspect the deep of each level of iteration and adjuct all the continue keywords. On the other hand, if you increase the level of nesting, you could mistakenly leave a lower argument to continue that does not produce a loop interruption.

Conclusions

I don’t like PHP very much, and the presence of these odd behaviours, like the continue within switct makes it a lot less readable that what I would like it to be.
This does not mean that the language is bad, or it cannot work; it simply means it can be difficult to embrace PHP when you are used to other languages, even those that are similar by syntax.

The article PHP Warning: "continue" targeting switch is equivalent to "break". Did you mean to use "continue 2"? has been posted by Luca Ferrari on October 11, 2021