PHP Sort Multidimensional Array by Value (usort, Tested)
usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.
PHP 8.6 is the next PHP release, scheduled for general availability on 19 November 2026. Its headline feature is partial function application: write str_replace('hello', 'hi', ?) and, instead of calling the function, PHP gives you back a closure waiting for the missing argument. Alongside that come a new clamp() function, default values for readonly properties, a Time\Duration class, a SortDirection enum, JSON error messages that tell you where the error is, and #[\Override] on class constants.
The PHP team released PHP 8.6.0RC3 on 8 October 2026, with RC4 expected on 22 October, according to the PHP 8.6.0RC3 announcement. I compiled RC3 from the official source tarball (after checking its SHA-256 hash against the announcement) and ran every example below with declare(strict_types=1). Release candidates only receive bug fixes, so these features should behave the same in the final release, but treat RC builds as test-only and don’t run them in production.
These dates come from the RC3 announcement and the release schedule published alongside it:
PHP 8.5, the current stable version, came out on 20 November 2025 and introduced the pipe operator |>. Several 8.6 features build directly on it, as you’ll see.
The full rules are in the partial function application RFC. The short version: in a function call, replace one or more arguments with a placeholder, and PHP returns a Closure instead of calling the function.
? means “exactly one argument goes here”.... means “any remaining arguments go here”.<?php
declare(strict_types=1);
function price(float $amount, float $vatRate, string $currency): string {
return $currency . number_format($amount * (1 + $vatRate), 2);
}
$withUkVat = price(?, 0.20, '£');
echo $withUkVat(10), "\n";
£12.00
$withUkVat is a closure that takes one argument, $amount, and fills in the other two. Before 8.6, you’d write fn(float $amount): string => price($amount, 0.20, '£'), repeating the type and the parameter name yourself.
The RFC says the generated closure copies the names, types, defaults and return type of the parameters you left open. Reflection confirms it:
echo new ReflectionFunction($withUkVat), "\n";
Closure [ <user> static function {closure:pfa:php86.php:7} ] {
@@ php86.php 7 - 7
- Bound Variables [2] {
Variable #0 [ $vatRate ]
Variable #1 [ $currency ]
}
- Parameters [1] {
Parameter #0 [ <required> float $amount ]
}
- Return [ string ]
}
(I’ve shortened the file path.) The closure has a required float $amount parameter and returns string, exactly like the original function. Static analysers and IDEs can therefore understand it.
Callbacks are where PFA saves the most typing:
$names = ['hello world', 'hello php'];
print_r(array_map(str_replace('hello', 'hi', ?), $names));
Array
(
[0] => hi world
[1] => hi php
)
That’s the exact example the RFC uses to motivate the feature, and it works as advertised.
... leaves all remaining parameters open:
$sum3 = fn(int $a, int $b, int $c) => $a + $b + $c;
$plus10 = $sum3(10, ...);
echo $plus10(1, 2), "\n";
13
You can mix placeholders with named arguments, which is great for functions with long parameter lists:
$pad = str_pad(?, 8, pad_type: STR_PAD_LEFT, pad_string: '0');
echo $pad('42'), "\n";
00000042
It works on any callable, including object methods:
class Greeter {
public function greet(string $greeting, string $name): string {
return "$greeting, $name!";
}
}
$hello = (new Greeter)->greet('Hello', ?);
echo $hello('Mahir'), "\n";
Hello, Mahir!
The closure is a normal function, so calling it with too few arguments throws the usual error:
try { $withUkVat(); }
catch (ArgumentCountError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; }
ArgumentCountError: Too few arguments to function {closure:pfa:php86.php:7}(), 0 passed in php86.php on line 28 and exactly 1 expected
The {closure:pfa:...} name tells you the closure came from partial application, which helps when you’re reading a stack trace.
PHP 8.5’s pipe operator |> passes a value into a callable that takes one argument. That was awkward for functions like str_replace(), which need three, so in 8.5 you had to wrap them in arrow functions. With PFA, the chain reads naturally:
$slug = ' PHP 8.6 Is Coming '
|> trim(...)
|> strtolower(...)
|> str_replace(' ', '-', ?)
|> str_replace('.', '', ?);
echo $slug, "\n";
php-86-is-coming
trim(...) and strtolower(...) are first-class callables, which PHP has had since 8.1. The RFC describes first-class callable syntax as the simplest case of PFA. The new part is str_replace(' ', '-', ?). If you’re cleaning user input this way, remember that slugging isn’t the same as escaping; for output, my guide to preventing XSS with htmlspecialchars still applies.
clamp($value, $min, $max) returns the value if it’s within range, otherwise the nearest bound. It replaces the common max($min, min($max, $value)) pattern:
var_dump(clamp(15, 0, 10), clamp(-3, 0, 10), clamp(7, 0, 10), clamp(2.5, 1, 2), clamp('m', 'a', 'f'));
int(10)
int(0)
int(7)
int(2)
string(1) "f"
Two details from that output. First, when the value is out of range, you get the bound itself, including its type: clamp(2.5, 1, 2) returned int(2), not float(2). Pass floats as bounds if you need a float back. Second, clamp() works with any comparable values, including strings.
If $min is bigger than $max, it throws:
try { clamp(5, 10, 1); }
catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; }
ValueError: clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
A practical use is limiting a page number from the query string:
$page = clamp((int) '999', 1, 12);
echo "page $page\n";
page 12
That’s a sensible guard, but still validate input properly; my guide to PHP form validation and sanitisation covers the rest.
New 8.6 features work well together. Here a placeholder turns str_starts_with() into a filter callback, and clamp() into a reusable mapper:
$tags = ['php', 'python', 'phpunit', 'mysql'];
print_r(array_values(array_filter($tags, str_starts_with(?, 'php'))));
$inRange = clamp(?, 1, 100);
print_r(array_map($inRange, [-5, 50, 500]));
Array
(
[0] => php
[1] => phpunit
)
Array
(
[0] => 1
[1] => 50
[2] => 100
)
Use PFA when you’re simply fixing some arguments of an existing function, as in every example above. It’s shorter and keeps the original types. Stick with an arrow function (fn) when the callback does anything more, such as combining two calls, using an if, or transforming the value before passing it on. PFA can only fill in arguments; it can’t add logic.
Readonly properties arrived in PHP 8.1, but until now they couldn’t declare a default value, so you had to set them in the constructor. PHP 8.6 allows a default:
final class Config {
public function __construct(public readonly int $retries = 3) {}
public readonly string $env = 'production';
}
$c = new Config();
echo $c->env, " ", $c->retries, "\n";
try { $c->env = 'dev'; }
catch (Error $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; }
production 3
Error: Cannot modify readonly property Config::$env
The default is applied, and the property is still readonly afterwards. I ran the same class on PHP 8.4.26 for comparison, and it refused to compile: PHP Fatal error: Readonly property C::$env cannot have default value.
PHP 8.6 adds Time\Duration, described in the upgrade notes as a new class from the Duration RFC. It stores a length of time as whole seconds plus nanoseconds, with a negative flag. You create one with named constructors:
$d = Time\Duration::fromMinutes(90);
var_dump($d->seconds, $d->nanoseconds, $d->negative);
$total = Time\Duration::fromHours(1)->add(Time\Duration::fromSeconds(30, 0));
echo $total->seconds, "\n";
$iso = Time\Duration::fromIso8601DurationString('PT1H30M');
echo $iso->seconds, " ", Time\Duration::compare($iso, $d), "\n";
echo Time\Duration::fromMilliseconds(1500)->seconds, " ", Time\Duration::fromMilliseconds(1500)->nanoseconds, "\n";
int(5400)
int(0)
bool(false)
3630
5400 0
1 500000000
90 minutes is 5,400 seconds. One hour plus 30 seconds is 3,630. The ISO 8601 string PT1H30M equals the 90-minute duration, so compare() returns 0. And 1,500 milliseconds is stored as 1 second and 500,000,000 nanoseconds. Other methods on the class in RC3 include fromNanoseconds(), fromMicroseconds(), sub(), multiplyBy(), divideBy(), negate() and absolute().
PHP 8.6 adds a tiny global enum with two cases, SortDirection::Ascending and SortDirection::Descending. The SortDirection RFC explains it’s deliberately unbacked (no built-in string or integer value), so each application maps it to whatever it needs, for example in a query builder:
function orderBy(string $col, SortDirection $dir = SortDirection::Ascending): string {
return "ORDER BY $col " . match ($dir) {
SortDirection::Ascending => 'ASC',
SortDirection::Descending => 'DESC',
};
}
echo orderBy('created_at', SortDirection::Descending), "\n";
ORDER BY created_at DESC
The benefit is type safety: nobody can pass 'DSEC' by mistake. Column names still can’t be bound as parameters, so never pass user input straight into $col; values belong in PDO prepared statements.
The upgrade notes say PHP 8.6 adds “extra info about error location” to json_last_error_msg() and JsonException. Before, you just got “Syntax error”. Now:
json_decode('{"name": "Mahir", "age": }');
echo json_last_error_msg(), "\n";
try { json_decode("{\n \"a\": 1,\n \"b\": \n}", flags: JSON_THROW_ON_ERROR); }
catch (JsonException $e) { echo $e->getMessage(), "\n"; }
Syntax error near location 1:26
Syntax error near location 4:1
The location is line:column. In the first string, column 26 is the } where a value was expected. In the multi-line example, the error is reported at line 4, column 1, where the closing brace appears after the missing value. That’s a big help when debugging a large API response or config file.
PHP 8.3 added the #[\Override] attribute for methods, which makes PHP check that a method really overrides a parent method. PHP 8.6 extends it to class constants, including enum cases:
class Base { const LIMIT = 10; }
class Child extends Base { #[\Override] const LIMIT = 20; }
echo Child::LIMIT, "\n";
20
If someone later removes or renames Base::LIMIT, the attribute turns a silent bug into an error. I tried exactly that, with an empty Base class, and got:
Fatal error: Child::LIMIT has #[\Override] attribute, but no matching parent constant exists
The upgrade notes in the RC3 source list many smaller additions. A few that stood out for everyday web work:
mysqli::quote_string() and mysqli_quote_string().grapheme_strrev() in the Intl extension, for reversing strings with emoji and accents correctly.__debugInfo() allowed on enums.finfo_file() now works with remote streams.let or is as a class, function or constant name, which PHP.Watch lists in the RC3 changelog.Not yet in production. Until 19 November, PHP 8.6 is a release candidate. Good things to do now:
PHP 8.6.0 is scheduled for general availability on 19 November 2026. RC3 came out on 8 October 2026, with RC4 planned for 22 October and RC5 for 5 November.
It’s a PHP 8.6 feature where you call a function with ? or ... in place of some arguments. Instead of running the function, PHP returns a closure that takes the missing arguments, with the same types as the original parameters.
clamp($value, $min, $max) returns $value if it’s between the bounds, otherwise the nearest bound. It throws a ValueError if $min is greater than $max. It’s new in PHP 8.6.
Yes. PHP 8.6 lets you write public readonly string $env = 'production';. The property still can’t be changed after initialisation. On PHP 8.4, which I tested, the same code is a fatal error.
Yes, and that’s one of its main uses. $text |> str_replace(' ', '-', ?) passes $text into the ? position. The pipe operator itself has been available since PHP 8.5.
Not until general availability on 19 November 2026. Release candidates are for testing only. Use them in CI or a local environment to prepare your code.
It’s a new class that represents a length of time as seconds plus nanoseconds. You create it with methods such as fromMinutes() or fromIso8601DurationString(), and combine durations with add(), sub(), multiplyBy() and compare().
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.
Wire Composer PSR-4 autoload in PHP: map App\ to src/App/, require vendor/autoload.php, dump-autoload, and fix the usual class-not-found mistakes.
Use Europe/London in PHP the safe way: date_default_timezone_set, DateTimeImmutable, GMT vs BST, store UTC, display dd/mm/YYYY, and avoid DST surprises.
AGENTS.md is a plain Markdown file in your repo that tells AI coding agents how to build, test and work on your project. What it is, which tools read it, what to put in it, nested files, and a tested template with a script that checks its commands.