Chapter 5: Discovering Potential OOP Backward-Compatibility Breaks

This chapter marks the beginning of Part 2 of the book, PHP 8 Tricks. In this part, you'll discover the dark corners of PHP 8: the place where backward-compatibility breaks exist. This part gives you insight into how to avoid problems before migrating an existing application to PHP 8. You will learn what to look for in your existing code that could cause it to stop working after a PHP 8 upgrade. Once you master the topics presented in this part of the book, you will be well equipped to modify existing code in such a manner that it continues to function normally following a PHP 8 upgrade.

In this chapter, you will be introduced to new PHP 8 features specific to object-oriented programming (OOP). The chapter provides you with plenty of short code examples that clearly illustrate the new features and concepts. This chapter is critical in helping you quickly take advantage of the power of PHP 8 as you adapt the code examples for your own practice. The focus of this chapter is on situations where object-oriented code might break after a PHP 8 migration.

Topics covered in this chapter include the following:

  • Discovering core OOP coding differences
  • Navigating changes in magic methods
  • Taking control of serialization
  • Understanding expanded PHP 8 variance support
  • Handling Standard PHP Library (SPL) changes

Technical requirements

To examine and run the code examples provided in this chapter, the minimum recommended hardware is the following:

  • x86_64 based desktop PC or laptop
  • 1 gigabyte (GB) of free disk space
  • 4 GB of RAM
  • A 500 kilobits per second (Kbps) or faster internet connection

In addition, you will need to install the following software:

  • Docker
  • Docker Compose

Please refer to the Technical requirements section of Chapter 1, Introducing New PHP 8 OOP Features, for more information on Docker and Docker Compose installation, as well as how to build the Docker container used to demonstrate code explained in this book. In this book, we refer to the directory in which you restored the sample code for this book as /repo.

The source code for this chapter is located here: https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices.

We can now begin our discussion by examining core OOP coding differences.

Discovering core OOP coding differences

There are a number of significant changes to how you are able to write OOP code in PHP 8. In this section, we focus on three key areas that might present you with potential backward-compatibility breaks. The areas we address in this section are common bad practices associated with making static method calls, handling object properties, and PHP autoloading.

After reading this section, and working your way through the examples, you are in in a better position to spot OOP bad practices and to learn how PHP 8 has placed restrictions on such usage. In this chapter, you learn good coding practices, which will ultimately make you a better programmer. You will also be able to address changes in PHP autoloading that can potentially cause failure in an application migrated to PHP 8.

Let's first look at how PHP 8 has tightened up on making static calls.

Handling static calls in PHP 8

Surprisingly, PHP versions 7 and below allowed developers to make a static call to a class method not declared static. At first glance, any future developer reviewing your code immediately assumes that the method has been defined as static. This can lead to unexpected behavior as the future developer, operating under a false assumption, starts to misuse your code.

In this simple example, we define a Test class with a nonStatic() method. In the procedural code that follows the class definition, we echo the return value of this method, however, in doing so we make a static call:

// /repo/ch05/php8_oop_diff_static.php

class Test {

    public function notStatic() {

        return __CLASS__ . PHP_EOL;

    }

}

echo Test::notStatic();

When we run this code in PHP 7, here is the result:

root@php8_tips_php7 [ /repo/ch05 ]#

php php8_oop_diff_static.php

PHP Deprecated:  Non-static method Test::notStatic() should not be called statically in /repo/ch05/php8_oop_diff_static.php on line 11

Test

As you can see from the output, PHP 7 issues a deprecation notice, but allows the call to be made! In PHP 8, however, the result is a fatal Error, as shown here:

root@php8_tips_php8 [ /repo/ch05 ]#

php php8_oop_diff_static.php

PHP Fatal error:  Uncaught Error: Non-static method Test::notStatic() cannot be called statically in /repo/ch05/php8_oop_diff_static.php:11

Calling a non-static method using static method call syntax is a bad practice in the sense that well-written code makes the intention of the code developer crystal clear. If you do not define a method as static, but later call it in a static sense, a developer assigned to maintain your code in the future might become confused and could make wrong assumptions about the original intent of the code. The end result will be even more bad code!

In PHP 8, you can no longer call a non-static method using a static method call. Let's now have a look at another bad practice involving treating object properties as keys.

Dealing with object property handling changes

Arrays have been a central feature in PHP all the way back to the earliest versions. OOP, on the other hand, was not introduced until PHP 4. In the early days of OOP, array functions were often expanded to accommodate object properties. This led to a blurring of the distinction between an object and an array, which in turn spawned a number of bad practices.

In order to maintain a clear separation between array handling and object handling, PHP 8 now restricts the array_key_exists() function to only accept an array as an argument. To illustrate this, consider the following example:

  1. First, we define a simple anonymous class with a single property:

    // /repo/ch05/php8_oop_diff_array_key_exists.php

    $obj = new class () { public $var = 'OK.'; };

  2. We then run three tests that each check for the existence of $var, using isset(), property_exists(), and array_key_exists():

    // not all code is shown

    $default = 'DEFAULT';

    echo (isset($obj->var))

        ? $obj->var : $default;

    echo (property_exists($obj,'var'))

        ? $obj->var : $default;

    echo (array_key_exists('var',$obj))

        ? $obj->var : $default;

When we run this code in PHP 7, all tests succeed, as shown here:

root@php8_tips_php7 [ /repo/ch05 ]#

php php8_oop_diff_array_key_exists.php

OK.OK.OK.

In PHP 8, however, a fatal TypeError occurs, as array_key_exists() now only accepts an array as an argument. The PHP 8 output is shown here:

root@php8_tips_php8 [ /repo/ch05 ]#

php php8_oop_diff_array_key_exists.php

OK.OK.PHP Fatal error:  Uncaught TypeError: array_key_exists(): Argument #2 ($array) must be of type array, class@anonymous given in /repo/ch05/php8_oop_diff_array_key_exists.php:10

The best practice is to use either property_exists() or isset(). We now turn our attention to changes in PHP autoloading.

Working with PHP 8 autoloading

The basic autoloading class mechanism first introduced in PHP 5.1 works the same in PHP 8. The main difference is that the support for the global function __autoload(), deprecated in PHP 7.2, has been completely removed in PHP 8. Starting with PHP 7.2, developers were encouraged to register their autoloading logic using spl_autoload_register(), available for that purpose since PHP 5.1. Another major difference is how spl_autoload_register() reacts if unable to register an autoloader.

An understanding of how the autoloading process works when using spl_autoload_register() is critical to your work as a developer. Failure to grasp how PHP automatically locates and loads classes will limit your ability to grow as a developer and could have a detrimental impact on your career path.

Before getting into spl_autoload_register(), let's first have a look at the __autoload() function.

Understanding the __autoload() function

The __autoload() function was used by many developers as the primary source of autoloading logic. This function behaves much as a magic method does and that's why it's called automatically depending on the context. Circumstances that would trigger an automatic call to the __autoload() function include the moment when a new class instance is created, but where the class definition has not yet been loaded. Further, if the class extends another class, the autoload logic is also invoked in order to load the super class prior to the creation of the subclass that extends it.

The advantage of using the __autoload() function was that it was quite easy to define, and was often defined in a website's initial index.php file. The disadvantages included the following:

  • __autoload() was a PHP procedural function; not defined nor controlled using OOP principles. This can become an issue when defining unit tests for an application, for example.
  • If your application uses namespaces, the __autoload() function must be defined in the global namespace; otherwise, classes outside of the namespace in which the __autoload() function is defined will fail to load.
  • The __autoload() function doesn't work well with spl_autoload_register(). If you define autoloading logic using both the __autoload() function and spl_autoload_register(), the __autoload() function logic is entirely ignored.

