scrolltotop

new Get your brand recommended by ChatGPT, Perplexity & AI search engines. Discover GEO.

new We help companies keep track of engineering health with monthly reports. Start your review today.

What's New in PHP 8.6

Home Company Blog What's New in PHP 8.6
What's New in PHP 8.6

PHP 8.6 is taking shape, and it already includes several changes that can make everyday PHP code cleaner, safer, and easier to operate.

PHP 8.6.0 Alpha 3 is currently available for testing. Beta 1 is planned for August 13, 2026, the first release candidate is planned for September 24, and the final release is currently planned for November 19, 2026. The schedule may still change, and Alpha builds are not suitable for production. They exist so developers, framework maintainers, and extension authors can test the release and report problems early.

That early-testing window is exactly the part we care about at Moneo. Maintaining and modernizing production PHP applications is a big part of our engineering work, so every new PHP release goes through the same quiet routine here: read the RFCs as they land, run the alpha builds against real codebases, and plan calm upgrades months before release day. This guide is the written-up version of that homework.

PHP 8.6 is not built around one huge language change. It is better understood as a sharpening pass over the entire platform, and there is a common thread running through almost everything in it: fewer silent failures, clearer intent. The language gains syntax that says what the code means. The runtime replaces warning-and-false patterns with structured errors and safer defaults. The bundled extensions collect dozens of small upgrades that each remove a workaround somewhere. And a set of deprecations and stricter checks turns quiet misbehavior into loud, fixable errors.

This article follows that arc: where 8.6 sits in the release calendar, what changes in the language, the runtime, and the libraries, what might break in existing projects, and how to test all of it with Docker today.

This article is based on the official PHP 8.6 upgrade notes, accepted RFCs, and the PHP 8.6 release schedule available on August 6, 2026. More changes can still be merged before the feature freeze.

Where PHP 8.6 Lands

Before looking at any feature, it helps to answer two questions: when will 8.6 actually arrive, and what happens to the versions you are running right now? Together they decide how urgent this release is for your team.

The release schedule

The current PHP 8.6 schedule is:

Date Release or milestone
July 2, 2026 Alpha 1
July 16, 2026 Alpha 2
July 30, 2026 Alpha 3
August 11, 2026 RFC merge deadline for Beta 1
August 13, 2026 Beta 1 and soft feature freeze
August 27, 2026 Beta 2
September 10, 2026 Beta 3
September 22, 2026 Hard feature freeze
September 24, 2026 Release Candidate 1
October 8, 2026 Release Candidate 2
October 22, 2026 Release Candidate 3
November 5, 2026 Release Candidate 4
November 19, 2026 General availability

Source: PHP 8.6 preparation and release timetable

Which PHP versions are supported today?

Each stable PHP branch receives two years of active support after its first stable release. During active support, regular bug fixes and security fixes are released. After active support ends, the branch receives two more years of support for critical security issues only. Once this period ends, the branch reaches end of life and no longer receives official fixes.

As of August 6, 2026, the officially supported PHP versions are:

PHP version Current status Active support until Security support until
PHP 8.2 Security fixes only December 31, 2024 December 31, 2026
PHP 8.3 Security fixes only December 31, 2025 December 31, 2027
PHP 8.4 Active support December 31, 2026 December 31, 2028
PHP 8.5 Active support December 31, 2027 December 31, 2029

Source: PHP supported versions

What this means for development teams

PHP 8.2 is still supported, but only for critical security issues, and that support ends on December 31, 2026. Teams that still run PHP 8.2 should already have an upgrade plan.

PHP 8.3 is also in security-only support. It is not immediately end of life, but it no longer receives normal bug fixes.

PHP 8.4 and PHP 8.5 are the current actively supported branches. For a new production project, PHP 8.5 is usually the best default when the required frameworks, Composer packages, operating system packages, and extensions support it.

PHP 8.6 does not appear in the supported versions table yet because it is still in development. Its official support period will start after the stable release.

The practical recommendation is simple:

  • Use a supported stable version in production.
  • Test PHP 8.6 locally and in CI before the stable release.
  • Do not deploy PHP 8.6 Alpha builds to production.
  • Do not stay on an end-of-life PHP version simply because the application still appears to work.

In short: production stays on a supported stable branch, and 8.6 is something you test on the side. The rest of this article is about what you will find when you do — starting with the code itself.

The Language: Code That Says What It Means

Only two headline additions land at the syntax level this time, but both target the same daily annoyance: code that spends more characters on ceremony than on intent. One shortens the callbacks you write every day, the other gives a name to a pattern you have been writing by hand for years.

Partial Function Application

Partial Function Application is the most visible language change in PHP 8.6.

It allows a function or method call to leave one or more arguments open. PHP then returns a Closure instead of immediately calling the function.

A ? placeholder represents one missing argument. An ... placeholder represents the remaining arguments.

RFC: Partial Function Application v2

Before PHP 8.6

Imagine that we want to replace a common greeting in a list of messages:

$messages = [
    'hello John',
    'hello Sarah',
    'hello Michael',
];

$result = array_map(
    static fn (string $message): string =>
        str_replace('hello', 'hi', $message),
    $messages,
);

This is valid code, but the arrow function mostly repeats information that already exists in str_replace().

With PHP 8.6

$messages = [
    'hello John',
    'hello Sarah',
    'hello Michael',
];

