- Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
You have a list of results. You want the first one. You reach for reset($results). It works, and then a code reviewer asks why you're mutating an array's internal pointer to peek at element zero. You don't have a good answer, because there isn't one. You did it because for twenty-five years PHP had no clean way to say "give me the first value."
PHP 8.5, released in November 2025, closes that gap. Two functions: array_first() and array_last(). They do exactly what the names say, they don't touch the internal pointer, and they behave sanely on empty input. The RFC passed with almost no opposition, which tells you how long this hole has been annoying people.
The four ways you used to do it
Getting the first value out of a PHP array has always worked. It just never looked good. Here are the patterns you've written:
// 1. reset() — mutates the internal pointer
$first = reset($results);
// 2. array_key_first() then index back in
$first = $results[array_key_first($results)];
// 3. array_values() then [0], allocates a whole array
$first = array_values($results)[0];
// 4. array_slice() + current(), also allocates
$first = current(array_slice($results, 0, 1));
The end() versions for the last element are worse, because end() walks and moves the pointer to the tail. None of these read as intent. Every one of them makes a reviewer stop for half a second to decode what you meant.
The reset()/end() pair carries a real side effect. They move the array's internal pointer. If any code downstream relies on current() or a foreach that assumed a fresh pointer, you've introduced a bug that only shows up under specific ordering. It's rare, but when it bites, it bites in production and nowhere else.
What 8.5 gives you
$first = array_first($results);
$last = array_last($results);
That's the whole feature. Both take an array, return the value of the first or last element, and leave the internal pointer where it was. No mutation, no allocation, no key gymnastics.
They respect insertion order, which is the order PHP arrays already guarantee. Keys don't matter:
$config = [
'timeout' => 30,
'retries' => 3,
'backoff' => 'exponential',
];
array_first($config); // 30
array_last($config); // 'exponential'
Both work on string-keyed maps, integer-keyed lists, and gap-filled arrays alike. You're asking for a value by position, not by key, so the key shape is irrelevant.
The empty-array behavior, which is the whole point
This is where the old approaches leaked. Watch what reset() does on an empty array:
$empty = [];
$value = reset($empty); // false
It returns false. So does end(). Now you have the classic PHP ambiguity: was the array empty, or did it genuinely contain false as its first element? You can't tell from the return value. You end up writing if ($value === false && !empty($empty)) or bailing out with an empty() guard before the call.
array_key_first() on an empty array is its own trap:
$empty = [];
$key = array_key_first($empty); // null
$value = $empty[$key]; // $empty[null] → warning + null
You get an "undefined array key" warning because you're indexing with null. In strict projects that's an error, not a warning.
array_first() returns null on an empty array, quietly, no warning:
array_first([]); // null
array_last([]); // null
One caveat worth being honest about: null is also a legitimate value an array can hold, so array_first() alone can't distinguish "empty" from "first element is null." That's the same limitation ?? has everywhere in PHP. If you need that distinction, check $array === [] first. But you're no longer confusing an empty array with a stored false, and you're no longer eating a warning to read element zero. For the overwhelming majority of call sites, array_first() returning null is exactly the behavior you wanted.
Where it actually cleans up code
The wins aren't in toy examples. They show up in the small helper methods scattered across a codebase. A repository that returns the most recent match:
public function latestFor(CustomerId $id): ?Order
{
$orders = $this->findByCustomer($id); // sorted desc
return array_first($orders);
}
Before 8.5, that method body was either a reset() with a comment explaining the pointer side effect, or a three-line if (empty(...)) return null; dance. Now it's one line that reads as the sentence you'd say out loud.
Pulling the last event off a stream:
$lastEvent = array_last($aggregate->recordedEvents());
Grabbing the head of a parsed CSV to sniff headers, taking the final segment of a path split, reading the newest entry in an audit log. All of them collapse to a named call. The intent is on the surface instead of buried in a pointer-moving idiom.
A note on nested and chained calls
Because these functions don't mutate, they compose. You can call them on the result of a filter or a map without worrying about pointer state:
$firstActive = array_first(
array_filter($users, fn (User $u) => $u->isActive())
);
array_filter() preserves keys, so the result can start at key 3 or 7. That's exactly the case where array_values($filtered)[0] used to be the "safe" idiom, allocating a reindexed array just to read one element. array_first() skips the allocation and ignores the keys entirely.
The same holds for the last element after a sort:
usort($scores, fn ($a, $b) => $a->points <=> $b->points);
$highest = array_last($scores);
Polyfill for pre-8.5 code
If you maintain a library that supports older PHP, you can ship a polyfill and start using the names now. The symfony/polyfill-php85 package covers these once it lands; a hand-rolled version is a few lines:
if (!function_exists('array_first')) {
function array_first(array $array): mixed
{
foreach ($array as $value) {
return $value;
}
return null;
}
}
if (!function_exists('array_last')) {
function array_last(array $array): mixed
{
return empty($array)
? null
: $array[array_key_last($array)];
}
}
The foreach-and-return trick for array_first() reads the first value without touching the pointer or allocating, which is what the native function does under the hood. Once your minimum version is 8.5, delete the polyfill and let the engine handle it.
When you still reach for the old functions
array_first() and array_last() give you the value. If you need the key, array_key_first() and array_key_last() are still the right tools, and they've been around since PHP 7.3. If you genuinely need to walk the internal pointer for a stateful iteration, reset(), end(), next(), and current() still exist. That's a narrow case, and if you're doing it, do it on purpose with a comment, not as an accidental way to read element zero.
For the everyday "what's the first thing in this list" question, the answer is finally a function that says so.
Which of your helper methods gets shorter the day you bump to 8.5? The repository latest() methods are usually the first to fall.
If this was useful
A function like array_first() is a small thing, but it's the same instinct that keeps a codebase readable at the large scale: say what you mean at the call site, and keep the mechanical noise out of the domain. When your entities and use cases stop leaking pointer tricks and framework idioms, they get easier to test and easier to keep alive across a framework swap. That boundary is what Decoupled PHP is about: domain code that reads like the business, infrastructure code that handles the mechanics.
Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

Top comments (0)