To illustrate potential problems, we'll define an OopBreakScan class, discussed in more detail in Chapter 11, Migrating Existing PHP Apps to PHP 8:

  1. First, we define and then add a method to the OopBreakScan class that scans the file contents for the __autoload() function. Note that the error message is a class constant defined in the Base class that simply warns of the presence of the __autoload() function:

    namespace Migration;

    class OopBreakScan extends Base {

        public static function scanMagicAutoloadFunction(

            string $contents, array &$message) : bool {

            $found  = 0;

            $found += (stripos($contents,

                'function __autoload(') !== FALSE);

            $message[] = ($found)

                       ? Base::ERR_MAGIC_AUTOLOAD

                       : sprintf(Base::OK_PASSED,

                           __FUNCTION__);

            return (bool) $found;

        }

        // remaining methods not shown

    This class extends a MigrationBase class (not shown). This is significant as any autoloading logic needs to find not only the subclass but its super class as well.

  2. Next, we define a calling program in which a magic __autoload() function is defined:

    // /repo/ch05/php7_autoload_function.php

    function __autoLoad($class) {

        $fn = __DIR__ . '/../src/'

            . str_replace('', '/', $class)

            . '.php';

        require_once $fn;

    }

  3. We then make use of the class by having the calling program scan itself:

    use MigrationOopBreakScan;

    $contents = file_get_contents(__FILE__);

    $message  = [];

    OopBreakScan::

        scanMagicAutoloadFunction($contents, $message);

    var_dump($message);

Here is the output running in PHP 7:

root@php8_tips_php7 [ /repo/ch05 ]#

php php7_autoload_function.php

/repo/ch05/php7_autoload_function.php:23:

array(1) {

  [0] =>  string(96) "WARNING: the "__autoload()" function is removed in PHP 8: replace with "spl_autoload_register()""

}

As you can see from the output, the MigrationOopBreakScan class was autoloaded. We know this because the scanMagicAutoloadFunction method was invoked, and we have its results. Furthermore, we know that the MigrationBase class was also autoloaded. The reason we know this is because the error message that appears in the output is a constant of the super class.

However, the same code running in PHP 8 produces this result:

root@php8_tips_php8 [ /repo/ch05 ]#

php php7_autoload_function.php

PHP Fatal error:  __autoload() is no longer supported, use spl_autoload_register() instead in /repo/ch05/php7_autoload_function.php on line 4

This result is not surprising as support for the magic __autoload() function was removed in PHP 8. In PHP 8, you must use spl_autoload_register() instead. We now turn our attention to spl_autoload_register().

Learning to use spl_autoload_register()

The primary advantage of the spl_autoload_register() function is that it allows you to register more than one autoloader. Although this might seem like overkill, imagine the nightmare scenario where you are using a number of different open source PHP libraries... and where they all have their own autoloaders defined! As long as all such libraries use spl_autoload_register(), having multiple autoloader callbacks poses no problem.

Each autoloader registered using spl_autoload_register() must be callable. Any of the following are considered callable:

  • A PHP procedural function
  • An anonymous function
  • A class method that can be called in a static manner
  • Any class instance that defines the __invoke() magic method
  • An array in this form: [$instance, 'method']

    Tip

    Composer maintains its own autoloader, which in turn relies upon spl_autoload_register(). If you are using Composer to manage your open source PHP packages, you can simply include /path/to/project/vendor/autoload.php at the start of your application code to use the Composer autoloader. To have Composer autoload your application source code files, add one or more entries into the composer.json file under the autoload : psr-4 key. For more information, see https://getcomposer.org/doc/04-schema.md#psr-4.

A quite typical autoloader class might appear as follows. Note that this is the class we use for many of the OOP examples shown in this book:

  1. In the __construct() method, we assign the source directory. Following that, we call spl_auto_register() using the array callable syntax noted above:

    // /repo/src/Server/Autoload/Loader.php

    namespace ServerAutoload;

    class Loader {

        const DEFAULT_SRC = __DIR__ . '/../..';

        public $src_dir = '';

        public function __construct($src_dir = NULL) {

            $this->src_dir = $src_dir

                ?? realpath(self::DEFAULT_SRC);

            spl_autoload_register([$this, 'autoload']);

        }

  2. The actual autoloading code is similar to that shown in our __autoload() function example above. Here is the method that does the actual autoloading:

        public function autoload($class) {

            $fn = str_replace('', '/', $class);

            $fn = $this->src_dir . '/' . $fn . '.php';

            $fn = str_replace('//', '/', $fn);

            require_once($fn);

        }

    }

Now that you have an idea of how to use the spl_auto_register() function, we must examine a potential code break when running PHP 8.

A potential spl_auto_register() code break in PHP 8

The second argument to the spl_auto_register() function is an optional Boolean value that defaults to FALSE. If the second argument is set to TRUE, the spl_auto_register() function throws an Exception in PHP 7 and below versions if an autoloader fails to register. In PHP 8, however, if the data type of the second argument is anything other than callable, a fatal TypeError is thrown instead, regardless of the value of the second argument!

The simple program example shown next illustrates this danger. In this example, we use the spl_auto_register() function to register a PHP function that does not exist. We set the second argument to TRUE:

// /repo/ch05/php7_spl_spl_autoload_register.php

try {

    spl_autoload_register('does_not_exist', TRUE);

    $data = ['A' => [1,2,3],'B' => [4,5,6],'C' => [7,8,9]];

    $response = new ApplicationStrategyJsonResponse($data);

    echo $response->render();

} catch (Exception $e) {

    echo "A program error has occurred ";

}

If we then run this block of code in PHP 7, here is the result:

root@php8_tips_php7 [ /repo/ch05 ]#

php php7_spl_spl_autoload_register.php

A program error has occurred

As you can determine from the output, an Exception is thrown. The catch block is invoked, and the message A program error has occurred appears. When we run the same program in PHP 8, however, a fatal Error is thrown:

root@php8_tips_php8 [ /repo/ch05 ]#

php php7_spl_spl_autoload_register.php

PHP Fatal error:  Uncaught TypeError: spl_autoload_register(): Argument #1 ($callback) must be a valid callback, no array or string given in /repo/ch05/php7_spl_spl_autoload_register.php:12

Obviously, the catch block was bypassed as it was designed to catch an Exception, not an Error. The simple solution is to have the catch block catch Throwable instead of Exception. This allows the same code to run in either PHP 7 or PHP 8.

Here is how the rewritten code might appear. The output is not shown as it's identical to the same example running in PHP 7:

// /repo/ch05/php8_spl_spl_autoload_register.php

try {

    spl_autoload_register('does_not_exist', TRUE);

    $data = ['A' => [1,2,3],'B' => [4,5,6],'C' => [7,8,9]];

    $response = new ApplicationStrategyJsonResponse($data);

    echo $response->render();

} catch (Throwable $e) {

    echo "A program error has occurred ";

}

You now have a better understanding of PHP 8 autoloading, and how to spot and correct potential autoloading backward-compatibility breaks. Let's now have a look at changes in PHP 8 pertaining to magic methods.

Navigating changes in magic methods

PHP magic methods are predefined hooks that interrupt the normal flow of an OOP application. Each magic method, if defined, alters the behavior of the application from the minute the object instance is created, up until the point where the instance goes out of scope.

Important note

An object instance goes out of scope when it's unset or overwritten. Object instances also go out of scope when defined in a function or class method, and the execution of that function or class method ends. Ultimately, if for no other reason, an object instance goes out of scope when the PHP program ends.

This section will give you a solid understanding of important changes to magic method usage and behavior introduced in PHP 8. Once you understand the situations described in this section, you will be in a position to make the appropriate code modifications to prevent your application code from failing should you migrate to PHP 8.

Let's first have a look at changes to the object construct method.

Dealing with constructor changes

Ideally, the class constructor is a method that's called automatically when the object instance is created and is used to perform some sort of object initialization. This initialization most typically involves populating object properties with values supplied as arguments to this method. The initialization could also perform any necessary tasks such as opening file handles, establishing a database connection, and so forth.

In PHP 8, a number of changes in how the class constructor is invoked have been made. This means there's a potential for a backwards compatibility break when you migrate your application to PHP 8. The first change we'll examine has to do with deprecated usage of a method with the same name as the class being used as the class constructor.

Handling changes in a method and class of the same name

In the first PHP OOP implementation, introduced in PHP version 4, it was determined that a method with the same name as the class would assume the role of class constructor, and would be automatically called when a new object instance was created.

It's a little known fact that, even in PHP 8, functions, methods, and even class names, are case-insensitive. Thus $a = new ArrayObject(); is equivalent to $b = new arrayobject();. Variable names, on the other hand, are case-sensitive.

Starting with PHP 5, along with a new and much more robust OOP implementation, magic methods were introduced. One of these methods is __construct(), specifically reserved for class construction, designed to replace the older usage. Using a method with the same name as the class as a constructor was supported through the remaining versions of PHP 5, and all the way through all versions of PHP 7 as well.

In PHP 8, support for a class constructor method with the same name as the class itself has been removed. If a __construct() method is also defined, you will have no problem: __construct() takes precedence as a class constructor. If there is no __construct() method, and you detect a method with the same name as the class (), you have the potential for failure. Please bear in mind that both method and class names are case-insensitive!

Have a look at the following example. It works in PHP 7 but not in PHP 8:

  1. First, we define a Text class with a class constructor method of the same name. The constructor method creates an SplFileObject instance based upon the supplied filename:

    // /repo/ch05/php8_oop_bc_break_construct.php

    class Text {

        public $fh = '';

        public const ERROR_FN = 'ERROR: file not found';

        public function text(string $fn) {

            if (!file_exists($fn))

                throw new Exception(self::ERROR_FN);

            $this->fh = new SplFileObject($fn, 'r');

        }

        public function getText() {

            return $this->fh->fpassthru();

        }

    }

  2. We then add three lines of procedural code to exercise the class, supplying the filename of a file containing the Gettysburg Address:

    $fn   = __DIR__ . '/../sample_data/gettysburg.txt';

    $text = new Text($fn);

    echo $text->getText();

  3. Running the program in PHP 7 first produces a deprecation notice, followed by the expected text. Only the first few lines of the output are shown here:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php8_bc_break_construct.php

    PHP Deprecated:  Methods with the same name as their class will not be constructors in a future version of PHP; Text has a deprecated constructor in /repo/ch05/php8_bc_break_construct.php on line 4

    Fourscore and seven years ago our fathers brought forth on this continent a new nation, conceived in liberty and dedicated to the proposition that all men are created equal. ... <remaining text not shown>

  4. Running the same program in PHP 8, however, a fatal Error is thrown instead, as you can see from this output:

    root@php8_tips_php8 [ /repo/ch05 ]# php php8_bc_break_construct.php

    PHP Fatal error:  Uncaught Error: Call to a member function fpassthru() on string in /repo/ch05/php8_bc_break_construct.php:16

It's important to note that the error shown in PHP 8 does not tell you the real reason why the program failed. Hence, it's extremely important that you scan your PHP applications, especially older applications, to see if there's a method with the same name as the class. Accordingly, the best practice is to simply rename the method having the same name as the class to __construct().

Now let's have a look at how inconsistencies in handling Exception and exit in the class constructor have been addressed in PHP 8.

Addressing inconsistencies in the class constructor

Another issue addressed in PHP 8 has to do with a situation where the class construct method either throws an Exception, or executes exit(). In PHP versions prior to PHP 8, if an Exception is thrown in the class constructor, the __destruct() method, if defined, is not called. On the other hand, if either exit() or die() (both PHP functions are equivalent to each other) is used in the constructor, the __destruct() method is called. In PHP 8, this inconsistency is addressed. Now, in either case, the __destruct() method is not called.

You may be wondering why this is of concern. The reason why you need to be aware of this important change is that you might have logic residing in the __destruct() method that was called in a situation where you also might call either exit() or die(). In PHP 8, you can no longer rely upon this code, which may cause a backwards compatibility break.

In this example, we have two connection classes. ConnectPdo uses the PDO extension to provide query results, whereas ConnectMysqli uses the MySQLi extension:

  1. We begin by defining an interface specifying a query method. This method requires a SQL string as an argument and is expected to return an array as a result:

    // /repo/src/Php7/Connector/ConnectInterface.php

    namespace Php7Connector;

    interface ConnectInterface {

        public function query(string $sql) : array;

    }

  2. Next, we define a base class in which there is a __destruct() magic method defined. Because this class implements ConnectInterface but doesn't define query(), it's marked abstract:

    // /repo/src/Php7/Connector/Base.php

    namespace Php7Connector;

    abstract class Base implements ConnectInterface {

        const CONN_TERMINATED = 'Connection Terminated';

        public $conn = NULL;

        public function __destruct() {

            $message = get_class($this)

                     . ':' . self::CONN_TERMINATED;

            error_log($message);

        }

    }

  3. Next, we define the ConnectPdo class. It extends Base, and its query() method uses PDO syntax to produce a result. The __construct() method throws a PDOException if there is a problem creating the connection:

    // /repo/src/Php7/Connector/ConnectPdo.php

    namespace Php7Connector;

    use PDO;

    class ConnectPdo extends Base {

        public function __construct(

            string $dsn, string $usr, string $pwd) {

            $this->conn = new PDO($dsn, $usr, $pwd);

        }

        public function query(string $sql) : array {

            $stmt = $this->conn->query($sql);

            return $stmt->fetchAll(PDO::FETCH_ASSOC);

        }

    }

  4. In a similar manner, we define the ConnectMysqli class. It extends Base, and its query() method uses MySQLi syntax to produce a result. The __construct() method executes die() if there is a problem creating the connection:

    // /repo/src/Php7/Connector/ConnectMysqli.php

    namespace Php7Connector;

    class ConnectMysqli extends Base {

        public function __construct(

            string $db, string $usr, string $pwd) {

            $this->conn = mysqli_connect('localhost',

                $usr, $pwd, $db)

                or die("Unable to Connect ");

        }

        public function query(string $sql) : array {

            $result = mysqli_query($this->conn, $sql);

            return mysqli_fetch_all($result, MYSQLI_ASSOC);

        }

    }

  5. Finally, we define a calling program that uses the two connection classes described previously, and defines invalid values for the connection string, username, and password:

    // /repo/ch05/php8_bc_break_destruct.php

    include __DIR__ . '/../vendor/autoload.php';

    use Php7Connector {ConnectPdo,ConnectMysqli};

    $db  = 'test';

    $usr = 'fake';

    $pwd = 'xyz';

    $dsn = 'mysql:host=localhost;dbname=' . $db;

    $sql = 'SELECT event_name, event_date FROM events';

  6. Next, in the calling program, we call both classes and attempt to execute a query. The connection deliberately fails as we supply the wrong username and password:

    $ptn = "%2d : %s : %s ";

    try {

        $conn = new ConnectPdo($dsn, $usr, $pwd);

        var_dump($conn->query($sql));

    } catch (Throwable $t) {

        printf($ptn, __LINE__, get_class($t),

               $t->getMessage());

    }

    $conn = new ConnectMysqli($db, $usr, $pwd);

    var_dump($conn->query($sql));

  7. As you now know from our discussion above, the output running in PHP 7 shows the PDOException being thrown from the class constructor when the ConnectPdo instance is created. On the other hand, when the ConnectMysqli instance fails, die() is called, with the message Unable to Connect. You also see, on the very last line of the output, the error log information originating from the __destruct() method. Here is that output:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php8_bc_break_destruct.php

    15 : PDOException : SQLSTATE[28000] [1045] Access denied for user 'fake'@'localhost' (using password: YES)

    PHP Warning:  mysqli_connect(): (HY000/1045): Access denied for user 'fake'@'localhost' (using password: YES) in /repo/src/Php7/Connector/ConnectMysqli.php on line 8

    Unable to Connect

    Php7ConnectorConnectMysqli:Connection Terminated

  8. In PHP 8, the __destruct() method is not called in either case, resulting in the output shown here. As you can see in the output, the PDOException is caught, and the die() command is issued. There is no output from the __destruct() method. The PHP 8 output is shown here:

    root@php8_tips_php8 [ /repo/ch05 ]#

    php php8_bc_break_destruct.php

    15 : PDOException : SQLSTATE[28000] [1045] Access denied for user 'fake'@'localhost' (using password: YES)

    PHP Warning:  mysqli_connect(): (HY000/1045): Access denied for user 'fake'@'localhost' (using password: YES) in /repo/src/Php7/Connector/ConnectMysqli.php on line 8

    Unable to Connect

Now that you have an idea how to spot a potential code break with regards to the __destruct() method along with a call to either die() or exit(), let's turn our attention to changes to the __toString() method.

Working with changes to __toString()

The __toString() magic method is invoked when an object is used as a string. A classic example is when you simply echo an object. The echo command expects a string as an argument. When non-string data is provided, PHP performs type juggling to convert the data to string. As an object cannot be readily converted to string, the PHP engine then looks to see if __toString() is defined, and if so, returns its value.

The major change in this magic method is the introduction of Stringable, a brand new interface. The new interface is defined as follows:

interface Stringable {

   public function __toString(): string;

}

Any class running in PHP 8 that defines the __toString() magic method silently implements the Stringable interface. This new behavior doesn't present any serious potential for a code break. However, since the class now implements the Stringable interface, you are no longer allowed to modify the __toString() method signature.

Here is a short example that reveals the new association with the Stringable interface:

  1. In this example, we define a Test class that defines __toString():

    // /repo/ch05/php8_bc_break_magic_to_string.php

    class Test {

        public $fname = 'Fred';

        public $lname = 'Flintstone';

        public function __toString() : string {

            return $this->fname . ' ' . $this->lname;

        }

    }

  2. We then create an instance of the class, followed by a ReflectionObject instance:

    $test = new Test;

    $reflect = new ReflectionObject($test);

    echo $reflect;

The first few lines of output running in PHP 7 (shown here) simply reveal that it's an instance of the Test class:

root@php8_tips_php7 [ /repo/ch05 ]#

php php8_bc_break_magic_to_string.php

Object of class [ <user> class Test ] {

  @@ /repo/ch05/php8_bc_break_magic_to_string.php 3-12

Running the same code example in PHP 8, however, reveals the silent association with the Stringable interface:

root@php8_tips_php8 [ /repo/ch05 ]#

php php8_bc_break_magic_to_string.php

Object of class [ <user> class Test implements Stringable ] {

  @@ /repo/ch05/php8_bc_break_magic_to_string.php 3-12

The output shows that even though you did not explicitly implement the Stringable interface, the association was created at runtime, and is revealed by the ReflectionObject instance.

Tip

For more information on magic methods, have a look at this documentation page: https://www.php.net/manual/en/language.oop5.magic.php.

Now that you have an understanding of the situations where PHP 8 code involving magic methods could cause a code break, let's have a look at changes in the serialization process.

Taking control of serialization

There are many times when native PHP data needs to be stored in a file, or in a database table. The problem with current technology is that direct storage of complex PHP data such as objects or arrays is simply not possible, with some exceptions.

One way to overcome this limitation is to convert the object or array into a string. JSON (JavaScript Object Notation) is often chosen for this reason. Once the data has been converted into a string, it can easily be stored in any file or database. However, there is a problem with formatting objects with JSON. Although JSON is able to represent object properties well enough, it's incapable of directly restoring the original object's class and methods.

To address this deficiency, the PHP language includes two native functions, serialize() and unserialize(), that can easily convert objects or arrays into a string and restore them back to their original state. As wonderful as this sounds, there are a number of issues associated with native PHP serialization.

Before we can properly discuss the problem with the existing PHP serialization architecture, we need to have a closer look at how native PHP serialization works.

Understanding PHP serialization

When a PHP object or array needs to be saved to a non-OOP environment such as a flat file or relational database table, serialize() can be used to flatten an object or array into a string, suitable for storage. Conversely, unserialize() restores the original object or array.

Here is a simple example that demonstrates this concept:

  1. First, we define a class with three properties:

    // /repo/ch05/php8_serialization.php

    class Test  {

        public $name = 'Doug';

        private $key = 12345;

        protected $status = ['A','B','C'];

    }

  2. We then create an instance, serialize the instance, and display the resulting string:

    $test = new Test();

    $str = serialize($test);

    echo $str . " ";

  3. Here is how the serialized object appears:

    O:4:"Test":3:{s:4:"name";s:4:"Doug";s:9:"Testkey"; i:12345;

    s:9:"*status";a:3:{i:0;s:1:"A";i:1;s:1:"B";i:2;s:1:"C";}}

    As you can see from the serialized string, the letter O designates Object, a is for array, s is for string and i is for integer.

  4. We then unserialize the object into a new variable and use var_dump() to examine the two:

    $obj = unserialize($str);

    var_dump($test, $obj);

  5. Placing the var_dump() outputs side by side, you can clearly see that the restored object is identical to the original:

Let's now have a look at the magic methods that supply legacy PHP serialization support: __sleep() and __wakeup().

Understanding the __sleep() magic method

The purpose of the __sleep() magic method is to provide a filter used to prevent certain properties from appearing in the serialized string. To use a user object as an example, you may wish to exclude sensitive properties such as a national identification number, credit card number, or password from the serialization.

Here is an example using the __sleep() magic method to exclude a password:

  1. First, we define a Test class with three properties:

    // /repo/ch05/php8_serialization_sleep.php

    class Test  {

        public $name = 'Doug';

        protected $key = 12345;

        protected $password = '$2y$10$ux07vQNSA0ctbzZcZNA'

             . 'lxOa8hi6kchJrJZzqWcxpw/XQUjSNqacx.';

  2. We then define a __sleep() method that excludes the $password property:

        public function __sleep() {

            return ['name','key'];

        }

    }

  3. We then create an instance of this class and serialize it. The last line echoes the state of the serialized string:

    $test = new Test();

    $str = serialize($test)

    echo $str . " ";

  4. In the output, you can clearly see that the $password property is not present. Here is the output:

    O:4:"Test":2:{s:4:"name";s:4:"Doug";s:6:"*key";i:12345;}

This is important in that, in most cases, the reason you need to serialize an object is you wish to store it somewhere, whether that be in a session file or in a database. If the filesystem or database is subsequently compromised, you have one less security vulnerability to worry about!

Understanding a potential code break in the __sleep() method

There is a potential code break involving the __sleep() magic method. In versions prior to PHP 8, if __sleep() returns an array with non-existent properties, they are still serialized and assigned a value of NULL. The problem with this approach is that when the object is subsequently unserialized, an extra property now appears, one that is not there by design!

In PHP 8, non-existent properties in the __sleep() magic method return are silently ignored. If your legacy code anticipates the old behavior and takes steps to delete the unwanted property, or even worse, if your code assumes the unwanted property exists, you will ultimately have an error. Such assumptions are extremely dangerous as they can lead to unexpected code behavior.

To illustrate the issue, have a look at the following code example:

  1. First, we define a Test class that defines __sleep() to return a variable that doesn't exist:

    class Test {

        public $name = 'Doug';

        public function __sleep() {

            return ['name', 'missing'];

        }

    }

  2. Next, we create an instance of Test and serialize it:

    echo "Test instance before serialization: ";

    $test = new Test();

    var_dump($test);

  3. We then unserialize the string into a new instance, $restored:

    echo "Test instance after serialization: ";

    $stored = serialize($test);

    $restored = unserialize($stored);

    var_dump($restored);

  4. In theory, the two object instances $test and $restored should be the same. However, have a look at the output running in PHP 7:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php8_bc_break_sleep.php

    Test instance before serialization:

    /repo/ch05/php8_bc_break_sleep.php:13:

    class Test#1 (1) {

      public $name =>  string(4) "Doug"

    }

    Test instance after serialization:

    PHP Notice:  serialize(): "missing" returned as member variable from __sleep() but does not exist in /repo/ch05/php8_bc_break_sleep.php on line 16

    class Test#2 (2) {

      public $name =>  string(4) "Doug"

      public $missing =>  NULL

    }

  5. As you can see from the output, the two objects are clearly not the same! However, in PHP 8, the non-existent property is ignored. Have a look at the same script running in PHP 8:

    root@php8_tips_php8 [ /repo/ch05 ]# php php8_bc_break_sleep.php

    Test instance before serialization:

    object(Test)#1 (1) {

      ["name"]=>  string(4) "Doug"

    }

    Test instance after serialization:

    PHP Warning:  serialize(): "missing" returned as member variable from __sleep() but does not exist in /repo/ch05/php8_bc_break_sleep.php on line 16

    object(Test)#2 (1) {

      ["name"]=>  string(4) "Doug"

    }