$replaceGreeting = str_replace('hello', 'hi', ?);

$result = array_map($replaceGreeting, $messages);

This expression does not call str_replace() immediately:

$replaceGreeting = str_replace('hello', 'hi', ?);

It creates a Closure that waits for the final argument.

The result is logically similar to this:

$replaceGreeting = static fn (string $value): string =>
    str_replace('hello', 'hi', $value);

The important difference is that PHP creates the closure signature from the original function. Parameter names, types, optional parameters, default values, references, and return type information can be preserved.

Real-world example: fixing a VAT rate

Consider an e-commerce pricing function:

function calculatePrice(
    float $netPrice,
    float $vatRate,
    float $discountRate = 0,
): float {
    $discountedPrice = $netPrice * (1 - $discountRate);

    return $discountedPrice * (1 + $vatRate);
}

A project may always use the same VAT rate in one country:

$addTurkishVat = calculatePrice(
    ?,
    vatRate: 0.20,
);

$prices = array_map($addTurkishVat, [100, 250, 500]);

The function now clearly communicates that the VAT rate is fixed and the net price will be supplied later.

Real-world example: creating invoice lines

function createInvoiceLine(
    string $product,
    int $quantity,
    float $unitPrice,
): array {
    return [
        'product' => $product,
        'quantity' => $quantity,
        'unit_price' => $unitPrice,
        'total' => $quantity * $unitPrice,
    ];
}

$createHostingLine = createInvoiceLine(
    'Cloud Hosting',
    ?,
    ?,
);

$line = $createHostingLine(
    quantity: 12,
    unitPrice: 49.90,
);

Two values are left open. The returned closure expects them in the same order.

Using the remaining-arguments placeholder

The ... placeholder means that the remaining arguments will be provided later:

function sendNotification(
    string $channel,
    string $recipient,
    string $message,
    bool $urgent = false,
): void {
    // Send the notification.
}

$sendEmail = sendNotification('email', ...);

$sendEmail(
    recipient: 'customer@example.com',
    message: 'Your order has been shipped.',
);

Delaying a complete function call

When every value is already provided but ... is still used, PHP creates a zero-argument closure. This can be useful when a job should be prepared now and executed later.

function generateReport(string $type, int $year): string
{
    return "Generating {$type} report for {$year}";
}

$job = generateReport('revenue', 2026, ...);

// The function runs here.
$result = $job();

Important details

Partial Function Application does not work directly with new expressions:

// Not supported.
$userFactory = new User(?, ?);

A static factory method works:

$userFactory = User::create(?, ?);

Fixed argument expressions are evaluated when the partial closure is created, not when it is later called. This matters when an argument performs work or reads changing state.

$sendLater = sendMessage(
    recipient: ?,
    requestId: createRequestId(),
);

Here, createRequestId() runs when $sendLater is created.

Partial Function Application should be useful in collection operations, validation pipelines, event handlers, middleware, dependency configuration, and callback-heavy framework code.

A native clamp()

The second language-level addition is smaller but will probably appear in more codebases: a native clamp() function.

RFC: clamp()

Its signature is:

clamp(mixed $value, mixed $min, mixed $max): mixed

The function returns the value when it is inside the range. When it is below the minimum, the minimum is returned. When it is above the maximum, the maximum is returned.

clamp(50, 0, 100);   // 50
clamp(-10, 0, 100);  // 0
clamp(150, 0, 100);  // 100

Before PHP 8.6, this pattern was often written as:

$value = max($minimum, min($maximum, $value));

The old form works, but clamp() says exactly what the code is trying to do.

Real-world example: API pagination

An API should not allow clients to request an unlimited number of records:

$requestedLimit = (int) ($_GET['limit'] ?? 20);

$limit = clamp(
    value: $requestedLimit,
    min: 1,
    max: 100,
);

A request for 5,000 records becomes 100. A request for zero becomes 1.

Real-world example: upload percentage

$uploadedBytes = 750;
$totalBytes = 500;

$percentage = clamp(
    value: ($uploadedBytes / $totalBytes) * 100,
    min: 0,
    max: 100,
);

echo $percentage; // 100

This protects the UI when an external source reports unexpected numbers.

Real-world example: retry delay

$attempt = 8;

$retryDelay = clamp(
    value: 2 ** $attempt,
    min: 1,
    max: 60,
);

sleep($retryDelay);

The retry delay can grow after each failure, but it never becomes greater than 60 seconds.

Real-world example: restricting a date

clamp() accepts comparable values, not only integers and floats.

$requestedDate = new DateTimeImmutable('2026-10-20');
$campaignStart = new DateTimeImmutable('2026-11-01');
$campaignEnd = new DateTimeImmutable('2026-11-30');

$effectiveDate = clamp(
    $requestedDate,
    $campaignStart,
    $campaignEnd,
);

echo $effectiveDate->format('Y-m-d'); // 2026-11-01

A ValueError is thrown when the minimum is greater than the maximum. A ValueError is also thrown when the minimum or maximum is NAN.

clamp(10, 100, 20);

// ValueError

One compatibility detail is worth noting. Projects that already define a global userland function named clamp() will have a naming conflict after upgrading.

The Runtime: Streams, Sessions, and Sockets

