Potential gotchas when migrating to PHP 8

PHP 8 was released in 2020, but as with any language release, adoption takes time. Operations teams need to make it available on their servers. Developers need to update their code.

Such was the case for two of my clients. Upgrading their environments to PHP 8 was the easy part. Then I had to fix what the upgrade broke.

For one client, I'm maintaining a legacy codebase of highly customized WordPress plugins. Parts of this code base originally ran on PHP 4.

The other client project consumes a couple of different JSON APIs, and reads and writes data from and to a MySQL database. Although this codebase is younger than my other client's, it was originally developed with PHP 5 as a target.

PHP has evolved considerably since the release of versions 4 and 5. Luckily, the issues I encoutered were small and easy to fix. This post is a rundown of some of those issues. It's not a comprehensive list of all issues you might encounter, but I hope it helps you prepare for your own migration.

The big lesson: PHP 8 is much stricter about syntax than previous version of the language. Coding issues that raised E_WARNING errors in PHP 7 now create E_ERROR or Fatal errors in PHP 8. Warning errors should be fixed, but can be ignored. Fatal errors, on the other hand, prevent further execution of the script.

Optional parameters should follow required parameters

As part of this migration process, I made sure that my development environment reported every error — warnings, notices, deprecated messages, all of it. The first message I encountered was something like this.

Deprecated: Optional parameter $a declared before required parameter $b is implicitly treated as a required parameter in /var/www/script.php on line 273

Optional parameters are function arguments that have been assigned a default value. Consider the function below.

function output_the_strings( $a = 'cotton candy', $b ) {
  print $a . PHP_EOL;
  print $b . PHP_EOL;
}

In this case, $a is considered an optional parameter because it's been assigned a default value. However, because of its position as the first parameter, it's treated as a required one. Attempting to invoke this function with a single argument —┬ásuch as output_the_strings('firetruck') — fails here. You'll receive a fatal ArgumentCountError message.

Although PHP 7 and PHP 8 both treat $a as a required parameter, in PHP 8, this syntax triggers a deprecated notice. Deprecated means that your code will still run for now, but may not in a future version of the language.

To avoid future problems, reorder the function's parameters so that the optional arguments follow the required ones.

function output_the_strings( $b, $a = 'cotton candy' ) {
  print $a . PHP_EOL;
  print $b . PHP_EOL;
}

Now output_the_strings( 'firetruck' ) works, and firetruck gets assigned to $b.

Undefined constants now cause fatal errors

One of the downsides to a legacy codebase, is that lines of code that may have worked in older versions of the language now throw errors. See if you can spot the error in the code below.

if( defined( COMMENT_LOGIN )  ) {
    $response[ 'comment_login' ] = COMMENT_LOGIN;
}

Yep! COMMENT_LOGIN is not encloded in quotes. PHP 7 fails gracefully here. You may receive a warning notice if you've configured your error reporting to allow E_WARNING messages. Otherwise PHP 7 covers your mistake. Of course, defined( COMMENT_LOGIN ) — without quotation marks — evaluates to false even if COMMENT_LOGIN has been defined.

PHP 8, on the other hand, rightfully treats this as a Fatal error. Your script will stop executing. To prevent this, make sure that you enclose the argument in quotes when using defined().

if( defined( 'COMMENT_LOGIN' )  ) {
    $response[ 'comment_login' ] = COMMENT_LOGIN;
}

Again: that's the correct syntax to use. PHP 8 makes it a breaking error.

Removed filter_var flags.

The filter_var function is a must for applications that handle untrusted input. But some of its flags have were removed in PHP 8. In particular, you can no longer explicitly set the FILTER_FLAG_SCHEME_REQUIRED and FILTER_FLAG_HOST_REQUIRED flags for the FILTER_VALIDATE_URL filter.

Perhaps you've used filter_var in a way that resembles the following code.