You might also observe that in PHP 7, a Notice is issued, whereas in PHP 8, the same situation produces a Warning. A pre-migration check for a potential code break in this case is difficult because you would need to determine, if the magic method __sleep() is defined, whether or not a non-existent property is being included in the list.

Let's now have a look at the counterpart method, __wakeup().

Learning about __wakeup()

The purpose of the __wakeup() magic method is mainly to perform additional initialization on the unserialized object. Examples would be to restore a database connection or reinstate a file handle. Here's a very simple example that uses __wakeup() magic to re-open a file handle:

  1. First, we define a class that opens a file handle upon instantiation. We also define a method that returns the contents of the file:

    // /repo/ch05/php8_serialization_wakeup.php

    class Gettysburg {

        public $fn = __DIR__ . '/gettysburg.txt';

        public $obj = NULL;

        public function __construct() {

            $this->obj = new SplFileObject($this->fn, 'r');

        }

        public function getText() {

            $this->obj->rewind();

            return $this->obj->fpassthru();

        }

    }

  2. To use the class, create an instance, and run getText(). (This assumes that the file referenced by $this->fn exists!)

    $old = new Gettysburg();

    echo $old->getText();

  3. The output (not shown) is the Gettysburg Address.
  4. If we now attempt to serialize this object, a problem arises. Here's an example of code that would serialize the object:

    $str = serialize($old);

  5. At this point, running the code in place so far, here is the output:

    PHP Fatal error:  Uncaught Exception: Serialization of 'SplFileObject' is not allowed in /repo/ch05/php8_serialization_wakeup.php:19

  6. In order to fix this problem, we return to the class and add a __sleep() method that prevents the SplFileObject instance from being serialized:

        public function __sleep() {

            return ['fn'];

        }

  7. If we then rerun the code to serialize the object, all is well. Here is the code to unserialize and call getText():

    $str = serialize($old);

    $new = unserialize($str);

    echo $new->getText();

  8. However, if we then attempt to unserialize the object, another error occurs:

    PHP Fatal error:  Uncaught Error: Call to a member function rewind() on null in /repo/ch05/php8_serialization_wakeup.php:13

    The problem, of course, is that the file handle was lost during serialization. When the object was unserialized, the __construct() method was not called.

  9. This is exactly why the __wakeup() magic method exists. To resolve the error, we define a __wakeup() method that calls the __construct() method:

        public function __wakeup() {

            self::__construct();

        }

  10. If we rerun the code, we now see the Gettysburg Address twice (not shown).