The language changes are the part you will see in code reviews. The next group lives a level lower, in the plumbing everything else stands on: how PHP waits on I/O, how it reports stream failures, how it protects sessions, and how it speaks TLS. Most of it exists so that infrastructure code can fail loudly and precisely instead of quietly and vaguely — which is exactly where PHP has historically been weakest.

A modern I/O polling API

PHP 8.6 adds a native polling API under the Io\Poll namespace.

RFC: Polling API

I/O polling allows a program to monitor many sockets, streams, pipes, or other resources and wait until one of them is ready for reading or writing.

PHP already has stream_select(), but it is based on the older select() system call and has scaling limitations. The new API can use modern operating system backends:

  • epoll on Linux
  • kqueue on macOS and BSD
  • Event Ports on Solaris and illumos
  • WSAPoll on Windows
  • poll as a fallback on other POSIX systems

The backend is selected automatically unless the application requests a specific one.

What it is, and what it is not

This is not native async and await syntax.

It is a low-level polling layer. Frameworks and libraries such as event loops, WebSocket servers, message consumers, and application servers can build higher-level asynchronous APIs on top of it.

Most Laravel, Symfony, or WordPress applications will not call this API directly. Its largest effect may arrive through infrastructure libraries.

Basic server example

The following example follows the API described in the RFC:

use Io\Poll\Context;
use Io\Poll\Event;

$poll = new Context();

$server = stream_socket_server(
    'tcp://0.0.0.0:8080',
    $errorCode,
    $errorMessage,
);

if ($server === false) {
    throw new RuntimeException($errorMessage, $errorCode);
}

stream_set_blocking($server, false);

$serverHandle = new StreamPollHandle($server);

$poll->add(
    $serverHandle,
    [Event::Read],
    ['type' => 'server'],
);

while (true) {
    $watchers = $poll->wait(timeoutSeconds: 1);

    foreach ($watchers as $watcher) {
        if (!$watcher->hasTriggered(Event::Read)) {
            continue;
        }

        $handle = $watcher->getHandle();

        if (!$handle instanceof StreamPollHandle) {
            continue;
        }

        $client = stream_socket_accept(
            $handle->getStream(),
            timeout: 0,
        );

        if ($client === false) {
            continue;
        }

        fwrite($client, "Hello from PHP 8.6\n");
        fclose($client);
    }
}

A polling context can also report which backend it selected:

$poll = new Io\Poll\Context();

printf(
    "Polling backend: %s\n",
    $poll->getBackend()->name,
);

The API also supports one-shot watchers and edge-triggered events on platforms that provide them.

Structured stream error handling

Where the polling API is about waiting on I/O, this change is about what happens when I/O goes wrong — and it is the clearest example of the "fewer silent failures" theme in the whole release.

PHP stream errors have traditionally been awkward to handle. A function such as fopen() usually emits a warning and returns false:

$file = fopen('/missing/report.csv', 'r');

Many projects suppress the warning:

$file = @fopen('/missing/report.csv', 'r');

This hides useful information. Another approach is to temporarily install a PHP error handler, which adds complexity and may catch unrelated warnings.

PHP 8.6 introduces structured stream errors.

RFC: Stream Error Handling Improvements

The new API includes:

  • StreamError
  • StreamException
  • StreamErrorCode
  • StreamErrorMode
  • StreamErrorStore
  • stream_last_errors()
  • stream_clear_errors()

The available reporting modes are:

StreamErrorMode::Error;
StreamErrorMode::Exception;
StreamErrorMode::Silent;

The default remains StreamErrorMode::Error, which preserves the traditional warning-based behavior.

Real-world example: importing a file with exceptions

$context = stream_context_create([
    'stream' => [
        'error_mode' => StreamErrorMode::Exception,
    ],
]);

try {
    $file = fopen(
        '/imports/customers.csv',
        'r',
        false,
        $context,
    );

    while (($row = fgetcsv($file)) !== false) {
        // Import customer data.
    }

    fclose($file);
} catch (StreamException $exception) {
    foreach ($exception->getErrors() as $error) {
        error_log(sprintf(
            'Stream error [%s]: %s',
            $error->code->name,
            $error->message,
        ));
    }
}

A StreamError contains structured fields such as:

$error->code;
$error->message;
$error->wrapperName;
$error->severity;
$error->terminating;
$error->param;

This makes it possible to treat a missing file differently from a permission error without parsing human-readable warning strings.

try {
    $file = fopen(
        '/imports/customers.csv',
        'r',
        false,
        $context,
    );
} catch (StreamException $exception) {
    $error = $exception->getErrors()[0] ?? null;

    if ($error?->code === StreamErrorCode::NotFound) {
        echo 'The import file has not arrived yet.';
    } elseif ($error?->code === StreamErrorCode::PermissionDenied) {
        echo 'The import directory permissions are incorrect.';
    } else {
        echo 'The import could not be started.';
    }
}

Error settings must be passed through an explicit stream context. They cannot be applied globally through stream_context_set_default(). This restriction protects libraries that depend on the existing warning behavior.

Several stream functions also gain optional context parameters, including stream_select(), stream_copy_to_stream(), stream_socket_pair(), and stream_is_local().

Safer session defaults

The same defensive mindset reaches session handling, and this is the change most likely to require action in ordinary business applications — not because the features are complex, but because the defaults change underneath you.

PHP 8.6 changes three default session settings:

