Troubleshooting 12 min read

12 PHP 8 Error Messages and Exactly How to Fix Them

Undefined array key, null passed to a non-nullable parameter, readonly property modification and eight more PHP 8 errors - each with the cause and the fix.

These are the messages you will actually see in the log during a PHP 8 upgrade, with the cause and the fix for each one. Almost all of them come from the same place: PHP 8 stopped being lenient about types, about removed functions and about parameter lists.

The short answer

The errors fall into four groups. Removed functions are fatal the moment the code path runs. Type strictness turns silent conversions into notices and fatals. Signature changes turn invalid parameter lists into compile-time errors. And the language changes around arrays, properties and arguments produce errors that look like bugs in your code but are actually changes in the contract.

The fix is always the same shape: find the call site, decide what the code meant, and make the type explicit. The list below gives the exact change for each message.

The 12 error messages

1. Undefined array key "foo"

What it means: the code read an array key that is not there. Before PHP 8 this was a notice and the value was null; since PHP 8 it is a warning, and the value is still null.

$name = $user['name'];            // undefined array key "name"
$name = $user['name'] ?? '';      // the fix, with a default
$name = $user['name'] ?? null;    // or with null as the default

How to fix it: decide whether the key is optional. If it is, use the null coalescing operator. If it is not, the data is missing and the code below it will break, so treat it as an error rather than as a warning.

2. Undefined variable $foo

What it means: the variable was used before it was assigned. It was a notice in PHP 7 and a warning since PHP 8, and the value is null.

if (!isset($total)) {
    $total = 0;
}

How to fix it: find out why it was not set. The usual causes are a renamed variable, a return that was added in one branch, or a variable that used to be a parameter and is now missing.

3. Attempt to read property "foo" on null

What it means: something returned null where an object was expected, and the code immediately read a property from it. It is a warning in PHP 8.

// before
$name = $repository->find($id)->name;

// after
$record = $repository->find($id);
if ($record === null) {
    throw new NotFoundException(sprintf('Record %d not found', $id));
}
$name = $record->name;

How to fix it: this one is usually a real bug that PHP 7 hid: a missing row, a failed lookup, an empty collection. Do not silence it with ->?->name unless a missing value really is an expected outcome.

4. Passing null to parameter #2 ($name) of type string is deprecated

What it means: an internal function was given null for a parameter that does not accept it. PHP 8.1 deprecated it; it will become an error later.

// before
$length = strlen($input);        // $input may be null

// after
$length = strlen($input ?? '');   // if null and empty are the same here

How to fix it: look at what the function does with an empty string. For strlen, trim and most string functions, an empty string is the right default. For explode() or a date function, the right default may be different, so decide per call site rather than blanket replacing.

5. Required parameter $b follows optional parameter $a

What it means: a function declares a required parameter after an optional one. PHP 8 made this a fatal error at compile time, so the file will not even load.

// before
function connect(string $host, int $port = 3306, string $socket) {}

// after, the safe way for a public API
function connect(string $host, string $socket, int $port = 3306) {}

How to fix it: give the trailing parameter a null default and make it nullable, or reorder the parameters and update every call site. Reordering is the safer choice for a public API, because it cannot change the meaning of an existing call.

6. Cannot modify readonly property Foo::$bar

What it means: a property declared readonly was written twice, or was written from outside the scope that declared it.

final class Money
{
    public function __construct(public readonly int $amount) {}

    public function double(): self
    {
        $this->amount = $this->amount * 2;   // error
        return new self($this->amount * 2);
    }
}

How to fix it: readonly properties can be written once, from inside the declaring scope. If the value has to change, create a new instance with the new value. If it was written twice from the constructor, the constructor is doing too much.

7. Call to undefined function each()

What it means: each() was removed in PHP 8.0. The call is a fatal error as soon as that line runs.

// before
while (list($key, $value) = each($items)) {}

// after
foreach ($items as $key => $value) {}

How to fix it: replace it with a foreach loop. The full removal list is in the deprecated functions article.

8. Call to undefined function mysql_connect()

What it means: the mysql_* procedural API was removed in PHP 7.0. The code is older than PHP 7.

// before
$result = mysql_query('SELECT id FROM users', $conn);
while ($row = mysql_fetch_assoc($result)) {}

// after
$stmt = $pdo->query('SELECT id FROM users');
while ($row = $stmt->fetch()) {}

How to fix it: migrate the database layer to PDO or MySQLi. This is the largest single task in most legacy migrations and it deserves its own plan. Note that mysql_query() returned false on failure while PDO throws by default, so the error handling has to be revisited as well.

9. Cannot use positional argument after named argument

What it means: PHP 8.0 added named arguments. Once you name an argument, the ones after it must be named too.

// before
array_slice($items, 0, 10, true);

// after
array_slice($items, 0, length: 10, preserve_keys: true);

How to fix it: name the remaining arguments, or move the named one to the end. This error appears when a library started using named arguments in its own calls.

10. Maximum execution time of 30 seconds exceeded