Now you have an idea of how PHP native serialization works, and also know a bit about the __sleep() and __wakeup() magic methods, as well as potential code breaks. Let's now have a look at an interface that was designed to facilitate the custom serialization of objects.

Introducing the Serializable interface

In order to facilitate the serialization of objects, the Serializable interface was added to the language beginning with PHP 5.1. The idea behind this interface was to provide a way of identifying objects that had the ability to serialize themselves. In addition, the methods specified by this interface were designed to provide some degree of control over object serialization.

As long as a class implements this interface, developers are assured that two methods are defined: serialize() and unserialize(). Here is the interface definition:

interface Serializable {

    public serialize () : string|null

    public unserialize (string $serialized) : void

}

Any class that implements this interface has its custom serialize() and unserialize() methods automatically invoked during native serialization or unserialization. To illustrate this technique, consider the following example:

  1. First, we define a class that implements the Serializable interface. The class defines three properties – two of type string, the other representing date and time:

    // /repo/ch05/php8_bc_break_serializable.php

    class A implements Serializable {

        private $a = 'A';

        private $b = 'B';

        private $u = NULL;

  2. We then define a custom serialize() method that initializes the date and time before serializing the object's properties. The unserialize() method restores values to all properties:

        public function serialize() {

            $this->u = new DateTime();

            return serialize(get_object_vars($this));

        }

        public function unserialize($payload) {

            $vars = unserialize($payload);

            foreach ($vars as $key => $val)

                $this->$key = $val;

        }

    }

  3. We then create an instance and examine its contents using var_dump():

    $a1 = new A();

    var_dump($a1);

  4. The output from var_dump() shows us that the u property is not yet initialized:

    object(A)#1 (3) {

      ["a":"A":private]=> string(1) "A"

      ["b":"A":private]=> string(1) "B"

      ["u":"A":private]=> NULL

    }

  5. We then serialize it, and restore it to a variable, $a2:

    $str = serialize($a1);

    $a2 = unserialize($str);

    var_dump($a2);

  6. From the var_dump() output below, you can see that the object has been fully restored. In addition, we know that the custom serialize() method was invoked because the u property is initialized with a date and time value. Here is the output:

    object(A)#3 (3) {

      ["a":"A":private]=> string(1) "A"

      ["b":"A":private]=> string(1) "B"

      ["u":"A":private]=> object(DateTime)#4 (3) {

        ["date"]=> string(26) "2021-02-12 05:35:10.835999"

        ["timezone_type"]=> int(3)

        ["timezone"]=> string(3) "UTC"

      }

    }