session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax

RFC: Secure Session Defaults

Strict session mode

session.use_strict_mode = 1

Strict mode rejects session IDs that were supplied by the client but were never initialized by the server. This reduces the risk of session fixation.

Custom session handlers that accept externally supplied IDs may need to implement validateId() and create_sid(), or explicitly turn strict mode off after a careful security review.

HttpOnly session cookies

session.cookie_httponly = 1

The browser will no longer allow JavaScript to read the PHP session cookie through document.cookie.

The browser still sends the cookie with valid requests. JavaScript simply cannot read its value.

Applications should not use the PHP session cookie as a client-side JavaScript token.

SameSite Lax

session.cookie_samesite = Lax

Browsers will usually not send the session cookie on a cross-site POST request. This provides additional protection against some cross-site request attacks.

However, this change may affect:

  • SAML login flows
  • Cross-domain authentication
  • Embedded applications
  • Legacy payment return flows
  • Cross-site form submissions
  • Applications running inside third-party iframes

An application that truly requires a cross-site session cookie may need:

session.cookie_samesite = None
session.cookie_secure = 1

Do not change this blindly. Test the complete login, payment, SSO, and callback flow first.

How to inspect the defaults

var_dump([
    'strict_mode' => ini_get('session.use_strict_mode'),
    'httponly' => ini_get('session.cookie_httponly'),
    'samesite' => ini_get('session.cookie_samesite'),
]);

For many business applications, the session changes may create more migration work than the new language syntax. Authentication and cross-domain flows should be part of every PHP 8.6 test plan.

Better TLS and networking support

Rounding out the runtime work, PHP 8.6 adds several lower-level TLS and stream improvements. These matter mostly to long-lived services, gateways, and libraries that manage their own connections.

TLS session resumption

RFC: TLS session resumption

New stream context options allow applications and libraries to save and restore TLS sessions, manage server-side session storage, and control session cache behavior.

This may reduce connection setup overhead for systems that repeatedly connect to the same secure services.

External pre-shared keys

OpenSSL streams gain support for external TLS pre-shared keys through new client and server callbacks.

This is mainly relevant to specialized service-to-service or device communication systems.

TLS 1.3 early data

TLS 1.3 early data, also called 0-RTT, is supported for streams.

It can reduce latency by allowing a client to send some data before the full handshake completes. However, early data can be replayed. It should be used only for operations that are safe to repeat, such as certain idempotent reads.

Do not use 0-RTT for operations such as creating payments, confirming orders, changing passwords, or other actions that must never be replayed.

Socket keepalive and connection options

New stream context options include:

so_reuseaddr
so_keepalive
tcp_keepidle
tcp_keepintvl
tcp_keepcnt
so_linger

These options provide more control over long-lived TCP connections, workers, gateways, and real-time services.

PHP 8.6 also adds stream_socket_get_crypto_status() and new crypto status constants.

Developer Experience: Help for the Bad Days

Not every improvement needs an architecture diagram. The next batch shares a humbler goal: when something goes wrong — an invalid webhook payload, a property that refuses to write, a constant that silently stopped overriding anything — PHP 8.6 gives you a better answer, faster.

Better JSON error locations

PHP 8.6 adds more location information to JSON parsing errors returned by:

json_last_error_msg();

and to the message inside:

JsonException;

Source: PHP 8.6 upgrade notes

Consider debugging a webhook payload:

$payload = <<<'JSON'
{
    "customer": {
        "name": "John",
        "email": "john@example.com",
    }
}
JSON;

try {
    $data = json_decode(
        $payload,
        true,
        flags: JSON_THROW_ON_ERROR,
    );
} catch (JsonException $exception) {
    error_log(
        'Invalid webhook payload: ' .
        $exception->getMessage()
    );
}

The payload contains an invalid trailing comma. Older PHP versions generally report only a syntax error. PHP 8.6 includes more information about where the parser found the problem.

This is a small change, but it can save time when debugging large API payloads, webhook requests, configuration files, or imported JSON documents.

Better Reflection APIs

PHP 8.6 adds several Reflection improvements aimed at the tools that inspect your code: serializers, mappers, containers, and documentation generators.

ReflectionProperty::isReadable() and isWritable()

RFC: Reflection property readability and writability

A public property is not always writable. It may be readonly, use asymmetric visibility, have property hooks, or already be initialized in a way that blocks another write.

The new methods are:

ReflectionProperty::isReadable(
    ?string $scope,
    ?object $object = null,
): bool;

ReflectionProperty::isWritable(
    ?string $scope,
    ?object $object = null,
): bool;

A serializer is the natural use case:

final class CustomerData
{
    public function __construct(
        public readonly string $id,
        public string $name,
    ) {
    }
}

$customer = new CustomerData(
    id: 'customer-100',
    name: 'John',
);

$property = new ReflectionProperty(
    CustomerData::class,
    'id',
);

var_dump(
    $property->isReadable(
        scope: null,
        object: $customer,
    ),
);

var_dump(
    $property->isWritable(
        scope: null,
        object: $customer,
    ),
);

This is useful for serializers, object mappers, ORMs, dependency injection containers, admin panels, and debugging tools.

The methods cannot guarantee that every read or write will succeed. A property hook can still throw an exception based on application logic. However, they provide a much better answer than checking only isPublic().

