This is somewhat similar to question: How to determine the first and last iteration in a foreach loop?, however, that +10 years old question is heavily array
oriented and none of the answer are compatible with the fact that many different types can be looped on.
Given a loop on something that can be iterated in PHP >= 7.4 (arrays, iterators, generators, PDOStatement
, DatePeriod
, object properties,...), how can we trigger, in an efficient way, code that needs to happen before / after the loop, but only in the case the loop would be entered?
A typical use case could be the generation of an HTML list:
<ul>
<li>...</li>
<li>...</li>
...
</ul>
<ul>
and </ul>
must be printed only if there are some elements.
Those are the constraint I discovered so far:
empty()
: Can't be used on generators/iterators.each()
: is deprecated.iterator_to_array()
defeats the advantage of generators.- A boolean flag tested inside and after the loop is not considered efficient as it would result in that test to be executed at every single iteration instead of once at the start and once at the end of the loop.
- While output buffering or string concatenations to generate the output may be used in the above example, it would not fit the case where a loop would not produce any output. (thanks @barmar for the additional idea)
The following code snippet summarize many different types on which we can iterate with foreach
, it can be used as a start to provide an answer:
<?php
// Function that iterates on $iterable
function iterateOverIt($iterable) {
// How to generate the "<ul>"?
foreach ($iterable as $item) {
echo "<li>", $item instanceof DateTime ? $item->format("c") : (
isset($item["col"]) ? $item["col"] : $item
), "</li>\n";
}
// How to generate the "</ul>"?
}
// Empty array
iterateOverIt([]);
iterateOverIt([1, 2, 3]);
// Empty generator
iterateOverIt((function () : Generator {
return;
yield;
})());
iterateOverIt((function () : Generator {
yield 4;
yield 5;
yield 6;
})());
// Class with no public properties
iterateOverIt(new stdClass());
iterateOverIt(new class { public $a = 7, $b = 8, $c = 9;});
$db = mysqli_connect("localhost", "test", "test", "test");
// Empty resultset
iterateOverIt($db->query("SELECT 0 FROM DUAL WHERE false"));
iterateOverIt($db->query("SELECT 10 AS col UNION SELECT 11 UNION SELECT 12"));
// DatePeriod generating no dates
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), new DateTime("2020-01-01 00:00:00"), DatePeriod::EXCLUDE_START_DATE));
iterateOverIt(new DatePeriod(new DateTime("2020-01-01 00:00:00"), new DateInterval("P1D"), 3, DatePeriod::EXCLUDE_START_DATE));
Such a script should result in the following output:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<ul>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<ul>
<li>10</li>
<li>11</li>
<li>12</li>
</ul>
<ul>
<li>2020-01-02T00:00:00+00:00</li>
<li>2020-01-03T00:00:00+00:00</li>
<li>2020-01-04T00:00:00+00:00</li>
</ul>