Let's now have a look at issues with the serialization process for objects that implement the Serializable interface.

Examining PHP serializable interface issues

There is an overall problem with the earlier approach to serialization. If a class to be serialized has defined a __wakeup() magic method, it's not invoked immediately upon unserialization. Rather, any defined __wakeup() magic methods are first queued up, the entire chain of objects is unserialized, and only then are methods in the queue executed. This can result in a mismatch between what is seen by an object's unserialize() method compared to what is seen by its queued __wakeup() method.

This architectural flaw can result in inconsistent behavior and ambiguous results when dealing with objects that implement the Serializable interface. Many developers consider the Serializable interface to be severely broken due to the need to create back references when the serialization of nested objects occurs. This need arises in situations where nested serialization calls occur.

Such nested calls might occur, for example, when a class defines a method that in turn calls the PHP serialize() function. The order in which back references are created is preset in PHP serialization prior to PHP 8, potentially causing an avalanche of cascading failures.

The solution is to use two new magic methods to give you complete control over serialization and unserialization sequencing, described next.

New magic methods to control PHP serialization

A new way of controlling serialization was first introduced in PHP 7.4 and carried over into PHP 8. In order to take advantage of this new technology, all you need to do is to implement two magic methods: __serialize() and __unserialize(). If implemented, PHP turns control over serialization entirely to the __serialize() method. Likewise, unserialization is entirely controlled by the __unserialize() magic method. The __sleep() and __wakeup() methods, if defined, are ignored.

As a further benefit, PHP 8 provides full support for the two new magic methods in the following SPL classes:

  • ArrayObject
  • ArrayIterator
  • SplDoublyLinkedList
  • SplObjectStorage

    Best practice

    To gain full control over serialization, implement the new __serialize() and __unserialize() magic methods. You no longer need to implement the Serializable interface, nor do you need to define __sleep() and __wakeup(). For more information on the eventual discontinuation of the Serializable interface, see this RFC: https://wiki.php.net/rfc/phase_out_serializable.