Parameter-level DocComments

RFC: Parameter DocComments

PHP 8.6 allows a DocComment to be attached directly to a parameter:

function exportOrders(
    /** Customer account identifier */
    string $accountId,

    /** Maximum number of orders */
    int $limit = 100,
): array {
    return [];
}

It can be read with:

$function = new ReflectionFunction('exportOrders');

foreach ($function->getParameters() as $parameter) {
    printf(
        "%s: %s\n",
        $parameter->getName(),
        $parameter->getDocComment() ?: 'No documentation',
    );
}

This can help API documentation generators, CLI frameworks, form builders, and internal development tools.

More Reflection namespace helpers

PHP 8.6 also adds:

ReflectionConstant::inNamespace();
ReflectionAttribute::inNamespace();
ReflectionAttribute::getNamespaceName();
ReflectionAttribute::getShortName();

These methods reduce manual string parsing when tools inspect constants and attributes.

#[\Override] for class constants and enum cases

The #[\Override] attribute can now be applied to class constants, including enum cases.

RFC: #[\Override] for class constants

class BaseApiConfig
{
    public const TIMEOUT = 30;
}

class PaymentApiConfig extends BaseApiConfig
{
    #[\Override]
    public const TIMEOUT = 10;
}

PHP can verify that the constant really overrides a constant from a parent class or interface.

This catches mistakes such as:

class PaymentApiConfig extends BaseApiConfig
{
    #[\Override]
    public const TIMEOUT_SECONDS = 10;
}

Because the parent does not define TIMEOUT_SECONDS, PHP reports an error.

The benefit is the same as using #[\Override] on methods and properties. It documents intent and allows the engine to catch accidental renames.

__debugInfo() for enums

Enums can define the __debugInfo() magic method in PHP 8.6.

RFC: Debuggable Enums

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Completed = 'completed';
    case Failed = 'failed';

    public function __debugInfo(): array
    {
        return [
            'name' => $this->name,
            'value' => $this->value,
            'is_final' => $this === self::Completed,
        ];
    }
}

var_dump(PaymentStatus::Completed);

This allows applications to show more useful enum information in debug output.

PHP 8.6 also deprecates declaring __debugInfo() with ?array or array|null as its return type. Use array instead.

mysqli::quote_string()

PHP 8.6 adds:

mysqli::quote_string();
mysqli_quote_string();

RFC: MySQLi quote string

real_escape_string() escapes the content, but the developer must still add SQL quotes:

$value = $mysqli->real_escape_string($name);

$sql = "SELECT * FROM customers WHERE name = '$value'";

The new method both escapes the value and adds the surrounding quotes:

$sql = sprintf(
    'SELECT * FROM customers WHERE name = %s',
    $mysqli->quote_string($name),
);

Prepared statements are still the preferred solution:

$statement = $mysqli->prepare(
    'SELECT * FROM customers WHERE name = ?'
);

$statement->bind_param('s', $name);
$statement->execute();

quote_string() is mainly useful for database tools, SQL generators, migrations, and legacy systems that must create complete SQL strings.

The Standard Library Keeps Pace

The same sharpening pass continues through the bundled extensions. None of these will headline a conference talk, but each one removes a workaround, a temporary file, or a hand-rolled parser somewhere in a real codebase.

Intl and Unicode improvements

PHP 8.6 expands the Intl extension with several additions.

IntlNumberRangeFormatter

The new IntlNumberRangeFormatter class formats a range of two numbers according to a locale and formatting skeleton.

This can be useful for prices, measurements, analytics, and reporting interfaces.

$formatter = new IntlNumberRangeFormatter(
    locale: 'en-US',
    skeleton: 'currency/USD',
);

$result = $formatter->formatRange(100, 250);

The exact output depends on the locale and ICU version.

grapheme_strrev()

RFC: grapheme_strrev()

Normal byte-based string reversal can break Unicode characters, combining marks, and emoji sequences. grapheme_strrev() reverses text by visible grapheme clusters.

$reversed = grapheme_strrev('A🙂B');

This is more appropriate for user-visible international text than reversing raw bytes.

Spoof checking

New SpoofChecker methods include:

SpoofChecker::areBidiConfusable();
SpoofChecker::getBidiSkeleton();
SpoofChecker::getSkeleton();

These functions can help detect visually confusing or directionally confusing strings. They may be useful in username systems, domain tools, security products, and moderation systems.

URI improvements

PHP 8.6 extends the URI APIs added in earlier PHP 8 releases.

New functionality includes:

Uri\Rfc3986\Uri::getUriType();
Uri\WhatWg\Url::isSpecialScheme();
Uri\Rfc3986\Uri::getHostType();
Uri\WhatWg\Url::getHostType();
Uri\Rfc3986\UriBuilder;

RFC: URI follow-up improvements

Applications often build URLs with string concatenation:

$url = $baseUrl . '/orders?' . http_build_query($query);

This can become fragile when the base URL already contains a path, query, fragment, port, or encoded characters.

Uri\Rfc3986\UriBuilder provides a structured approach to building RFC 3986 URIs. It can reduce manual parsing and encoding mistakes in HTTP clients, SDKs, gateways, and routing tools.

Host type detection is also useful when an application needs to distinguish IPv4, IPv6, registered names, or other host forms before applying security or routing rules.

ZIP archives in memory