$is_valid_url = filter_var(
  $url,
  FILTER_VALIDATE_URL,
    array(
      'flags' => FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED
    );
);

In PHP 7.1 and 7.2, this did not raise any errors. As of PHP 7.3, however, using these flags raised a deprecated notice.

As of PHP 8, however, using these flags triggers a fatal error. To resolve it, use FILTER_VALIDATE_URL without the flags.

$is_valid_url = filter_var( $url, FILTER_VALIDATE_URL );

Since PHP 8 now requires the scheme and host name as part of its validation test, you do not need to explictly set those flags.

Function overrides must contain the same number of arguments

PHP 8 also enforces a stricter syntax for class method overrides. When one class extends another and contains a function of the same name, the number of arguments need to match.

Say we have a class called Mom. Mom has a method named does_a_thing.

class Mom {
  public static function does_a_thing( $a, $b, $c ) {
    print $a . '<br>';
    print $b . '<br>';
    print $c . '<br>';
  }
}

class Kid extends Mom {
  public static function does_a_thing( $a, $b ) {
    print $a . '<br>';
    print $b . '<br>';
  }
}

Kid is a class that extends Mom, however it's does_a_thing() method only has two parameters. In this case, PHP 7 raises a warning-level error.

Warning: Declaration of Kid::does_a_thing($a, $b) should be compatible with Mom::does_a_thing($a, $b, $c) in /var/www/script.php on line 26

Kid::does_a_thing() still gets invoked, despite the warning. In PHP 8, however, this triggers a Fatal error.

Fatal error: Declaration of Kid::does_a_thing($a, $b) must be compatible with Foo::does_a_thing($a, $b, $c) in /var/www/script.php on line 26

Our should in PHP 7 becomes a must in PHP 8. Here too, the fix is easy: ensure that the overriding method has the same number of parameters as the original method.

Object initialization

Lastly, PHP 8 now prohibits assigning property names to null objects. Consider the following example.

$cars[0]->make = 'Fiat';

Neither $cars nor the first object in the $cars array exist yet. Although PHP 7 politely informs you that you are Creating default object from empty value, it behaves as though you've initialized the array and the object. As shown in (Figure 1), print_r( $cars ) displays an array containing one stdClass object whose sole property is name.

Array
(
    [0] => stdClass Object
        (
            [make] => Fiat
        )
)
Figure 1: PHP 7 creates an object when you assign a property to an empty value. PHP 8 does not.

In PHP 8, this causes your code to fail. You'll receive an error that's similar to the following.

Fatal error: Uncaught Error: Attempt to assign property "name" on null in /var/www/script.php:19 Stack trace: #0 {main} thrown in /var/www/script.php on line 19

One way to avoid this issue: initialize an empty object before assigning properties.

$cars = [];
$cars[0] = new stdClass();

$cars[0]->make = 'Fiat';

Another option is to change your objects to arrays. You can use type juggling / casting to convert the arrays back to objects if you'd like to minimize the amount of code you need to rewrite. Here's an example.

$cars = [];
$cars[0]['make'] = 'Fiat';
$cars[0] = (object)$cars[0];

You can also use the array_map function if your array has multiple items.

$cars = array_map( function( $i ){
  $i = (object)$i;
  return $i;
}, $cars );

Either of these examples would allow you to continue using $cars[0]->make elsewhere in your code.

Conclusion

Migrating from PHP 7 to 8 may introduce some unexpected errors and behaviors in your code. Generally, however, the issues are easy to fix. PHP 8 introduces some excellent changes and improvements to the language. Most of all, I think it encourages developers to be a bit more disciplined about our code.

If you have a legacy codebase of WordPress plugins that could use some updating, or a LAMP application that needs some rewriting, that's something Webinista, Inc. can do. Get in touch.

Subscribe to the Webinista (Not) Weekly

A mix of tech, business, culture, and a smidge of humble bragging. I send it sporadically, but no more than twice per month.

View old newsletters