As an example of the new PHP serialization usage, consider the following code example:

  1. In the example, a Test class is initialized with a random key upon instantiation:

    // /repo/ch05/php8_bc_break_serialization.php

    class Test extends ArrayObject {

        protected $id = 12345;

        public $name = 'Doug';

        private $key = '';

        public function __construct() {

            $this->key = bin2hex(random_bytes(8));

        }

  2. We add a getKey() method that reveals the current key value:

        public function getKey() {

            return $this->key;

        }

  3. When serialized, the key is filtered out of the resulting string:

        public function __serialize() {

            return ['id' => $this->id,

                    'name' => $this->name];

        }

  4. Upon unserialization, a new key is generated:

        public function __unserialize($data) {

            $this->id = $data['id'];

            $this->name = $data['name'];

            $this->__construct();

        }

    }

  5. We now create an instance, and reveal the key:

    $test = new Test();

    echo " Old Key: " . $test->getKey() . " ";

    Here is how the key might appear:

    Old Key: mXq78DhplByDWuPtzk820g==

  6. We add code to serialize the object and display the string:

    $str = serialize($test);

    echo $str . " ";

    Here is how the serialized string might appear:

    O:4:"Test":2:{s:2:"id";i:12345;s:4:"name";s:4:"Doug";}

    Note from the output that the secret does not appear in the serialized string. This is important because if the storage location of the serialized string is compromised, a security vulnerability might be exposed, giving an attacker a way to break into your system.

  7. We then add code to unserialize the string and reveal the key:

    $obj = unserialize($str);

    echo "New Key: " . $obj->getKey() . " ";

    Here is the last bit of output. Notice that a new key has been generated:

    New Key: kDgU7FGfJn5qlOKcHEbyqQ==

As you can see, using the new PHP serialization feature is not complicated. Any timing issues are now fully in your control because the new magic methods are executed in the order in which the objects are serialized and unserialized.

Important note

PHP 7.4 and above understands serialized strings from older versions of PHP, however, strings serialized by PHP 7.4 or 8.x might not be properly unserialized by older versions of PHP.

Tip

For a full discussion, please see the RFC on custom serialization:

https://wiki.php.net/rfc/custom_object_serialization

You now have a full understanding of PHP serialization and the improved support provided by the two new magic methods. It's now time to shift gears and examine how PHP 8 expands variance support.

Understanding PHP 8 expanded variance support

The concept of variance is at the heart of OOP. Variance is an umbrella term that covers how the various subtypes interrelate. Some 20 years ago, a pair of early computer scientists, Wing and Liskov, devised an important theorem that is at the heart of OOP subtypes, now known as the Liskov Substitution Principle.

Without going into the precise mathematics, this principle can be paraphrased as follows:

Class X can be considered a subtype of class Y if you are able to substitute an instance of X in place of an instance of Y, and the application's behavior does not change in any way.

Tip

The actual paper that first described and provided the precise mathematical formulaic definition of the Liskov Substitution Principle can be found here: A behavioral notion of subtyping, ACM Transactions on Programming Languages and Systems, by B. Liskov and J. Wing, November 1994 (https://dl.acm.org/doi/10.1145/197320.197383).

In this section, we examine how PHP 8 provides enhanced variance support in the form of covariant returns and contraviariant parameters. An understanding of covariance and contravariance will increase your ability to write good solid code. Without this understanding, your code might produce inconsistent results and become the source of many bugs.

Let's start by covering covariant returns.

Understanding covariant returns

Covariance support in PHP is designed to preserve the ordering of types from the most specific to the most general. A classic example of this is seen in how try / catch blocks are formulated:

  1. In this example, a PDO instance is created inside the try block. The following two catch blocks look first for a PDOException. Following this is a second catch block that catches any class that implements Throwable. Because both the PHP Exception and Error classes implement Throwable, the second catch block ends up as a fallback for any error other than a PDOException:

    try {

        $pdo = new PDO($dsn, $usr, $pwd, $opts);

    } catch (PDOException $p) {

        error_log('Database Error: ' . $p->getMessage());

    } catch (Throwable $t) {

        error_log('Unknown Error: ' . $t->getMessage());

    }

  2. In this example, if a PDO instance fails due to invalid parameters, the error log would have the entry Database Error followed by a message gleaned from the PDOException.
  3. On the other hand, if some other general error occurred, the error log would have the entry Unknown Error followed by the message coming from some other Exception or Error class.
  4. In this example, however, the order of the catch blocks is reversed:

    try {

        $pdo = new PDO($dsn, $usr, $pwd, $opts);

    } catch (Throwable $t) {

        error_log('Unknown Error: ' . $t->getMessage());

    } catch (PDOException $p) {

        error_log('Database Error: ' . $p->getMessage());

    }

  5. Due to the way PHP covariance support works, the second catch block would never be invoked. Instead, all error log entries originating from this block of code would have an entry starting with Unknown Error.

Let's now have a look at how PHP covariance support applies to object method return data types:

  1. First, we define an interface, FactoryIterface, that identifies a method, make(). This method accepts an array as an argument and is expected to return an object of type ArrayObject:

    interface FactoryInterface {

        public function make(array $arr): ArrayObject;

    }

  2. Next, we define an ArrTest class that extends ArrayObject:

    class ArrTest extends ArrayObject {

        const DEFAULT_TEST = 'This is a test';

    }

  3. The ArrFactory class implements FactoryInterface and fully defines the make() method. Note, however, that this method returns the ArrTest data type and not ArrayObject:

    class ArrFactory implements FactoryInterface {

        protected array $data;

        public function make(array $data): ArrTest {

            $this->data = $data;

            return new ArrTest($this->data);

        }

    }

  4. In the block of procedural calling code, we create an instance of ArrFactory, and run its make() method twice, theoretically producing two ArrTest instances. We then use var_dump() to reveal the current state of the two objects produced:

    $factory = new ArrFactory();

    $obj1 = $factory->make([1,2,3]);

    $obj2 = $factory->make(['A','B','C']);

    var_dump($obj1, $obj2);

  5. In PHP 7.1, as it does not support covariant return data types, a fatal Error is thrown. The output, shown here, tells us that the method return type declaration doesn't match what's been defined in FactoryInterface:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php8_variance_covariant.php

    PHP Fatal error:  Declaration of ArrFactory::make(array $data): ArrTest must be compatible with FactoryInterface::make(array $arr): ArrayObject in /repo/ch05/php8_variance_covariant.php on line 9

  6. When we run the same code in PHP 8, you can see that covariance support is provided for return types. Execution proceeds unhindered, as shown here:

    root@php8_tips_php8 [ /repo/ch05 ]#

    php php8_variance_covariant.php

    object(ArrTest)#2 (1) {

      ["storage":"ArrayObject":private]=>

      array(3) {

        [0]=>    int(1)

        [1]=>    int(2)

        [2]=>    int(3)

      }

    }

    object(ArrTest)#3 (1) {

      ["storage":"ArrayObject":private]=>

      array(3) {

        [0]=>    string(1) "A"

        [1]=>    string(1) "B"

        [2]=>    string(1) "C"

      }

    }

ArrTest extends ArrayObject and is a suitable subtype that clearly meets the criteria defined by the Liskov Substitution Principle. As you can see from the last output, PHP 8 more fully embraces true OOP principles than the earlier versions of PHP. The end result is that your code and application architecture can be much more intuitive and logically reasonable when using PHP 8.

Let's now have a look at contravariant parameters.

Using contravariant parameters

Whereas covariance concerns the ordering of subtypes from general to specific, contravariance concerns the reverse: from specific to general. In PHP 7 and earlier, full support for contravariance was not available. Accordingly, implementing an interface or extending an abstract class, in PHP 7, parameter type hints are invariant.

In PHP 8, on the other hand, due to support for contravariant parameters, you are free to be specific in top-level super classes and interfaces. As long as the subtype is compatible, you can then modify the type hint in the extending or implementing class to be more general.

This gives you much more freedom in defining an overall architecture where you define interfaces or abstract classes. Developers using your interfaces or super classes are given a great deal more flexibility in PHP 8 when it comes to implementing descendent class logic.

