10 September 2026 shipped PHP 8.6.0beta3. This is no longer “an RFC sketch”: the language has partial function application, clamp(), Time\Duration, a dedicated poll API, and a pile of practical fixes in streams, sessions, and crypto.
It is still a beta. Do not put it in production: bugs are still being fixed, and a few API edges may move before GA (target date 19 November 2026). It is already worth reading if you ship Laravel services, queues, integrations, and CLI tools.
- Partial function application
- clamp()
- Time\Duration
- Io\Poll
- Readonly properties with defaults
- Streams, TLS, and I/O errors
- Safer session defaults
- Smaller changes that matter
- Deprecations
- What this means for Laravel
- How to try the beta
1. Partial function application
The headline language feature. You call a function, replace some arguments with ? or ..., and get a Closure instead of a result. It extends first-class callables from PHP 8.1 (strtoupper(...)) and pairs well with the 8.5 pipe operator.
<?php
$greet = str_replace('hello', 'hi', ?);
echo $greet('hello world'); // hi world
$titles = ['hello php', 'hello laravel'];
$mapped = array_map(str_replace('hello', 'hi', ?), $titles);
// ['hi php', 'hi laravel']
Placeholders:
?— exactly one argument filled in later;...— “the remaining arguments as-is”.
<?php
function discount(int $percent, float $price, bool $taxInclusive = true): float
{
$net = $price * (1 - $percent / 100);
return $taxInclusive ? $net : $net * 1.2;
}
$minus20 = discount(20, ...);
$minus20(1000); // 800
$minus20(1000, false); // 960
$onlyPrice = discount(15, ?, false);
$onlyPrice(200); // 204
With |> you can drop a lot of one-off arrow functions:
<?php
$names = [' alice ', 'BOB', 'carol'];
$normalized = $names
|> array_map(trim(...), ?)
|> array_map(strtolower(...), ?);
// ['alice', 'bob', 'carol']
Practice: PFA copies types, defaults, and attributes such as
#[SensitiveParameter]. Forarray_map/array_filtercallbacks it is cleaner than handwritten arrows. If a line is harder to read with?, keep a normal call.
2. clamp()
The helper every codebase reinvented: below the minimum → minimum; above the maximum → maximum.
<?php
clamp(1, 0, 3); // 1
clamp(1, 2, 5); // 2
clamp(4, 1, 3); // 3
$opacity = clamp($request->integer('opacity'), 0, 100);
$retry = clamp($attempt * 250, 250, 5_000);
If $min > $max, or a bound is NAN, PHP throws ValueError. Pagination, percents, rate limits, and UI sliders are the obvious wins.
3. Time\Duration
DateInterval can say “2 months” — and months have different lengths. That is a bad model for timeouts, backoff, and stopwatches. Time\Duration is a readonly “egg-timer” class: fixed seconds + nanoseconds, no timezones, no calendar tricks.
<?php
use Time\Duration;
$timeout = Duration::fromMilliseconds(500);
$backoff = Duration::fromSeconds(1)->multiplyBy(2); // 2 seconds
$limit = Duration::fromIso8601DurationString('PT1H30M');
if (Duration::compare($timeout, $backoff) < 0) {
$wait = $backoff;
}
$poll->wait($timeout);
Factories: fromSeconds(), fromMilliseconds(), fromMicroseconds(), fromNanoseconds(), fromMinutes(), fromHours(), fromIso8601DurationString(). Operations are immutable: add(), sub(), multiplyBy(), divideBy(), negate(), absolute(). Comparisons like < work; + is not overloaded. Overflow throws Time\TimeException.
Note: ISO strings may only contain the time part (
PT...).P1Dand “one month” are rejected on purpose, so a timeout always means the same number of nanoseconds.
4. Io\Poll
stream_select() is still there, but it is classic select(): descriptor caps, linear cost, no epoll/kqueue. PHP 8.6 adds Io\Poll — epoll on Linux, kqueue on BSD/macOS, WSAPoll on Windows, and a poll() fallback.
This is not a full event loop like Revolt or AMPHP. It is a low-level wait across streams. Frameworks can sit on one backend; you can write a small TCP/WebSocket server without ext-uv.
<?php
use Io\Poll\Context;
use Io\Poll\Event;
use Io\Poll\StreamPollHandle;
use Time\Duration;
$poll = new Context();
$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, false);
$poll->add(new StreamPollHandle($server), [Event::Read], ['role' => 'accept']);
while (true) {
foreach ($poll->wait(Duration::fromSeconds(1)) as $watcher) {
if ($watcher->hasTriggered(Event::Read)) {
// accept / fread / fwrite
}
}
}
In beta3, Context::wait() takes a Time\Duration (not the RFC’s early “seconds + microseconds” pair) and rejects an oversized $maxEvents.
5. Readonly properties with defaults
Previously a readonly property could not be given a default unless a constructor assigned it. In 8.6 this is legal:
<?php
final class FeatureFlags
{
public readonly bool $darkMode = false;
public readonly int $rolloutPercent = 0;
}
final readonly class ListQuery
{
public function __construct(
public string $q = '',
public int $page = 1,
public int $perPage = 20,
) {}
}
Fewer empty constructors for DTOs and config objects. Unmodified readonly fields stay locked after clone-with — that is a hardening on top of PHP 8.5, not a new clone syntax.
6. Streams, TLS, and I/O errors
This is “housekeeping that should have landed years ago”, not a toy feature.
- Stream errors API.
StreamException,StreamError, mode/code enums,stream_last_errors()/stream_clear_errors(), and context optionserror_mode,error_store,error_handler. Typed errors instead of a warning somewhere in the log. - Faster copies.
php_stream_copy_to_stream_ex()usessendfile,splice,copy_file_range,TransmitFile. - Socket context options: keepalive (
so_keepalive,tcp_keepidle, …),so_reuseaddr,so_linger, and in beta3 alsoso_rcvbuf/so_sndbuf. - TLS. Session resumption, external PSK, TLS 1.3 0-RTT / early data,
WANT_READ/WANT_WRITEstatus, AES-SIV, and$salt_lengthfor RSA-PSS inopenssl_sign()/openssl_verify().
<?php
$ctx = stream_context_create([
'socket' => [
'so_keepalive' => true,
'tcp_keepidle' => 60,
'so_rcvbuf' => 262144,
'so_sndbuf' => 262144,
],
'ssl' => [
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT,
],
]);
7. Safer session defaults
Defaults that should have been on years ago:
session.use_strict_mode = 1session.cookie_httponly = 1session.cookie_samesite = Lax
Also: SessionHandler::validateId() is implemented, so strict mode applies to the built-in handler. Session objects without create_sid()/validateId() are deprecated. Invalid SameSite, lifetime, and NULs in path/domain now warn.
Laravel already sets HttpOnly/SameSite in most apps. Check custom
SessionHandlerclasses and plain-PHP legacy: the new engine defaults may quietly fix cookies — or break an odd cross-site login that relied on an empty SameSite.
8. Smaller changes that matter
| Change | Why it helps |
|---|---|
json_decode error location | Column/position in the message, not a vague syntax error |
SortDirection enum | Explicit sort direction instead of magic ints |
pack()/unpack() < and > | Endianness in the format string |
Uri\Rfc3986\UriBuilder | URI building on top of the 8.5 API, plus host/type helpers |
grapheme_strrev() | Reverse by grapheme, not by byte |
IntlNumberRangeFormatter | “1–3 kg”, “€10–20” with locale rules |
mysqli_quote_string() | Quoting without the old real_escape ritual |
ZipArchive::openString() | Open a ZIP from an in-memory string |
| EXIF from WebP | exif_read_data() is no longer JPEG/TIFF-only |
gmp_prevprime(), gmp_powm_sec() | Crypto arithmetic without hand-rolled loops |
PDO_PGSQL ATTR_CHUNK_SIZE | Fetch large result sets in chunks |
| libsodium KEMs (X-Wing, ML-KEM768) | Post-quantum key exchange if built with libsodium 1.0.22 |
trim() also strips \f | Form-feed is now in the default charset |
Faster array_intersect() | A free win on large arrays |
<?php
try {
json_decode('{ "ok": true, }', flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
// 8.6 includes the error location — easier to spot a trailing comma
logger()->warning($e->getMessage());
}
$binary = pack('n<', 0x1234); // little-endian 16-bit
[$value] = unpack('n<', $binary);
9. Deprecations
8.6 keeps cleaning 1990s aliases and awkward overloads. Warnings in beta, removal candidates for 9.0.
is_long(),is_integer()→is_int();is_double()→is_float();doubleval()→floatval()- The third argument to
define()(case-insensitive constants) - Naming a function
readonly returninsidefinally- Objects in
http_build_query()andarray_walk()/array_walk_recursive()— convert withget_object_vars()first - Objects in zlib/bzip2 filters
spl_object_hash()→spl_object_id();spl_classes()→ Reflectionmetaphone(),strcoll(),SORT_LOCALE_STRING- CSV methods on
SplFileObjectand most convenience methods onArrayIterator mysqli_get_charset(),mysqli_stmt_init(), instantiatingmysqli_stmtwithout SQL- mbregex; objects in
mb_convert_variables() - Nullable return type on
__debugInfo(); returning a value from__construct()/__destruct() is_a()/is_subclass_of()with a string when$allow_stringis false
Using more than 16 filters in a php://filter URL without filter.max_filter_count is also deprecated. If you built a homemade pipeline of convert.base64-* layers, check it.
10. What this means for Laravel
- Do not ship 8.6 to production yet. Laravel will follow after GA and a green CI matrix.
- PFA cleans up collection callbacks, jobs, and pipelines. The 8.5 pipe
|>becomes more useful in 8.6. - clamp() replaces homegrown helpers for page size, retry delay, and percentages.
- Duration is a good candidate for HTTP, queue, and health-check timeouts once the ecosystem catches up.
- Sessions and cookies. If you are not on Laravel’s session layer, re-test login: SameSite=Lax and HttpOnly are now engine defaults.
- Deprecations. Hunt
is_integer,http_build_query($dto), custom session handlers, and Spl iterators in legacy packages.
<?php
$page = clamp((int) $request->query('per_page', 20), 1, 100);
$clean = $request->collect('tags')
->map(trim(...))
->filter(filled(...))
->values();
11. How to try the beta
# Official QA builds
https://www.php.net/pre-release-builds.php
https://downloads.php.net/~svpernova09/php-8.6.0beta3.tar.xz
git clone https://github.com/php/php-src.git --depth 1 --branch php-8.6.0beta3
# Docker (verify the tag)
docker run --rm php:8.6-rc php -v
Minimum checklist:
- Run 8.6 next to 8.4/8.5, not instead of them.
- Run the test suite with
error_reporting=E_ALL— deprecations show up immediately. - Do not turn JIT/Opcache “for luck” on the first pass: correctness first, speed second.
- Read NEWS between beta2 and beta3: plenty of security fixes (Phar UAF, SOAP WSDL, streams, SimpleXML).
Takeaway
PHP 8.6 beta3 is not a cosmetic minor. Partial application makes callbacks humane, clamp() and Time\Duration close holes everyone patched with helpers, the poll API and stream errors move I/O closer to what Go/Node have had for years, and the new session defaults leave fewer foot-guns enabled out of the box.
Treat it as a sandbox until GA. If you need a readiness audit or a Laravel migration plan, write to me via Telegram @rootseo or the contact page.
Comments
Comments appear after moderation.
No published comments yet. Be the first.