PHP 8.5's Hidden Gems: array_first, clone with, and the Features Nobody Is Talking About
Beyond the pipe operator and URI extension, PHP 8.5 ships quality-of-life features that clean up your daily code immediately — here's what to know.
The PHP 8.5 coverage cycle did what it always does: the headline features sucked up all the oxygen. The pipe operator |> was everywhere. The new URI extension got well-deserved attention. But PHP 8.5 shipped with a cluster of smaller improvements that are going to make a quiet, meaningful difference in the PHP you write every single day. This post is about those.
array_first() and array_last()
PHP 7.3 added array_key_first() and array_key_last() — useful, but somewhat backwards. You rarely want the first key; you almost always want the first value. To get it, PHP developers have been reaching for reset() and end(), which are functionally terrible for this purpose: they mutate the array’s internal pointer (a side effect you almost never want) and they behave unreliably on expressions that aren’t plain variables.
PHP 8.5 fixes this with array_first() and array_last():
$items = ['apple', 'banana', 'cherry'];
echo array_first($items); // apple
echo array_last($items); // cherry
Both return null when the array is empty, making them safe to compose with the null coalescing operator without any guard clauses:
$config = [];
$driver = array_first($config) ?? 'file'; // 'file'
That replaces the previous contenders, all of which had drawbacks:
// reset() mutates the array pointer — wrong semantics
$first = reset($items);
// Works but requires knowing the destructuring trick
[$first] = $items;
// Roundabout — fetches the key first, then the value
$first = $items[array_key_first($items)];
In application code, the new functions are most useful in service classes and pipeline stages that work with plain PHP arrays rather than Eloquent collections. A value object factory, for instance, reads much more clearly:
class CurrencyResolver
{
public function __construct(private readonly array $supported) {}
public function default(): string
{
return array_first($this->supported) ?? throw new \RuntimeException(
'No supported currencies configured.'
);
}
public function fallback(): string
{
return array_last($this->supported) ?? 'USD';
}
}
No pointer side effects, no cryptic destructuring, no key lookups. Just readable intent.
clone with Property Modifications
PHP’s clone keyword has existed since PHP 5. The problem is that it was always a blunt instrument: clone an object, get a shallow copy, then manually mutate the clone to adjust whatever you needed. That mutation pattern broke entirely when readonly properties arrived — you cannot write to a readonly property after construction, even on a clone.
PHP 8.5 makes clone accept a second argument: an associative array of properties and the values they should have in the cloned object.
readonly class Money
{
public function __construct(
public int $amount,
public string $currency
) {}
}
$original = new Money(100, 'USD');
$adjusted = clone($original, ['amount' => 150]);
echo $original->amount; // 100 — unchanged
echo $adjusted->amount; // 150
Properties not listed in the array carry over from the original. The clone honors __clone() magic methods and property hooks, so no surprises if your class has them.
Cleaning Up the “with-er” Pattern
The main beneficiary is the wither pattern — a common idiom in modern PHP for producing modified copies of immutable value objects. Before PHP 8.5, each method had to reconstruct the entire object explicitly:
readonly class HttpRequest
{
public function __construct(
public string $method,
public string $path,
public array $headers,
public string $body,
) {}
public function withMethod(string $method): static
{
return new static($method, $this->path, $this->headers, $this->body);
}
public function withHeader(string $key, string $value): static
{
return new static(
$this->method,
$this->path,
[...$this->headers, $key => $value],
$this->body
);
}
}
Every constructor argument has to be threaded through every wither method, even the ones it has nothing to do with. Adding a new property means updating every wither. With clone with, each method becomes a one-liner:
readonly class HttpRequest
{
public function __construct(
public string $method,
public string $path,
public array $headers,
public string $body,
) {}
public function withMethod(string $method): static
{
return clone($this, ['method' => $method]);
}
public function withHeader(string $key, string $value): static
{
return clone($this, ['headers' => [...$this->headers, $key => $value]]);
}
}
Adding a new property to the constructor no longer means updating every wither. The reduction in boilerplate is real, and it compounds across larger domain models.
As a bonus, clone can now be used as a first-class callable, which means you can pass it to higher-order functions like array_map():
// Clone an array of objects
$copies = array_map(clone(...), $originals);
Attributes on Class Constants
PHP 8.0 introduced attributes — structured, reflectable metadata attached to classes, functions, methods, properties, and parameters. What was conspicuously missing: class constants. PHP 8.5 closes that gap.
use Symfony\Component\Serializer\Attribute\Groups;
class OrderStatus
{
#[Groups(['public'])]
const PENDING = 'pending';
#[Groups(['public'])]
const COMPLETED = 'completed';
#[Groups(['internal'])]
const REFUND_PENDING = 'refund_pending';
}
This is useful anywhere serialization, documentation generation, or validation tooling previously had to fall back on docblock conventions or custom workarounds. The Symfony serializer’s group system, for example, can now be applied to constants directly.
Reflection support is complete, so custom tools can inspect constant attributes at runtime:
$reflection = new ReflectionClassConstant(OrderStatus::class, 'PENDING');
$groups = $reflection->getAttributes(Groups::class);
For package authors building domain-language APIs or annotation-driven validation, this eliminates a long-standing gap.
Fatal Error Backtraces
This one is small but meaningful for anyone who has stared at a fatal error log entry with no call stack.
Before PHP 8.5, fatal errors — real engine-level fatals like E_ERROR, out-of-memory crashes, infinite recursion stack exhaustions — logged the file and line where the crash occurred, but nothing about how execution arrived there. You could identify where but not why.
PHP 8.5 includes a full backtrace in fatal error output, the same call stack you get with exceptions. For applications using structured error aggregation tools like Sentry or Bugsnag, the payloads you receive after upgrade will automatically include significantly more context. No configuration change required.
Handler Introspection
Two small quality-of-life functions round out the release: get_error_handler() and get_exception_handler(). They return the currently registered error and exception handler callables, respectively, or null if no custom handler is set.
Previously, the only way to check what handler was registered was to call set_error_handler() with a new callable — which returned the previous handler as a side effect — and then immediately reinstall the old handler. It worked, but it was a code smell.
$current = get_exception_handler();
if ($current === null) {
set_exception_handler(function (\Throwable $e) {
// your fallback logging
});
}
This is primarily useful inside libraries, middleware, and framework bootstrap code that needs to be aware of existing handler state before installing its own.
What to Do Right Now
If you are already on PHP 8.5, none of these features require configuration or package updates — they are part of the language. For array_first() and array_last(), the migration is purely opportunistic: grep for reset( calls and array_key_first( patterns and decide case by case whether the new functions read more clearly. For clone with, the wins are highest in readonly value object hierarchies with multiple wither methods.
The main deprecations to watch for when upgrading are the backtick operator as an alias for shell_exec() and the non-canonical cast names (boolean), (integer), (double), and (binary). A quick search in your codebase should surface anything that needs updating before the upgrade.
PHP 8.5 is a release that rewards reading the full changelog. The headliners are real improvements — but so is everything listed here.