Web Development 195 views

PHP 8.6 beta: what’s new for developers

A tour of PHP 8.6.0beta3: partial function application, clamp(), Time\Duration, Io\Poll, safer session defaults, and deprecations — with examples, and a reminder that this is not GA.

PHP 8.6 beta: what’s new for developers

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.


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]. For array_map/array_filter callbacks 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...). P1D and “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 options error_mode, error_store, error_handler. Typed errors instead of a warning somewhere in the log.
  • Faster copies. php_stream_copy_to_stream_ex() uses sendfile, splice, copy_file_range, TransmitFile.
  • Socket context options: keepalive (so_keepalive, tcp_keepidle, …), so_reuseaddr, so_linger, and in beta3 also so_rcvbuf / so_sndbuf.
  • TLS. Session resumption, external PSK, TLS 1.3 0-RTT / early data, WANT_READ/WANT_WRITE status, AES-SIV, and $salt_length for RSA-PSS in openssl_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 = 1
  • session.cookie_httponly = 1
  • session.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 SessionHandler classes 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

ChangeWhy it helps
json_decode error locationColumn/position in the message, not a vague syntax error
SortDirection enumExplicit sort direction instead of magic ints
pack()/unpack() < and >Endianness in the format string
Uri\Rfc3986\UriBuilderURI 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 WebPexif_read_data() is no longer JPEG/TIFF-only
gmp_prevprime(), gmp_powm_sec()Crypto arithmetic without hand-rolled loops
PDO_PGSQL ATTR_CHUNK_SIZEFetch 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 \fForm-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
  • return inside finally
  • Objects in http_build_query() and array_walk() / array_walk_recursive() — convert with get_object_vars() first
  • Objects in zlib/bzip2 filters
  • spl_object_hash() → spl_object_id(); spl_classes() → Reflection
  • metaphone(), strcoll(), SORT_LOCALE_STRING
  • CSV methods on SplFileObject and most convenience methods on ArrayIterator
  • mysqli_get_charset(), mysqli_stmt_init(), instantiating mysqli_stmt without 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_string is 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

  1. Do not ship 8.6 to production yet. Laravel will follow after GA and a green CI matrix.
  2. PFA cleans up collection callbacks, jobs, and pipelines. The 8.5 pipe |> becomes more useful in 8.6.
  3. clamp() replaces homegrown helpers for page size, retry delay, and percentages.
  4. Duration is a good candidate for HTTP, queue, and health-check timeouts once the ecosystem catches up.
  5. Sessions and cookies. If you are not on Laravel’s session layer, re-test login: SameSite=Lax and HttpOnly are now engine defaults.
  6. 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:

  1. Run 8.6 next to 8.4/8.5, not instead of them.
  2. Run the test suite with error_reporting=E_ALL — deprecations show up immediately.
  3. Do not turn JIT/Opcache “for luck” on the first pass: correctness first, speed second.
  4. 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.

Message on Telegram
Cookies This site uses cookies to improve the service and analytics. By continuing, you agree to data processing.