PHP 8.6 adds:

ZipArchive::openString();
ZipArchive::closeString();

Source: PHP 8.6 upgrade notes

These methods can reduce the need to create temporary files when an application receives or generates a ZIP archive in memory.

Possible use cases include:

  • Creating downloadable report bundles
  • Reading archives received from object storage
  • Processing uploaded ZIP files
  • Building export packages in workers
  • Testing archive generation without writing permanent files

The exact memory usage still matters. Very large archives should not automatically be kept entirely in memory.

cURL, Fileinfo, GMP, and other additions

The official PHP 8.6 upgrade notes include many smaller additions.

cURL

curl_getinfo() can include size_delivered, which reports the number of bytes passed to the download callback. This requires libcurl 8.20.0 or later.

CURLOPT_SEEKFUNCTION allows libcurl to rewind and resend a streamed request body when required by redirects, authentication, or connection retries.

Fileinfo

finfo_file() can work with remote streams.

This can be useful for validating remote objects, custom stream wrappers, or files stored in object storage. Applications should still consider size limits and network timeouts before reading remote content.

GMP

New functions include:

gmp_powm_sec();
gmp_prevprime();

gmp_powm_sec() provides side-channel-quiet modular exponentiation. gmp_prevprime() returns the largest prime smaller than a given number when supported by the installed GNU MP version.

Socket address lookup

socket_addrinfo_lookup() gains an optional error output argument, making failures easier to inspect programmatically.

Built-in INI defaults

ini_get_all() now includes a builtin_default_value entry when detailed output is requested. This makes it easier for diagnostics and configuration tools to distinguish PHP's built-in default from values applied through php.ini, the command line, or runtime configuration.

Performance

All of these features would be a harder sell if they arrived with a slowdown. They do not. PHP 8.6 carries its own collection of runtime and library optimizations, and several of them connect directly to the new syntax above.

Source: PHP 8.6 upgrade notes

array_map() callback optimization

When array_map() uses a first-class callable or Partial Function Application callback, PHP may compile it into code similar to a foreach loop.

$normalize = trim(?);

$values = array_map($normalize, $values);

This can avoid intermediate closure creation, reduce callback overhead, and give the JIT more information. In other words, the cleaner Partial Function Application style is not just shorter — it can also be the faster path.

Other core improvements

PHP 8.6 also improves:

  • Argument passing to known constructors
  • The TAILCALL virtual machine
  • Thread-safe builds
  • Some simple printf() calls using %s and %d

Simple printf() calls may be compiled into string interpolation, avoiding a full function call and repeated format parsing.

Standard library improvements

Performance work includes:

  • array_fill_keys()
  • array_map() with multiple arrays
  • array_sum() and array_product() for integer-only arrays
  • array_unshift()
  • array_walk()
  • str_split()
  • Binary-string parsing in intval()

JSON, URI, DOM, ZIP, and Intl

There are also improvements for:

  • JSON encoding of arrays and objects
  • Pretty-print indentation in json_encode()
  • DOM splitText()
  • URI parsing and normalized getters
  • ZIP operations
  • Several Intl functions that return arrays

What to expect

Most normal applications should expect incremental improvements, not a dramatic universal speed increase.

Do not publish production benchmark claims based on an Alpha build. Reliable comparisons should use the same application, configuration, extensions, workload, machine, and database, preferably with a release candidate or final build.

What Might Break

So far this has been the gift list. Every release also comes with a bill, and PHP 8.6 presents it in two forms: deprecations you should plan around, and stricter runtime behavior that turns previously silent problems into visible ones. It is the same "fewer silent failures" philosophy — this time applied to your legacy code.

Deprecations to plan for

Mbregex is deprecated

RFC: End of life for Oniguruma and mbregex

The mbregex part of the Mbstring extension is deprecated because its underlying Oniguruma library is no longer maintained.

Search older projects for functions such as:

mb_ereg
mb_eregi
mb_ereg_replace
mb_eregi_replace
mb_split
mb_regex_encoding

Projects should migrate to PCRE-based preg_* functions where possible.

Do not perform a blind text replacement. Mbregex and PCRE may have different syntax or behavior for some patterns. Add tests before changing regular expressions.

Constructor and destructor return values

Returning a value from __construct() or __destruct() is deprecated:

class Report
{
    public function __construct()
    {
        return true;
    }
}

Making a constructor or destructor a generator is also deprecated.

RFC: Deprecate return values from constructors

These patterns are unusual, but they may exist in old code, generated code, or heavily dynamic frameworks.

Long php://filter chains

Using more than 16 filters in a php://filter URL without an explicit configuration now emits an E_DEPRECATED warning.

RFC: Limit the maximum number of filters in a chain

Applications that intentionally use a large filter chain can configure filter.max_filter_count in the stream context or move to stream_filter_append().

This change also provides security hardening against abusive filter chains.

Nullable __debugInfo() return types

Declaring __debugInfo() with ?array or array|null is deprecated. Use array.

Stricter runtime behavior

Beyond deprecations, PHP 8.6 makes many invalid operations fail more clearly. This is generally good, but old applications may depend on values being silently converted, truncated, or ignored.

More TypeError and ValueError exceptions