Let's have a look at how PHP 8 support for contravariant parameters works:

  1. In this example, we first define a IterObj class that extends the built-in ArrayIterator PHP class:

    // /repo/ch05/php8_variance_contravariant.php

    class IterObj extends ArrayIterator {}

  2. We then define an abstract Base class that mandates a method, stringify(). Note that the data type for its only argument is IterObj:

    abstract class Base {

        public abstract function stringify(IterObj $it);

    }

  3. Next, we define a IterTest class that extends Base and provides an implementation for the stringify() method. Of particular interest is that we override the data type, changing it to iterable:

    class IterTest extends Base {

        public function stringify(iterable $it) {

            return implode(',',

                iterator_to_array($it)) . " ";

        }

    }

    class IterObj extends ArrayIterator {}

  4. The next few lines of code create instances of IterTest, IterObj, and ArrayIterator. We then invoke the stringify() method twice, supplying each of the latter objects as an argument:

    $test  = new IterTest();

    $objIt = new IterObj([1,2,3]);

    $arrIt = new ArrayIterator(['A','B','C']);

    echo $test->stringify($objIt);

    echo $test->stringify($arrIt);

  5. Running this code example in PHP 7.1 produces the expected fatal Error as shown here:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php8_variance_contravariant.php

    PHP Fatal error:  Declaration of IterTest::stringify(iterable $it) must be compatible with Base::stringify(IterObj $it) in /repo/ch05/php8_variance_contravariant.php on line 11

    Because PHP 7.1 does not provide support for contravariant parameters, it treats the data type for its parameters as invariant, and simply displays a message indicating that the data type of the child class is incompatible with the data type specified in the parent class.

  6. PHP 8, on the other hand, provides support for contravariant parameters. Accordingly, it recognizes that IterObj, the data type specified in the Base class, is a subtype compatible with iterable. Further, both arguments provided are compatible with iterable as well, allowing program execution to proceed. Here is the PHP 8 output:

    root@php8_tips_php8 [ /repo/ch05 ]# php php8_variance_contravariant.php

    1,2,3

    A,B,C

The main advantage you derive from PHP 8 support for covariant returns and contravariant parameters is the ability to override not only method logic but the method signature as well. You will find that although PHP 8 is much stricter in its enforcement of good coding practices, the enhanced variance support gives you greater freedom in designing your inheritance structure. In a certain sense, at least with regards to parameter and return value data types, PHP 8 is, if anything, less restrictive!

Tip

For a full explanation of how variance support is applied in PHP 7.4 and PHP 8, have a look here: https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters.

We'll now have a look at changes to the SPL and how those changes can have an impact on application performance after migrating to PHP 8.

Handling Standard PHP Library (SPL) changes

The SPL is an extension that contains key classes that implement basic data structures and enhance OOP functionality. It was first introduced in PHP 5 and is now included by default in all PHP installations. Covering the entire SPL is beyond the scope of this book. Instead, in this section, we discuss where significant changes have occurred in the SPL when running PHP 8. In addition, we give you tips and guidance on SPL changes that have the potential to cause your existing applications to stop working.

We start by examining changes to the SplFileObject class.

Understanding changes to SplFileObject

SplFileObject is an excellent class that incorporates most of the standalone f*() functions, such as fgets(), fread(), fwrite(), and so forth, into a single class. SplFileObject ::__construct() method arguments mirror the arguments provided to the fopen() function.

The main difference in PHP 8 is that a relatively obscure method, fgetss(), has been removed from the SplFileObject class. The SplFileObject::fgetss() method, available in PHP 7 and below, mirrors the standalone fgetss() function in that it combines fgets() with strip_tags().

For the sake of illustration, let's assume you have created a website that allows users to upload text files. Before displaying content from the text file, you wish to remove any markup tags. Here is an example that uses the fgetss() method to accomplish this:

  1. We first define a block of code that acquires the filename:

    // /repo/ch05/php7_spl_splfileobject.php

    $fn = $_GET['fn'] ?? '';

    if (!$fn || !file_exists($fn))

        exit('Unable to locate file');

  2. We then create the SplFileObject instance, and read the file line by line using the fgetss() method. At the end, we echo the safe contents:

    $obj = new SplFileObject($fn, 'r');

    $safe = '';

    while ($line = $obj->fgetss()) $safe .= $line;

    echo '<h1>Contents</h1><hr>' . $safe;

  3. Let's say that the file to be read is this:

    <h1>This File is Infected</h1>

    <script>alert('You Been Hacked');</script>

    <img src="http://very.bad.site/hacked.php" />

  4. Here is the output running in PHP 7.1 using this URL:

    http://localhost:7777/ch05/php7_spl_splfileobject.php? fn=includes/you_been_hacked.html

As you can see from the output shown next, all HTML markup tags have been removed:

Figure 5.1 – Result after reading a file using SplFileObject::fgetss()

Figure 5.1 – Result after reading a file using SplFileObject::fgetss()

To accomplish the same thing in PHP 8, the code shown previously would need to be modified by replacing fgetss() with fgets(). We would also need to use strip_tags() on the line concatenated to $safe. Here is how the modified code might appear:

// /repo/ch05/php8_spl_splfileobject.php

$fn = $_GET['fn'] ?? '';

if (!$fn || !file_exists($fn))

    exit('Unable to locate file');

$obj = new SplFileObject($fn, 'r');

$safe = '';

while ($line = $obj->fgets())

    $safe .= strip_tags($line);

echo '<h1>Contents</h1><hr>' . $safe;

The output from the modified code is identical to that shown in Figure 5.1. We'll now turn our attention to changes in another SPL class: SplHeap.

Examining changes to SplHeap

SplHeap is a foundational class used to represent data structured as a binary tree. Two additional classes are also available that build upon SplHeap. SplMinHeap organizes the tree with the minimum value at the top. SplMaxHeap does the reverse, placing the maximum value at the top.

A heap structure is especially useful in situations where data arrives out of order. Once inserted into the heap, the item is automatically placed in its proper order. Thus, at any given moment, you can display the heap safe in the knowledge that all items will be in order without having to run one of the PHP sort functions.

The key to maintaining the automatic sort order is to define an abstract method, compare(). As this method is abstract, SplHeap cannot be instantiated directly. Instead, you need to extend the class and implement compare().

There is the potential for a backward-compatible code break when using SplHeap in PHP 8 as the method signature for compare() must be exactly as follows: SplHeap::compare($value1, $value2).