What it means: the script ran longer than max_execution_time. It is not really a PHP 8 error, but it very often appears during an upgrade because the upgrade made something slower.

set_time_limit(0);   // only for a CLI migration script

How to fix it: look for the real cause before raising the limit. The usual culprits after an upgrade are a query that now uses an index it did not use before, an unbounded loop that used to short-circuit on a removed function, or a retry loop that now retries because the error type changed. Raising the limit hides all three.

11. Class "Foo" not found

What it means: the class could not be autoloaded. After an upgrade this is usually one of three things: a namespace change in a new dependency version, a case sensitivity problem on a case insensitive filesystem, or a class that moved.

// PSR-4 requires the file name to match the class name
// src/Service/OrderProcessor.php  ->  namespace App\Service; class OrderProcessor

How to fix it: check the autoload map with composer dump-autoload -o, then compare the namespace in the class with the one in the use statement. On Linux, a file called orderprocessor.php instead of OrderProcessor.php works on macOS and fails in production.

12. Array to string conversion

What it means: an array was used where a string was expected. It was a notice in PHP 7 and a warning since PHP 8, and the result is the literal string "Array".

// before
echo $order->items;          // "Array"

// after
echo implode(', ', $order->items);
echo json_encode($order->items);

How to fix it: decide what the string should be. Often the code wanted a single field from an array of objects, or a JSON representation. This warning is frequently a sign that a function started returning an array where it used to return a scalar.

Reading the log

During an upgrade the log is more useful than the browser, because the errors appear on the routes that only exist in production. Three habits make the difference:

  • Turn every notice into a visible line. Make sure error_reporting includes E_ALL and that display_errors is off in production while log_errors is on. A notice that is swallowed is a bug you will find again in six months.
  • Read the file and the line, not just the message. The line number tells you which of the twelve categories you are looking at, and the file tells you how old that code is.
  • Count by category. Four hundred "Undefined array key" warnings are one problem. Four hundred warnings spread over six categories are six problems, and the order you fix them in should start with the fatal ones.

Work through them in this order

  1. Fatal errors first: undefined removed functions and the class loading failures. They stop the code from running at all.
  2. Compile-time errors: the invalid parameter lists. The file does not load until they are fixed.
  3. Type strictness notices: the null parameters. These are the ones that change behaviour, so they need a decision per call site.
  4. Property and argument contract changes: readonly properties, the named argument order and the array to string conversions.
  5. The undefined keys and variables: the largest group, and usually the cheapest per fix. Group them by file and fix them in one pass.

A compatibility scan gives you this grouping directly, because it reports by rule rather than by message. php2shell lists every finding with the file, the line, the PHP version it affects and the recommended change.

Reporting during the migration

Two settings decide how much noise you see while you work:

// in php.ini or the CI configuration
error_reporting = E_ALL;
display_errors = Off;      // on a server
log_errors = On;

On a developer machine, display_errors = On is fine and much faster to work with. On a staging or production server, keep the display off and read the log: a warning printed into a JSON response breaks the response for the client that called your API.

In PHPUnit, make the deprecations fail the build while you are migrating, so nothing slips through:

vendor/bin/phpunit --display-deprecations --fail-on-deprecation

A summary table

MessageSeverity in PHP 8Where it comes from
Undefined array keyWarningMissing array key
Undefined variableWarningVariable never assigned
Attempt to read property on nullWarningA lookup returned nothing
Passing null to parameter of type stringDeprecated since 8.1Strict internal function types
Required parameter follows optional parameterFatal at compile timeInvalid signature
Cannot modify readonly propertyFatalReadonly contract
Call to undefined function each()FatalRemoved in PHP 8.0
Call to undefined function mysql_connect()FatalRemoved in PHP 7.0
Cannot use positional argument after named argumentFatalNamed arguments, new in PHP 8.0
Maximum execution time exceededFatalUsually a slow path, not a PHP change
Class not foundFatalAutoload, namespace or file name case
Array to string conversionWarningA return type changed to an array
Disclaimer. This article is guidance for planning a migration. The automatic fixes that php2shell applies are best-effort transformations. Always review the diff and run your test suite in a staging environment before you deploy. php2shell does not guarantee that an upgrade is free of runtime errors or business interruptions.

Frequently asked questions

Is 'Undefined array key' a fatal error?

It is a warning in PHP 8 and the value evaluates to null, so the code keeps running until something downstream expects the missing value. Treat it as an error: it almost always points at a missing array key or an unchecked database row.

How do I fix 'Required parameter follows optional parameter'?

Make the parameter list consistent. Either give the optional parameter a null default and make the following ones optional as well, or reorder the parameters and update every call site. Reordering is the safer option for public APIs because it cannot change the meaning of an existing call.

Why do I get 'Cannot modify readonly property'?

The property was declared readonly and has already been initialised. Readonly properties can only be written once, from inside the declaring scope. If your code needs to change the value later, remove the readonly modifier or clone the object and modify the copy.

Where to go next