Stricter checks affect functions in areas such as:

  • cURL
  • DOM
  • GD
  • GMP
  • Intl
  • PCNTL
  • PCRE
  • Phar
  • POSIX
  • Sessions
  • SOAP
  • Sodium
  • SPL
  • ZIP
  • Zlib
  • Standard file and string functions

Examples include invalid enum-like integer flags, NUL bytes in paths or environment values, out-of-range numeric arguments, malformed option arrays, and invalid callback return values.

preg_grep() behavior

preg_grep() now returns false instead of a partial array when a PCRE execution error occurs, such as malformed UTF-8 input used with the /u modifier.

Code that assumes it always receives an array should be checked.

Form feed trimming

Form feed (\f) is now part of the default character list removed by trim(), ltrim(), and rtrim().

RFC: Trim form feed

This will not affect most applications, but it can change results in systems that process control characters or fixed-format text.

SPL file iteration

Some SplFileObject behavior around next(), fgets(), current(), seek(), and end-of-file handling has changed for better consistency.

Projects with low-level file parsers should add focused tests instead of relying only on broad integration tests.

How to Test PHP 8.6 Today

Knowing what might break in general is one thing. Knowing whether your application breaks is the question that matters — and you can answer it this afternoon, without touching the PHP installed on your machine. Docker makes the whole exercise disposable: the PHP 8.6 image runs in an isolated container and can be removed after testing.

Official PHP Docker tags include PHP 8.6.0 Alpha 3 images.

Source: Official PHP Docker image tags

Run it in Docker

Check the version:

docker run --rm \
  php:8.6.0alpha3-cli-trixie \
  php -v

Then create a file named php86-test.php to try the features from earlier in this article:

<?php

declare(strict_types=1);

echo 'PHP version: ' . PHP_VERSION . PHP_EOL;
echo PHP_EOL;

echo "1. clamp()" . PHP_EOL;

var_dump(clamp(150, 0, 100));
var_dump(clamp(-10, 0, 100));
var_dump(clamp(50, 0, 100));

echo PHP_EOL;
echo "2. Partial Function Application" . PHP_EOL;

function calculatePrice(
    float $netPrice,
    float $vatRate,
    float $discountRate = 0,
): float {
    $discountedPrice = $netPrice * (1 - $discountRate);

    return $discountedPrice * (1 + $vatRate);
}

$addVat = calculatePrice(
    ?,
    vatRate: 0.20,
);

var_dump($addVat(100));
var_dump($addVat(250));

echo PHP_EOL;
echo "3. JSON error location" . PHP_EOL;

