• 7 min read

PHP 8.6 Deprecations: Find Them in Your Codebase Before November 19

PHP 8.6 ships November 19 with a long list of deprecations. Here is which ones will hit real code and a small token-based scanner that finds them.

Featured image for "PHP 8.6 Deprecations: Find Them in Your Codebase Before November 19"

PHP 8.6 is scheduled for general availability on November 19, 2026, according to the PHP release wiki. Most of the attention goes to partial function application and clamp(). The less glamorous part of every release is the deprecation list, and 8.6 has a long one.

Deprecations do not break anything on 8.6. They emit E_DEPRECATED notices, and the functions keep working until a future major version removes them. But those notices land in your logs, they fail test suites that convert warnings to exceptions, and they are much cheaper to fix now than during a major upgrade. This post sorts the list by how likely each item is to show up in real code, then gives you a script to find them.

Everything below comes from Brent Roose’s What’s new in PHP 8.6 on stitcher.io, which links the underlying RFCs. That page was written in July and notes the list could still change, so confirm against the final release notes when 8.6.0 ships.

The ones most likely to bite

Return from a finally block. This is the one worth stopping for, because it changes behavior you may be relying on by accident:

function getConfig(): array
{
    try {
        return loadConfig();
    } finally {
        return [];
    }
}

The return in finally silently overrides the one in try. It is deprecated in 8.6 because it is a classic source of swallowed values and swallowed exceptions. Search for it by hand. A scanner built on function names will not find it.

Return values from constructors and destructors. A return 1; inside __construct() never did anything useful, since nobody can receive the value. It is now deprecated. Old code generated from templates sometimes has these.

Type alias functions. These are the most common hits in older codebases:

  • is_double() becomes is_float()
  • is_integer() and is_long() become is_int()
  • doubleval() becomes floatval()

They are one-line fixes and safe to apply with a find and replace.

spl_object_hash(). Use spl_object_id() instead. If you really need a hash-shaped string, the article shows a pipe-operator recipe that builds one from the object ID. Be careful here: spl_object_hash() returns a string and spl_object_id() returns an int, so any code that uses the result as an array key, or compares it to a stored value, deserves a second look.

mysqli::stmt_init(). If you still call $mysqli->stmt_init() followed by prepare(), call $mysqli->prepare($sql) directly. mysqli_get_charset() is deprecated too, and the RFC notes it has returned unreliable values since PHP 8.2.

Worth a grep, probably rare

Some deprecations target rarely used corners. Check them with a search anyway, since the cost is a few seconds:

  • define() with case_insensitive: true. Case-insensitive constants were removed long ago, so the argument does nothing.
  • is_a() and is_subclass_of() with allow_string: false when you pass a string.
  • strcoll() and the SORT_LOCALE_STRING sort flag.
  • metaphone(). The article points to userland double-metaphone packages as the replacement.
  • spl_classes(), replaced by ReflectionExtension::getClassNames().
  • The CSV methods on SplFileObject: fgetcsv(), fputcsv(), setCsvControl() and getCsvControl().
  • Several ArrayIterator methods, including asort(), ksort(), getFlags() and setFlags().
  • ReflectionMethod::invoke() with an object passed to a static method. Pass null.
  • Using a ReflectionProperty from one class to set a value on an unrelated object.
  • Passing objects to functions such as array_walk() that also accept arrays.
  • Session handlers passed to session_set_save_handler() that do not implement create_sid() and validateId().

Reserved words

PHP is also reserving a few identifiers for future syntax. Using let or is as a class or function name is deprecated, as is naming a class constant NAMESPACE, naming a function readonly, and using a bare _ as a constant or compile-time alias. If you have a domain class named Is or a helper called let(), this is where it shows up. Test frameworks and DSL-style libraries are the likeliest offenders.

A scanner for the function-based deprecations

Plain grep -rn is_long src/ will also match method calls, comments and strings. PHP’s built-in tokenizer is a better fit. This script walks a directory, tokenizes every .php file, and reports calls to the deprecated functions while skipping method calls, static calls and function declarations:

<?php

declare(strict_types=1);

$replacements = [
    'is_double'          => 'is_float()',
    'is_integer'         => 'is_int()',
    'is_long'            => 'is_int()',
    'doubleval'          => 'floatval()',
    'spl_object_hash'    => 'spl_object_id()',
    'spl_classes'        => 'ReflectionExtension::getClassNames()',
    'strcoll'            => 'no direct replacement, review usage',
    'metaphone'          => 'a userland double-metaphone package',
    'mysqli_get_charset' => 'no replacement, remove the call',
];

$skipBefore = [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON, T_FUNCTION, T_NEW];
$nameTokens = [T_STRING, T_NAME_FULLY_QUALIFIED];

function significant(array $tokens, int $from, int $step): int|null
{
    for ($i = $from; isset($tokens[$i]); $i += $step) {
        $ignored = [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT];

        if (! is_array($tokens[$i]) || ! in_array($tokens[$i][0], $ignored, true)) {
            return $i;
        }
    }

    return null;
}

$files = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($argv[1] ?? 'src', FilesystemIterator::SKIP_DOTS)
);

foreach ($files as $file) {
    if ($file->getExtension() !== 'php') {
        continue;
    }

    $tokens = token_get_all(file_get_contents($file->getPathname()));

    foreach ($tokens as $i => $token) {
        if (! is_array($token) || ! in_array($token[0], $nameTokens, true)) {
            continue;
        }

        $name = strtolower(ltrim($token[1], '\\'));

        if (! isset($replacements[$name])) {
            continue;
        }

        $next = significant($tokens, $i + 1, 1);
        $prev = significant($tokens, $i - 1, -1);

        if (($tokens[$next] ?? null) !== '(') {
            continue;
        }

        if ($prev !== null && is_array($tokens[$prev]) && in_array($tokens[$prev][0], $skipBefore, true)) {
            continue;
        }

        printf("%s:%d  %s() -> %s\n", $file->getPathname(), $token[2], $name, $replacements[$name]);
    }
}

Run it with php scan-86.php app/ and you get one line per hit, with the file, the line number and the suggested replacement. I ran the core of this loop against a small sample containing a plain call, a fully qualified call, a method named is_long(), a function declaration and an upper-case call with a space before the parenthesis. It flagged the three real calls and ignored the method and the declaration.

Two limits to know about. It matches on the function name, so a namespaced function of the same name in your own code would be a false positive. It also cannot see calls made through variable functions or string callbacks such as array_map('doubleval', $values). Grep for the quoted names to catch those.

Let PHP find the rest

The scanner covers the easy half. For the rest, run your test suite on a PHP 8.6 release candidate with error_reporting set to E_ALL and deprecations visible. If your suite turns notices into exceptions, for example through PHPUnit’s failOnDeprecation setting, the failures will point straight at the offending line. Add 8.6 as an allowed-failure job in CI now, so a new deprecation shows up the week it is introduced instead of the week you upgrade.

If you maintain a public package for PHP Architect readers or your own team, do this before you tag your next release. A dependency that emits deprecation notices on 8.6 is a support ticket waiting to happen for everybody who installs it.

A short plan

  1. Run the scanner and apply the alias replacements.
  2. Grep for finally, stmt_init, SplFileObject CSV calls and the reserved words listed above.
  3. Run the test suite on an 8.6 release candidate with deprecations visible.
  4. Add an 8.6 CI job that is allowed to fail.
  5. Re-read the final release notes on November 19 and check whether anything on this list changed.

None of this is hard, and most of it is mechanical. The point is to spend an afternoon on it now and have 8.6 be a boring upgrade.

Sources