DEV Community

Abdulloh Abdusamadov
Abdulloh Abdusamadov

Posted on

Why I Stopped Using reset() and end() in PHP (And What I Use Now)

If you have been writing PHP for a year or two, you have almost certainly written something like this:

$items = getFilteredOrders();
$first = reset($items);
$last  = end($items);
Enter fullscreen mode Exit fullscreen mode

It works. It runs. It looks fine in code review. And it contains a subtle bug waiting to happen.


The problem with reset() and end()

Both functions move PHP's internal array pointer as a side effect.

PHP arrays have a hidden cursor that tracks "the current position" in the array. Functions like current(), next(), prev(), and each() depend on where that cursor sits. reset() moves it to the first element. end() moves it to the last.

This is harmless in an isolated script. But in a real application, the same array often passes through multiple functions — and any function that relies on the cursor position after your call will get unexpected results.

Here is a concrete example:

function getFirstActiveOrder(array $orders): ?array
{
    // Moves the pointer to the first element
    $first = reset($orders);

    foreach ($orders as $order) {
        if ($order['status'] === 'active') {
            return $order;
        }
    }
    return null;
}
Enter fullscreen mode Exit fullscreen mode

At first glance this looks fine. But foreach in PHP actually resets the pointer to the beginning before iterating — so in this simple case you get lucky. The bug shows up in less obvious situations: when you call reset() inside a function that the caller is already iterating with current() / next(), or when you use end() to peek at the last item while a foreach is in progress on the same array reference.

The deeper issue: reset() and end() return false on an empty array. But false might also be a legitimate value stored in your array:

$flags = [true, false, true];
$last  = end($flags); // returns false — but is this "empty array" or "the value false"?
Enter fullscreen mode Exit fullscreen mode

You have to write:

if ($flags === [] || end($flags) === false) { ... }
Enter fullscreen mode Exit fullscreen mode

Which is already awkward. And it still mutates the pointer.


The modern solution: array_key_first() and array_key_last()

PHP 7.3 introduced two functions that solve the pointer problem completely:

$key   = array_key_first($items); // returns the first KEY, or null if empty
$key   = array_key_last($items);  // returns the last KEY, or null if empty
$first = $items[$key] ?? null;
Enter fullscreen mode Exit fullscreen mode

These functions are read-only. They inspect without touching the pointer. null on an empty array instead of false eliminates the ambiguity.

For associative arrays this is particularly clean — you get back the actual key ('order_id', 'sku', 0, whatever it is) and can use it directly.


PHP 8.5: array_first() and array_last()

PHP 8.5 goes one step further with two new functions that return the value directly:

$first = array_first($items); // returns the first value, or null if empty
$last  = array_last($items);  // returns the last value, or null if empty
Enter fullscreen mode Exit fullscreen mode

No pointer mutation. No false ambiguity. One function call.

The behavioral difference from reset() is easy to demonstrate:

$data = [10, 20, 30, 40, 50];

end($data); // move pointer to end

echo array_first($data); // 10 — pointer NOT moved, still at end
echo current($data);     // 50 — confirmed: pointer unchanged

echo reset($data);       // 10 — pointer IS reset to beginning
echo current($data);     // 10 — pointer moved
Enter fullscreen mode Exit fullscreen mode

array_first() reads without touching anything. reset() reads and moves. For any code that cares about cursor position — including code you did not write — this distinction matters.


The pattern that replaces most foreach searches

Here is a pattern that comes up constantly in backend PHP: find the first element matching some condition. Most developers write a foreach with a break:

$firstPending = null;
foreach ($orders as $order) {
    if ($order['status'] === 'pending') {
        $firstPending = $order;
        break;
    }
}
Enter fullscreen mode Exit fullscreen mode

With array_first() and array_filter() you can express this in one line:

$firstPending = array_first(array_filter($orders, fn($o) => $o['status'] === 'pending'));
Enter fullscreen mode Exit fullscreen mode

array_filter() produces a sub-array of matching orders. array_first() takes the first one, or returns null if none matched. No loop, no tracking variable, no break.

The same pattern works for the last match:

$lastShipped = array_last(array_filter($orders, fn($o) => $o['status'] === 'shipped'));
Enter fullscreen mode Exit fullscreen mode

Quick reference

Goal Old way Modern way
Get first value reset($arr) array_first($arr) (PHP 8.5)
Get last value end($arr) array_last($arr) (PHP 8.5)
Get first key array_key_first($arr) same (PHP 7.3+)
Get last key array_key_last($arr) same (PHP 7.3+)
First matching element foreach + break array_first(array_filter(...))
Last matching element foreach + break from end array_last(array_filter(...))

Wrapping up

reset() and end() are not wrong — they are just older tools with side effects that were acceptable before PHP had better alternatives. For any new code on PHP 7.3+ use array_key_first() and array_key_last(). On PHP 8.5 use array_first() and array_last() directly.

The rule is simple: if you only need to read a boundary value, use a function that does not move anything.


I explored this pattern — and 99 others like it — while writing Linear Code Mastery, a PHP 8.4/8.5 workbook of 100 progressive backend challenges built around real enterprise scenarios. If this kind of practical depth interests you, take a look.

Top comments (0)