try {
    json_decode(
        '{"name":"Moneo","active":true,}',
        true,
        flags: JSON_THROW_ON_ERROR,
    );
} catch (JsonException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

echo PHP_EOL;
echo "4. Session defaults" . PHP_EOL;

var_dump([
    'session.use_strict_mode' =>
        ini_get('session.use_strict_mode'),

    'session.cookie_httponly' =>
        ini_get('session.cookie_httponly'),

    'session.cookie_samesite' =>
        ini_get('session.cookie_samesite'),
]);

Run it from the same directory:

docker run --rm \
  -v "$PWD":/app \
  -w /app \
  php:8.6.0alpha3-cli-trixie \
  php php86-test.php

On Windows PowerShell:

docker run --rm `
  -v "${PWD}:/app" `
  -w /app `
  php:8.6.0alpha3-cli-trixie `
  php php86-test.php

Try the stream error API

The structured stream errors from the runtime section are easy to see in action. Create stream-test.php:

<?php

declare(strict_types=1);

$context = stream_context_create([
    'stream' => [
        'error_mode' => StreamErrorMode::Exception,
    ],
]);

try {
    fopen(
        '/this/file/does/not/exist.csv',
        'r',
        false,
        $context,
    );
} catch (StreamException $exception) {
    echo $exception->getMessage() . PHP_EOL;

    foreach ($exception->getErrors() as $error) {
        echo sprintf(
            "[%s] %s%s",
            $error->code->name,
            $error->message,
            PHP_EOL,
        );
    }
}

Run it:

docker run --rm \
  -v "$PWD":/app \
  -w /app \
  php:8.6.0alpha3-cli-trixie \
  php stream-test.php

Point it at an existing project

Feature demos are fun, but a real project needs more than a syntax check. Start by running the full test suite with E_ALL.

PHPUnit:

docker run --rm \
  -v "$PWD":/app \
  -w /app \
  php:8.6.0alpha3-cli-trixie \
  php \
  -d error_reporting=E_ALL \
  vendor/bin/phpunit

Pest:

docker run --rm \
  -v "$PWD":/app \
  -w /app \
  php:8.6.0alpha3-cli-trixie \
  php \
  -d error_reporting=E_ALL \
  vendor/bin/pest

A quick syntax check across the codebase:

docker run --rm \
  -v "$PWD":/app \
  -w /app \
  php:8.6.0alpha3-cli-trixie \
  sh -c \
  'find . -name "*.php" -not -path "./vendor/*" -exec php -l {} \;'

A syntax check is useful, but it does not detect runtime behavior changes, deprecations that occur only on certain paths, extension incompatibilities, or session problems. That is what the next step is for.

Build a project-specific image

The basic CLI image does not include every extension used by a normal application. A Laravel or Symfony project may require extensions such as:

pdo_mysql
intl
mbstring
bcmath
zip
opcache
redis

Create a temporary test image that is close to the production environment.

Example Dockerfile.php86:

FROM php:8.6.0alpha3-cli-trixie

RUN docker-php-ext-install \
    pdo_mysql \
    opcache

COPY --from=composer:2 \
    /usr/bin/composer \
    /usr/bin/composer

WORKDIR /app

COPY . .

Build it:

docker build \
  -f Dockerfile.php86 \
  -t application-php86 .

Run the tests:

docker run --rm \
  application-php86 \
  php vendor/bin/phpunit

For a meaningful result, match these parts of production as closely as possible:

  • PHP extensions
  • Extension versions
  • php.ini settings
  • Operating system libraries
  • Database version
  • Redis or queue service version
  • Web server or FPM configuration
  • Environment variables

Handle Composer platform checks

Composer may reject PHP 8.6 when one of the project dependencies has not updated its PHP version constraint yet.

Start with a normal installation:

composer install

For a temporary experiment, this may bypass only the PHP version constraint:

composer install --ignore-platform-req=php

This does not prove that the package supports PHP 8.6. It only ignores the Composer platform check.

The package may still fail because it uses removed behavior, depends on an incompatible extension, or has tests that do not pass on PHP 8.6.

Use this option only for early compatibility testing, not as proof that a production upgrade is safe.

You can also ask Composer which dependencies block the version:

composer why-not php 8.6

Make it a CI job

Local experiments answer today's questions; CI keeps answering them as both the code and the release evolve. The best early-adoption approach is to add PHP 8.6 as an allowed experimental job that does not block deployments at first, but makes compatibility failures visible.

A simple matrix might test:

PHP 8.4: required
PHP 8.5: required
PHP 8.6: experimental

The PHP 8.6 job should run:

  • Composer installation
  • Unit tests
  • Integration tests
  • Static analysis
  • Code style checks
  • Framework console boot
  • Database migrations on a temporary database
  • Authentication and session tests
  • File import and export tests
  • Queue worker startup

Once the release candidate is available and the application is compatible, the PHP 8.6 job can become required.

Planning the Production Upgrade

With the tooling in place, what remains is order and timing: which parts of the application to test first, and when moving to 8.6 actually makes sense.

What should business applications test first?

For a typical corporate application, test these areas first:

  1. Run PHPUnit or Pest with E_ALL.
  2. Search for deprecation warnings.
  3. Test normal login, logout, password reset, and session renewal.
  4. Test SAML, OAuth, and cross-domain authentication.
  5. Test payment redirects and callback flows.
  6. Test file uploads, downloads, imports, and exports.
  7. Test missing files, permission errors, and network failures.
  8. Test invalid API and JSON payloads.
  9. Search for mbregex functions.
  10. Test code that catches exact exception classes.
  11. Test ZIP, image, Intl, SOAP, and database operations.
  12. Verify Composer package support.
  13. Verify every required PHP extension.
  14. Test queue workers and long-running processes.
  15. Compare production-like performance only after the RC stage.

Do not test only successful requests.

Many PHP 8.6 compatibility changes affect failure paths. They appear when a remote API sends invalid data, a socket fails, a filename contains an unexpected character, an option is out of range, or a callback returns the wrong value.

Should you upgrade immediately?

Not in production.

PHP 8.6 Alpha 3 is useful for:

  • Learning the new features
  • Testing internal libraries
  • Finding compatibility problems early
  • Preparing Docker images
  • Adding an experimental CI job
  • Updating framework and package support
  • Reporting PHP bugs
  • Planning an upgrade project

A reasonable production adoption process is:

  1. Start local and CI testing during Alpha and Beta.
  2. Fix deprecations and compatibility issues.
  3. Test again with the first release candidate.
  4. Check the support status of frameworks, Composer packages, and extensions.
  5. Run security, regression, performance, and infrastructure tests.
  6. Prepare a rollback plan.
  7. Upgrade production only after the stable release and full application validation.

For a new production application today, use a supported stable version such as PHP 8.5 when the full technology stack supports it.

Final Thoughts

PHP 8.6 does not completely change how PHP applications are built. It improves many areas that developers deal with every day, and it does so with a consistent point of view.

Partial Function Application makes callbacks and function configuration shorter without hiding the original function signature. clamp() gives a clear native name to a very common range-limiting pattern. The new stream error API provides a structured alternative to warnings, temporary error handlers, and the @ operator. The polling API gives PHP core, extensions, and async libraries a modern cross-platform base for scalable I/O. Safer session defaults improve new installations, but they also require careful testing in SSO, cross-domain, embedded, and payment flows. Reflection, JSON, TLS, URI, Intl, ZIP, MySQLi, cURL, socket, and performance improvements make PHP more practical for modern backend systems.

The most important step is not to wait until release day. Add PHP 8.6 to local development or CI now, run the real application, and find problems while there is still time to fix them calmly.

Official References

Moneo as Your Enterprise Partner

We collaborate closely with enterprise teams to design, deliver, and operate systems built for the long run.

Start Partnership
Emir Karşıyakalı

Emir Karşıyakalı

Founder & CEO

Jump to

Moneo as Your Enterprise Partner

We collaborate closely with enterprise teams to design, deliver, and operate systems built for the long run.

Start Partnership
Partnership is at the core of what we do.

Unsure where to start?
Let's figure it out together 👋

Contact Us