Let's now have a look at a code example that uses SplHeap to build a list of billionaires organized by last name:

  1. First, we define a file with data on billionaires. In this example, we simply copied and pasted data from this source: https://www.bloomberg.com/billionaires/.
  2. We then define a BillionaireTracker class that extracts information from the pasted text into an array of ordered pairs. The full source code (not shown here) for the class can be found in the source code repository here:/repo/src/Services/BillionaireTracker.php.

    Here is how the data produced by the class appears:

    array(20) {

      [0] =>  array(1) {

        [177000000000] =>    string(10) "Bezos,Jeff"

      }

      [1] =>  array(1) {

        [157000000000] =>    string(9) "Musk,Elon"

      }

      [2] =>  array(1) {

        [136000000000] =>    string(10) "Gates,Bill"

      }

      ... remaining data not shown

    As you can see, the data is presented in descending order where the key represents net worth. In contrast, in our sample program, we plan to produce data in ascending order by last name.

  3. We then define a constant that identifies the billionaire data source file, and set up an autoloader:

    // /repo/ch05/php7_spl_splheap.php

    define('SRC_FILE', __DIR__

        . '/../sample_data/billionaires.txt');

    require_once __DIR__

        . '/../src/Server/Autoload/Loader.php';

    $loader = new ServerAutoloadLoader();

  4. Next, we create an instance of the BillionaireTracker class and assign the results to $list:

    use ServicesBillionaireTracker;

    $tracker = new BillionaireTracker();

    $list = $tracker->extract(SRC_FILE);

  5. Now comes the part of most interest: creating the heap. To accomplish this, we define an anonymous class that extends SplHeap. We then define a compare() method that performs the necessary logic to place inserted elements in their proper place. PHP 7 allows you to change the method signature. In this example, we provide arguments in the form of an array:

    $heap = new class () extends SplHeap {

        public function compare(

            array $arr1, array $arr2) : int {

            $cmp1 = array_values($arr2)[0];

            $cmp2 = array_values($arr1)[0];

            return $cmp1 <=> $cmp2;

        }

    };

    You might also note that the value for $cmp1 is assigned from the second array, and the value for $cmp2 is from the first array. The reason for this switch is because we wish to produce results in ascending order.

  6. We then use SplHeap::insert() to add elements to the heap:

    foreach ($list as $item)

        $heap->insert($item);

  7. Finally, we define a BillionaireTracker::view() method (not shown) to run through the heap and display results:

    $patt = "%20s %32s ";

    $line = str_repeat('-', 56) . " ";

    echo $tracker->view($heap, $patt, $line);

  8. Here is the output produced by our little program running in PHP 7.1:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php7_spl_splheap.php

    --------------------------------------------------------

               Net Worth                                Name

    --------------------------------------------------------

          84,000,000,000                       Ambani,Mukesh

         115,000,000,000                     Arnault,Bernard

          83,600,000,000                       Ballmer,Steve

          ... some lines were omitted to save space ...

          58,200,000,000                          Walton,Rob

         100,000,000,000                     Zuckerberg,Mark

    --------------------------------------------------------

                                           1,795,100,000,000

    --------------------------------------------------------

You will note, however, that when we attempt to run the same program in PHP 8, an error is thrown. Here is the output of the same program running in PHP 8:

root@php8_tips_php8 [ /repo/ch05 ]# php php7_spl_splheap.php

PHP Fatal error:  Declaration of SplHeap@anonymous::compare(array $arr1, array $arr2): int must be compatible with SplHeap::compare(mixed $value1, mixed $value2) in /repo/ch05/php7_spl_splheap.php on line 16

Accordingly, to get this working properly, we must redefine the anonymous class that extends SplHeap. Here is a modified version of that portion of the code:

$heap = new class () extends SplHeap {

    public function compare($arr1, $arr2) : int {

        $cmp1 = array_values($arr2)[0];

        $cmp2 = array_values($arr1)[0];

        return $cmp1 <=> $cmp2;

    }

};

The only change is in the compare() method signature. When executed, the results (not shown) are identical. The full code for PHP 8 can be viewed at /repo/ch05/php8_spl_splheap.php.

This concludes our discussion of changes to the SplHeap class. Please note that the same change also applies to SplMinHeap and SplMaxHeap. Let's now have a look at a potentially significant change in the SplDoublyLinkedList class.

Handling changes in SplDoublyLinkedList

The SplDoublyLinkedList class is an iterator that's able to display information in either FIFO (First-In, First-Out) or LIFO (Last-In, First-Out) order. It's more common, however, to say that you can iterate through the list in either forward or reverse order.

This is a very powerful addition to any developer's library. To do the same thing with ArrayIterator, for example, would require at least a dozen lines of code! Accordingly, PHP developers like to use this class for situations where they need to navigate a list in either direction at will.

Unfortunately, there is a potential code break due to a difference in the return value of the push() and unshift() methods. The push() method is used to add a value at the end of the list. The unshift() method, on the other hand, adds value to the beginning of the list.

In PHP 7 and below, these methods, if successful, returned Boolean TRUE. If the method failed, it returned Boolean FALSE. In PHP 8, however, neither method returns a value. If you look at the method signature in the current documentation, you will see a return data type of void. The potential code break can arise where you check to return a value of either push() or unshift() before continuing.

Let's have a look at a simple example that populates a doubly linked list with a simple list of five values, and displays them in both FIFO and LIFO order:

  1. First, we define an anonymous class that extends SplDoublyLinkedList. We also add a show() method that displays the contents of the list:

    // /repo/ch05/php7_spl_spldoublylinkedlist.php

    $double = new class() extends SplDoublyLinkedList {

        public function show(int $mode) {

            $this->setIteratorMode($mode);

            $this->rewind();

            while ($item = $this->current()) {

                echo $item . '. ';

                $this->next();

            }

        }

    };

  2. Next, we define an array of sample data, and use push() to insert the value into the linked list. Note that an if() statement is used to determine whether the operation succeeds or fails. If the operation fails, an Exception is thrown:

    $item = ['Person', 'Woman', 'Man', 'Camera', 'TV'];

    foreach ($item as $key => $value)

        if (!$double->push($value))

            throw new Exception('ERROR');

    This is the block of code where the potential code break exists. In PHP 7 and below, push() returns TRUE or FALSE. In PHP 8, there is no return value.

  3. We then use the SplDoublyLinkedList class constants to set the mode to FIFO (forward), and display the list:

    echo "**************** Foward ******************** ";

    $forward = SplDoublyLinkedList::IT_MODE_FIFO

             | SplDoublyLinkedList::IT_MODE_KEEP;

    $double->show($forward);

  4. Next, we use the SplDoublyLinkedList class constants to set the mode to LIFO (reverse), and display the list:

    echo " ************* Reverse ***************** ";

    $reverse = SplDoublyLinkedList::IT_MODE_LIFO

             | SplDoublyLinkedList::IT_MODE_KEEP;

    $double->show($reverse);

    Here is the output running in PHP 7.1:

    root@php8_tips_php7 [ /repo/ch05 ]#

    php php7_spl_spldoublylinkedlist.php

    **************** Foward ********************

    Person. Woman. Man. Camera. TV.

    **************** Reverse ********************

    TV. Camera. Man. Woman. Person.

  5. If we run the same code in PHP 8, this is the result:

    root@php8_tips_php8 [ /home/ch05 ]#

    php php7_spl_spldoublylinkedlist.php

    PHP Fatal error:  Uncaught Exception: ERROR in /home/ch05/php7_spl_spldoublylinkedlist.php:23

If no value is returned by push(), inside the if() statement PHP assumes NULL, which in turn is interpolated as Boolean FALSE! Accordingly, after the first push() command, the if() block causes an Exception to be thrown. Because the Exception is not caught, a fatal Error is generated.

To rewrite this block of code to work in PHP 8, all you need to do is to remove the if() statement, and not throw an Exception. Here's how the rewritten code block (shown in Step 2) might appear:

$item = ['Person', 'Woman', 'Man', 'Camera', 'TV'];

foreach ($item as $key => $value)

    $double->push($value);

Now, if we execute the rewritten code, the results are seen here:

root@php8_tips_php7 [ /home/ch05 ]#

php php8_spl_spldoublylinkedlist.php

**************** Foward ********************

Person. Woman. Man. Camera. TV.

**************** Reverse ********************

TV. Camera. Man. Woman. Person.

Now you have an idea of how to use SplDoublyLinkedList, and also know about the potential code break relating to push() or unshift(). You also have an idea about potential code breaks when using various SPL classes and functions in PHP 8. This concludes our discussion for this chapter.

Summary

In this chapter, you learned about potential problems in OOP code when migrating to PHP 8. In the first section, you learned how a number of bad practices were allowed in PHP 7 and earlier versions, but now represent a potential code break in PHP 8. With this knowledge, you are a better developer and can deliver high-quality code to benefit your company.

In the next section, you learned good habits when using magic methods. Potential code breaks can occur because PHP 8 now enforces a degree of consistency not seen in earlier versions of PHP. These inconsistencies involve class constructor usage and certain aspects of magic method usage. The following section taught you about PHP serialization and how changes made in PHP 8 can make your code more resilient and less vulnerable to errors or attacks during the serialize and unserialize process.

In this chapter, you also learned about enhanced PHP 8 support for covariant return types and contravariant parameters. Having knowledge of variance, and how support has improved in PHP 8, allows you to be more creative and flexible when developing class inheritance structures in PHP 8. You now know how to write code that was simply not possible in earlier versions of PHP.

The last section covered a number of key classes in the SPL. You learned a great deal about how basic data structures such as heap and linked lists can be implemented in PHP 8. The information in that section was critical in helping you to avoid problems with code involving the SPL.

The next chapter continues the discussion on potential code breaks. The emphasis in the next chapter, however, is on procedural rather than object code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.119.199