Chapter 11: Migrating Existing PHP Apps to PHP 8

Throughout the book, you have been warned of potential code breaks. Unfortunately, there are not really any good tools available that can scan your existing code and check for potential code breaks. In this chapter, we take you through the development of a set of classes that form the basis of a PHP 8 backward-compatible (BC) break scanner. In addition, you learn the recommended process to migrate an existing customer PHP application to PHP 8.

After reading through this chapter and carefully studying the examples, you are much better equipped to handle a PHP 8 migration. With knowledge of the overall migration procedure, you gain confidence and are able to perform PHP 8 migrations with a minimal number of problems.

The topics covered in this chapter include the following:

  • Understanding development, staging, and production environments
  • Learning how to spot BC breaks before a migration
  • Performing the migration
  • Testing and troubleshooting the migration

Technical requirements

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

  • An x86_64-based desktop PC or laptop
  • 1 gigabyte (GB) free disk space
  • 4 GB of RAM
  • 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 the 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 at https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices. We can now begin our discussion by having a look at environments used as part of the overall migration process.

Understanding development, staging, and production environments

The ultimate goal for a website update is to move the updated application code from development to production in as seamless a manner as possible. This movement of application code is referred to as deployment. Movement, in this context, involves copying application code and configuration files from one environment to another.

Before we get into the details of migrating an application to PHP 8, let's first have a look at what these environments are. Gaining an understanding of what form the different environments might take is critical to your role as a developer. With this understanding, you are in a better position to deploy your code to production with a minimal amount of errors.

Defining an environment

We use the word environment to describe a combination of software stacks that include the operating system, web server, database server, and PHP installation. In the past, the environment equated to a server. In this modern age, however, the term server is deceptive in that it implies a physical computer in a metal box sitting on a rack in some unseen server room. Today, this is more likely not going to be the case, given the abundance of cloud service providers and highly performant virtualization technologies (for example, Docker). Accordingly, when we use the term environment, understand this to mean either a physical or virtual server.

Environments are generally classified into three distinct categories: development, staging, and production. Some organizations also provide a separate testing environment. Let's first have a look at what is common across all environments.

Common components

It's important to note that what goes into all environments is driven by what is in the production environment. The production environment is the final destination of your application code. Accordingly, all other environments should match the operating system, database, web server, and PHP installation as closely as possible. Thus, for example, if the production environment enables the PHP OPCache extension, all other environments must enable this extension as well.

All environments, including the production environment, need to have an operating system and PHP installation at a minimum. Depending on the needs of your application, it's also quite common to have a web server and database server installed. The type and version of the web and database server should match that of the production environment as closely as possible.

As a general rule, the closer your development environment matches that of the production environment, the less chance there is of a bug cropping up after deployment.

We now look at what goes into a development environment.

Development environment

The development environment is where you initially develop and test your code. It is unique in that it has the tools needed for application maintenance and development. This would include housing a source code repository (for example, Git), as well as various scripts needed to start, stop, and reset the environment.

Often the development environment will have scripts to trigger an automated deployment procedure. Such scripts could take the place of commit hooks, designed to activate when you issue a commit to your source code repository. One example of this is Git Hooks, script files that can be placed in the .git/hooks directory.

Tip

For more information on Git Hooks, have a look at the documentation here: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks.

The traditional development environment consisted of a personal computer with a database server, web server, and PHP. This conventional paradigm fails to take into account the variations that might be present in the target production environment. If you have 12 customers that you work with regularly, for example, it's highly unlikely that all 12 customers have exactly the same OS, database server, web server, and version of PHP! The best practice is to model the production environment as closely as possible in the form of a virtual machine or Docker container.

The code editor or IDE (Integrated Development Environment) is thus not located inside the development environment. Rather, you perform code creation and editing outside of the development environment. You would then push your changes locally either by directly copying files into the virtual development environment via a shared directory, or by committing changes to the source code repository, and then pulling the changes from inside the development environment virtual machine.

It's also appropriate to perform unit testing in the development environment. Developing unit tests will not only give you greater assurance that your code works in production, but is also a great way to spot bugs in the early stages of application development. And, of course, you need to do as much debugging as possible in the local environment! Catching and fixing a bug in development generally takes a tenth of the time you might spend fixing a bug found in production!

Let's now examine the staging environment.

Staging environment

It's quite common for large application development projects to have multiple developers all working on the same code base. In this situation, using version control repositories is critical. The staging environment is where all of the developers upload their code after development environment testing and debugging phases are complete.

The staging environment must be an exact copy of the production environment. You can visualize the staging environment as the last step on an assembly line in a car plant. This is where all of the various pieces coming from one or more development environments are fit into place. The staging environment is a prototype of how production should appear.

It's important to note that often the staging server has direct internet access; however, it's usually located in a secure area that requires a password before you can gain access.

Finally, let's have a look at the production environment.

Production environment

The production environment is often maintained and hosted by the client directly. This environment is also referred to as the live environment. To make an analogy to a Bollywood production, if the development environment is practice, the staging environment is the dress rehearsal, and the production environment is the live show (perhaps minus the singing and dancing!).

The production environment has direct internet access but is protected by a firewall, and is often further protected by an intrusion detection and prevention system (for example, https://snort.org/). In addition, the production environment may be hidden behind a reverse proxy configuration that runs on an internet-facing web server. Otherwise, at least theoretically, the production environment should be an exact clone of the staging environment.

Now that you have an idea about the environments through which the application code moves on its way from development to production, let's have a look at a critical first step in a PHP 8 migration: spotting potential BC code breaks.

Learning how to spot BC breaks before a migration

Ideally, you should go into the PHP 8 migration with an action plan in hand. A critical part of this action plan includes getting an idea of how many potential BC breaks exist in your current code base. In this section, we show you how to develop a BC break sniffer that automates the process of looking through hundreds of code files for potential BC breaks.

First, we'll step back and review what we've learned so far about BC issues that might arise in PHP 8.

Gaining an overview of BC breaks

You already know, having read the previous chapters in this book, that potential code breaks originate from several sources. Let's briefly summarize the general trends that might lead to code failure after a migration. Please note that we do not cover these topics in this chapter as these are the topics that have all been covered in earlier chapters in this book:

  • Resource-to-object migration
  • Minimum versions for supporting OS libraries
  • Iterator to IteratorAggregate migration
  • Removed functions
  • Usage changes
  • Magic method signature enforcement

Many of the changes can be detected by adding a simple callback based upon preg_match() or strpos(). Usage changes are much more difficult to detect as at a glance there's no way for an automated break scanner to detect the result of usage without making extensive use of eval().

Let's now have a look at how a break scan configuration file might appear.

Creating a BC break scan configuration file

A configuration file allows us to develop a set of search patterns independently of the BC break scanner class. Using this approach, the BC break scanner class defines the actual logic used to conduct the search whereas the configuration file provides a list of specific conditions along with a warning and suggested remedial actions.

Quite a few potential code breaks can be detected by simply looking for the presence of the functions that have been removed in PHP 8. For this purpose, a simple strpos() search will suffice. On the other hand, a more complex search might require that we develop a series of callbacks. Let's first have a look at how configuration might be developed based on a simple strpos() search.

Defining a simple strpos() search configuration

In the case of a simple strpos() search, all we need to do is to provide an array of key/value pairs, where the key is the name of the removed function, and the value is its suggested replacement. The search logic in the BC break scanner class can then do this:

$contents = file_get_contents(FILE_TO_SEARCH);

foreach ($config['removed'] as $key => $value)

    if (str_pos($contents, $key) !== FALSE)  echo $value;

We will cover the full BC break scanner class implementation in the next section. For now, we just focus on the configuration file. Here's how the first few strpos() search entries might appear:

// /repo/ch11/bc_break_scanner.config.php

use MigrationBreakScan;

return [

    // not all keys are shown

    BreakScan::KEY_REMOVED => [

        '__autoload' => 'spl_autoload_register(callable)',

        'each' => 'Use "foreach()" or ArrayIterator',

        'fgetss' => 'strip_tags(fgets($fh))',

        'png2wbmp' => 'imagebmp',

        // not all entries are shown

    ],

];

Unfortunately, some PHP 8 backward incompatibilities might prove beyond the abilities of a simple strpos() search. We now turn our attention toward detecting potential breaks caused by the PHP 8 resource-to-object migration.

Detecting BC breaks associated with is_resource()

In Chapter 7, Avoiding Traps When Using PHP 8 Extensions, in the PHP 8 extension resource to object migration section, you learned that there is a general trend in PHP away from resources and toward objects. As you may recall, this trend in and of itself does not pose any threat of a BC break. However, if, in confirming that the connection has been made, your code uses is_resource(), there is a potential for a BC break.

In order to account for this BC break potential, our BC break scan configuration file needs to list any of the functions that formerly produced a resource but now produce an object. We then need to add a method in the BC break scan class (discussed next) that makes use of this list.

This is how the potential configuration key of affected functions might appear:

// /repo/ch11/bc_break_scanner.config.php

return [    // not all keys are shown

    BreakScan::KEY_RESOURCE => [

        'curl_init',

        'xml_parser_create',

        // not all entries are shown

    ],

];

In the break scan class, all we need to do is to first confirm that is_resource() is called, and then check to see if any of the functions listed under the BreakScan::KEY_RESOURCE array are present.

We now turn out attention to magic method signature violations.

Detecting magic method signature violations

PHP 8 strictly enforces magic method signatures. If your classes use loose definitions where you do not perform method signature data typing, and if you do not define a return value data type for magic methods, you are safe from a potential code break. On the other hand, if your magic method signatures do contain data types, and those data types do not match the strictly defined set enforced in PHP 8, you have a potential code break on your hands!

Accordingly, we need to create a set of regular expressions needed to detect magic method signature violations. In addition, our configuration should include the correct signature. In this manner, if a violation is detected, we can present the correct signature in the resulting message, speeding up the update process.

This is how a magic method signature configuration might appear:

// /repo/ch11/bc_break_scanner.config.php

use Php8MigrationBreakScan;

return [    

    BreakScan::KEY_MAGIC => [

    '__call' => [ 'signature' =>

        '__call(string $name, array $arguments): mixed',

        'regex' => '/__calls*((strings)?'

            . '$.+?(arrays)?$.+?)(s*:s*mixed)?/',

        'types' => ['string', 'array', 'mixed']],

    // other configuration keys not shown

    '__wakeup' => ['signature' => '__wakeup(): void',

        'regex' => '/__wakeups*()(s*:s*void)?/',

        'types' => ['void']],

    ]

    // other configuration keys not shown

];

You might notice that we included an extra option, types. This is included in order to automatically generate a regular expression. The code that does this is not shown. If you are interested, have a look at /path/to/repo/ch11/php7_build_magic_signature_regex.php.

Let's have a look at how you might handle complex break detection where a simple strpos() search is not sufficient.

Addressing complex BC break detection

In the case where a simple strpos() search proves insufficient, we can develop another set of key/value pairs where the value is a callback. As an example, take the potential BC break where a class defines a __destruct() method, but also uses die() or exit() in the __construct() method. In PHP 8 it's possible the __destruct() method might not get called under these circumstances.

In such a situation, a simple strpos() search is insufficient. Instead, we must develop logic that does the following:

  • Checks to see if a __destruct() method is defined. If so, no need to continue further as there is no danger of a break in PHP 8.
  • Checks to see if die() or exit() is used in the __construct() method. If so, issue a warning of a potential BC break.

In our BC break scan configuration array, the callback takes the form of an anonymous function. It accepts the file contents as an argument. We then assign the callback to an array configuration key and include the warning message to be delivered if the callback returns TRUE:

// /repo/ch11/bc_break_scanner.config.php

return [

    // not all keys are shown

   BreakScan::KEY_CALLBACK => [

    'ERR_CONST_EXIT' => [

      'callback' => function ($contents) {

        $ptn = '/__construct.*?{.*?(die|exit).*?}/im';

        return (preg_match($ptn, $contents)

                && strpos('__destruct', $contents)); },

      'msg' => 'WARNING: __destruct() might not get '

               . 'called if "die()" or "exit()" used '

               . 'in __construct()'],

    ], // etc.

    // not all entries are shown

];

In our BC break scanner class (discussed next), the logic needed to invoke the callbacks might appear as follows:

$contents = file_get_contents(FILE_TO_SEARCH);

$className = 'SOME_CLASS';

foreach ($config['callbacks'] as $key => $value)

    if ($value['callback']($contents)) echo $value['msg'];

If the requirements to detect additional potential BC breaks are beyond the capabilities of a callback, we would then define a separate method directly inside the BC break scan class.

As you can see, it's possible to develop a configuration array that supports not only simple strpos() searches, but also searches of greater complexity using an array of callbacks.

Now that you have an idea of what would go into a configuration array, it's time to define the main class that performs the break scanning.

Developing a BC break scan class

The BreakScan class is oriented toward a single file. In this class, we define methods that utilize the various break scan configuration just covered. If we need to scan multiple files, the calling program produces a list of files and passes them to BreakScan one at a time.

The BreakScan class can be broken down into two main parts: methods that define infrastructure, and methods that define how to conduct given scans. The latter is primarily dictated by the structure of the configuration file. For each configuration file section, we'll need a BreakScan class method.

Let's have a look at the infrastructural methods first.

Defining BreakScan class infrastructural methods

In this section, we have a look at the initial part of the BreakScan class. We also cover methods that perform infrastructure-related activities:

  1. First, we set up the class infrastructure, placing it in the /repo/src/Php8/Migration directory:

    // /repo/src/Php8/Migration/BreakScan.php

    declare(strict_types=1);

    namespace Php8Migration;

    use InvalidArgumentException;

    use UnexpectedValueException;

    class BreakScan {

  2. Next, we define a set of class constants to render messages indicating the nature of any given post-scan failure:

        const ERR_MAGIC_SIGNATURE = 'WARNING: magic method '

            . 'signature for %s does not appear to match '

            . 'required signature';

        const ERR_NAMESPACE = 'WARNING: namespaces can no '

            . 'longer contain spaces in PHP 8.';

        const ERR_REMOVED = 'WARNING: the following function'

            . 'has been removed: %s.  Use this instead: %s';

        // not all constants are shown

  3. We also define a set of constants that represent configuration array keys. We do this to maintain consistency between key definitions in the configuration file and calling program (discussed later):

        const KEY_REMOVED         = 'removed';

        const KEY_CALLBACK        = 'callbacks';

        const KEY_MAGIC           = 'magic';

        const KEY_RESOURCE        = 'resource';

  4. We then initialize key properties, representing the configuration, the contents of the file to be scanned, and any messages:

        public $config = [];

        public $contents = '';

        public $messages = [];

  5. The __construct() method accepts our break scan configuration file as an argument, and cycles through all of the keys to ensure they exist:

        public function __construct(array $config) {

            $this->config = $config;

            $required = [self::KEY_CALLBACK,

                self::KEY_REMOVED,

                self::KEY_MAGIC,

                self::KEY_RESOURCE];

            foreach ($required as $key) {

                if (!isset($this->config[$key])) {

                    $message = sprintf(

                        self::ERR_MISSING_KEY, $key);

                    throw new Exception($message);

                }

            }

        }

  6. We then define a method that reads in the contents of the file to be scanned. Note that we remove carriage returns (" ") and linefeeds (" ") in order to make scanning via regular expression easier to process:

        public function getFileContents(string $fn) {

            if (!file_exists($fn)) {

                self::$className = '';

                $this->contents  = '';

                throw new  Exception(

                    sprintf(self::ERR_FILE_NOT_FOUND, $fn));

            }

            $this->contents = file_get_contents($fn);

            $this->contents = str_replace([" "," "],

                ['', ' '], $this->contents);

            return $this->contents;

        }

  7. Some of the callbacks need a way to extract just the class name, or just the namespace. For that purpose, we define the static getKeyValue() method:

        public static function getKeyValue(

            string $contents, string $key, string $end) {

            $pos = strpos($contents, $key);

            $end = strpos($contents, $end,

                $pos + strlen($key) + 1);

            return trim(substr($contents,

                $pos + strlen($key),

                $end - $pos - strlen($key)));

        }

    This method looks for the keyword (for example, class). It then finds whatever follows the keyword up to the delimiter (for example, ';'). So, if you want to get the class name, you would execute the following: $name = BreakScan::geyKeyValue($contents,'class',';').

  8. We also need a way to retrieve and reset $this->messages. Here are the two methods to do that:

        public function clearMessages() : void {

            $this->messages = [];

        }

        public function getMessages(bool $clear = FALSE) {

            $messages = $this->messages;

            if ($clear) $this->clearMessages();

            return $messages;

        }

  9. We then define a method that runs all scans (covered in the next section). This method also collects the number of potential BC breaks detected and reports back the total:

        public function runAllScans() : int {

            $found = 0;

            $found += $this->scanRemovedFunctions();

            $found += $this->scanIsResource();

            $found += $this->scanMagicSignatures();

            $found += $this->scanFromCallbacks();

            return $found;

        }

Now that you have an idea of how the basic BreakScan class infrastructure might appear, let's have a look at the individual scan methods.

Examining individual scan methods

The four individual scan methods correspond directly to the top-level keys in the break scan configuration file. Each method is expected to accumulate messages about potential BC breaks in $this->messages. In addition, each method is expected to return an integer representing the total number of potential BC breaks detected.

Let's now examine these methods in order:

  1. The first method we examine is scanRemovedFunctions(). In this method, we search for the function name followed either directly by an open parenthesis, '(', or by a space and open parenthesis, ' ('. If the function is found, we increment $found, and add the appropriate warning and suggested replacement to $this-> messages. If no potential breaks are found, we add a success message and return 0:

    public function scanRemovedFunctions() : int {

        $found = 0;

        $config = $this->config[self::KEY_REMOVED];

        foreach ($config as $func => $replace) {

            $search1 = ' ' . $func . '(';

            $search2 = ' ' . $func . ' (';

            if (

                strpos($this->contents, $search1) !== FALSE

                ||

                strpos($this->contents, $search2) !== FALSE)

            {

                $this->messages[] = sprintf(

                    self::ERR_REMOVED, $func, $replace);

                $found++;

            }

        }

        if ($found === 0)

            $this->messages[] = sprintf(

                self::OK_PASSED, __FUNCTION__);

        return $found;

    }

    The main problem with this approach is that if the function is not preceded by a space, its use would not be detected. However, if we do not include the leading space in the search, we could end up with a false positive. For example, without the leading space, every single instance of foreach() would trigger a warning by the break scanner when looking for each()!

  2. Next we have a look at a method that scans for is_resource() usage. If a reference is located, this method runs through the list of functions that no longer produce a resource. If both is_resource() and one of these methods is located, a potential BC break is flagged:

    public function scanIsResource() : int {

        $found = 0;

        $search = 'is_resource';

        if (strpos($this->contents, $search) === FALSE)

            return 0;

        $config = $this->config[self::KEY_RESOURCE];

        foreach ($config as $func) {

            if ((strpos($this->contents, $func) !== FALSE)){

                $this->messages[] =

                    sprintf(self::ERR_IS_RESOURCE, $func);

                $found++;

            }

        }

        if ($found === 0)

            $this->messages[] =

                sprintf(self::OK_PASSED, __FUNCTION__);

        return $found;

    }

  3. We then have a look at what's required to go through our list of callbacks. As you recall, we need to employ callbacks in situations where a simple strpos() is insufficient. Accordingly, we first collect all the callback subkeys and loop through each one in turn. If there is no bottom-level key callback, we throw an Exception. Otherwise, we run the callback, supplying $this->contents as an argument. If any potential BC breaks are found, we add the appropriate error message, and increment $found:

    public function scanFromCallbacks() {

        $found = 0;

        $list = array_keys($this-config[self::KEY_CALLBACK]);

        foreach ($list as $key) {

            $config = $this->config[self::KEY_CALLBACK][$key]

                ?? NULL;

            if (empty($config['callback'])

                || !is_callable($config['callback'])) {

                $message = sprintf(self::ERR_INVALID_KEY,

                    self::KEY_CALLBACK . ' => '

                    . $key . ' => callback');

                throw new Exception($message);

            }

            if ($config['callback']($this->contents)) {

                $this->messages[] = $config['msg'];

                $found++;

            }

        }

        return $found;

    }

  4. Finally, we turn to by far the most complex method, which scans for invalid magic method signatures. The primary problem is that the method signatures vary widely, thus we need to build separate regular expressions to properly test validity. The regular expressions are stored in the BC break configuration file. If a magic method is detected, we retrieve its correct signature and add that to $this->messages.
  5. First, we check to see if there are any magic methods by looking for anything that matches function __:

    public function scanMagicSignatures() : int {

        $found   = 0;

        $matches = [];

        $result  = preg_match_all(

            '/function __(.+?)/',

            $this->contents, $matches);

  6. If the array of matches is not empty, we loop through the set of matches and assign to $key the magic method name:

       if (!empty($matches[1])) {

            $config = $this->config[self::KEY_MAGIC] ?? NULL;

            foreach ($matches[1] as $name) {

                $key = '__' . $name;

  7. If the configuration key matching this presumed magic method is not set, we assume it's either not a magic method, or is a method not in the configuration file, and thus nothing to worry about. Otherwise, if a key is present, we extract a substring representing the method call that is assigned to $sub:

                if (empty($config[$key])) continue;

                if ($pos = strpos($this->contents, $key)) {

                    $end = strpos($this->contents,

                        '{', $pos);

                $sub = (empty($sub) || !is_string($sub))

                     ? '' : trim($sub);

  8. We then pull the regular expression from the configuration and match it against the substring. The pattern represents a proper signature for that particular magic method. If preg_match() returns FALSE, we know the actual signature is incorrect and flag it as a potential BC break. We retrieve and store the warning message and increment $found:

                $ptn = $config[$key]['regex'] ?? '/.*/';

                if (!preg_match($ptn, $sub)) {

                    $this->messages[] = sprintf(

                      self::ERR_MAGIC_SIGNATURE, $key);

                    $this->messages[] =

                      $config[$key]['signature']

                      ?? 'Check signature'

                    $found++;

        }}}}

        if ($found === 0)

            $this->messages[] = sprintf(

                self::OK_PASSED, __FUNCTION__);

         return $found;

    }

This concludes our examination of the BreakScan class. Now we turn our attention to defining the calling program needed to run the scans programmed into the BreakScan class.

Building a BreakScan class calling program

The main job of the program that calls the BreakScan class is to accept a path argument and to recursively build a list of PHP files located in that path. We then loop through the list, extracting the contents of each file in turn, and run BC break scans. At the end, we present a report that can be either sparse or verbose, depending on the verbosity level selected.

Bear in mind that both the BreakScan class and the calling program we are about to discuss are designed to run under PHP 7. The reason we do not use PHP 8 is because we assume that a developer would wish to run the BC break scanner before they do a PHP 8 update:

  1. We start by configuring the autoloader and getting the path and verbosity levels either from the command line ($argv) or from the URL ($_GET). In addition, we present an option to write the results to a CSV file and accept as a parameter the name of such a file. You might note that we also perform a degree of input sanitization, although theoretically the BC break scanner will only be used on a development server, directly by a developer:

    // /repo/ch11/php7_bc_break_scanner.php

    define('DEMO_PATH', __DIR__);

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

    $loader = new ServerAutoloadLoader();

    use Php8MigrationBreakScan;

    // some code not shown

    $path = $_GET['path'] ?? $argv[1] ?? NULL;

    $show = $_GET['show'] ?? $argv[2] ?? 0;

    $show = (int) $show;

    $csv  = $_GET['csv']  ?? $argv[3] ?? '';

    $csv  = basename($csv);

  2. We next confirm the path. If it's not found, we exit and display usage information ($usage is not shown):

    if (empty($path)) {

        if (!empty($_SERVER['REQUEST_URI']))

            echo '<pre>' . $usage . '</pre>';

        else

            echo $usage;

        exit;

    }

  3. We then grab the BC break configuration file and create a BreakScan instance:

    $config  = include __DIR__

    . '/php8_bc_break_scanner_config.php';

    $scanner = new BreakScan($config);

  4. To build a list of files we use a RecursiveDirectoryIterator, wrapped inside a RecursiveIteratorIterator, starting from the given path. This list is then filtered by FilterIterator, limiting the scan to PHP files only:

    $iter = new RecursiveIteratorIterator(

        new RecursiveDirectoryIterator($path));

    $filter = new class ($iter) extends FilterIterator {

        public function accept() {

            $obj = $this->current();

            return ($obj->getExtension() === 'php');

        }

    };

  5. If the developer chooses the CSV option, an SplFileObject instance is created. At the same time, we write out an array of headers. Further, we define an anonymous function that writes to the CSV file:

    if ($csv) {

        $csv_file = new SplFileObject($csv, 'w');

        $csv_file->fputcsv(

            ['Directory','File','OK','Messages']);

    }

    $write = function ($dir, $fn, $found, $messages)

        use ($csv_file) {

        $ok = ($found === 0) ? 1 : 0;

        $csv_file->fputcsv([$dir, $fn, $ok, $messages]);

        return TRUE;

    };

  6. We launch the scan by looping through the list of files presented by the FilterIterator instance. As we are scanning file by file, on each pass $found is zeroed out. We do maintain $total, however, to give a total count of potential BC breaks at the end. You might also note that we distinguish files from directories. If the directory changes, its name is displayed as a header:

    $dir   = '';

    $total = 0;

    foreach ($filter as $name => $obj) {

        $found = 0;

        $scanner->clearMessages();

        if (dirname($name) !== $dir) {

            $dir = dirname($name);

            echo "Processing Directory: $name ";

        }

  7. We use SplFileObject::isDir() to determine if the item in the file list is a directory. If so, we continue with the next item on the list. We then push the file contents into $scanner and run all scans. Messages are then retrieved in the form of a string:

        if ($obj->isDir()) continue;

        $fn = basename($name);

        $scanner->getFileContents($name);

        $found    = $scanner->runAllScans();

        $messages = implode(" ", $scanner->getMessages());

  8. We use a switch() block to take action based on the display level represented by $show. Level 0 only shows files where potential BC breaks are found. Level 1 shows that plus messages. Level 2 shows all possible output, including success messages:

        switch ($show) {

            case 2 :

                echo "Processing: $fn ";

                echo "$messages ";

                if ($csv)

                    $write($dir, $fn, $found, $messages);

                break;

            case 1 :

                if (!$found) break;

                echo "Processing: $fn ";

                echo BreakScan::WARN_BC_BREAKS . " ";

                printf(BreakScan::TOTAL_BREAKS, $found);

                echo "$messages ";

                if ($csv)

                    $write($dir, $fn, $found, $messages);

                break;

            case 0 :

            default :

                if (!$found) break;

                echo "Processing: $fn ";

                echo BreakScan::WARN_BC_BREAKS . " ";

                if ($csv)

                    $write($dir, $fn, $found, $messages);

        }

  9. Finally, we accumulate the totals and display the final results:

        $total += $found;

    }

    echo " " . str_repeat('-', 40) . " ";

    echo " Total number of possible BC breaks: $total ";

Now that you have an idea how the calling might appear, let's have a look at the results of a test scan.

Scanning application files

For demonstration purposes, in the source code associated with this book, we have included an older version of phpLdapAdmin. You can find the source code at /path/to/repo/sample_data/phpldapadmin-1.2.3. For this demonstration, we opened a shell into the PHP 7 container and ran the following command:

root@php8_tips_php7 [ /repo ]#

php ch11/php7_bc_break_scanner.php

    sample_data/phpldapadmin-1.2.3/ 1 |less

Here is a partial result from running this command:

Processing: functions.php

WARNING: the code in this file might not be

compatible with PHP 8

Total potential BC breaks: 4

WARNING: the following function has been removed: function __autoload.  

Use this instead: spl_autoload_register(callable)

WARNING: the following function has been removed: create_function.  Use this instead: Use either "function () {}" or "fn () => <expression>"

WARNING: the following function has been removed: each.  Use this instead: Use "foreach()" or ArrayIterator

PASSED this scan: scanIsResource

PASSED this scan: scanMagicSignatures

WARNING: using the "@" operator to suppress warnings

no longer works in PHP 8.

As you can see from the output, although functions.php passed the scanMagicSignatures and scanIsResource scans, this code file used three functions that have been removed in PHP 8: __autoload(), create_function(), and each(). You'll also note that this file uses the @ symbol to suppress errors, which is no longer effective in PHP 8.

If you specified the CSV file option, you can open it in any spreadsheet program. Here's how it appears in Libre Office Calc:

Figure 11.1 – CSV file open in Libre Office Calc

Figure 11.1 – CSV file open in Libre Office Calc

You now have an idea of how to create an automated procedure to detect potential BC breaks. Please bear in mind that the code is far from perfect and doesn't cover every single possible code break. For that, you must rely upon your own judgment after having carefully reviewed the material in this book.

It's now time to turn our attention to the actual migration itself.

Performing the migration

Performing the actual migration from your current version to PHP version 8 is much like the process of deploying a new set of features to an existing application. If possible, you might consider running two websites in parallel until such time as you are confident the new version works as expected. Many organizations run the staging environment in parallel with the production environment for this purpose.

In this section, we present a twelve-step guide to perform a successful migration. Although we are focused on migrating to PHP 8, these twelve steps can apply to any PHP update you may wish to perform. Understanding and following these steps carefully is critical to the success of your production website. Included in the twelve steps are plenty of places where you can revert to an earlier version if you encounter problems.

Before we get into details, here is a general overview of a twelve-step migration process going from an older version of PHP to PHP 8:

  1. Carefully review the appropriate migration guide located in the PHP documentation appendices. In our case, we choose Migrating from PHP 7.4x to PHP 8.0x. (https://www.php.net/manual/en/appendices.php).
  2. Make sure your current code works on the current version of PHP.
  3. Back up the database (if any), all source code, and any associated files and assets (for example, CSS, JavaScript, or graphics images).
  4. Create a new branch for the soon-to-be-updated application code in your version control software.
  5. Scan for BC breaks (possibly using the BreakScan class discussed in the previous section).
  6. Update any incompatible code.
  7. Repeat steps 5 and 6 as needed.
  8. Upload your source code to the repository.
  9. Test the source code in a virtual environment updated to PHP 8 that closely simulates the production server.
  10. If the virtualized simulation is not successful, return to step 5.
  11. Update the staging server (or equivalent virtual environment) to PHP 8, making sure you can switch back to the old version.
  12. Run every test you can imagine. If not successful, switch back to the master branch and return to step 5. If successful, clone the staging environment to production.

Let's now look at each step in turn.

Step 1 – Review the migration guide

With every major release of PHP, the PHP core team posts a migration guide. The guide we are mainly concerned with in this book is Migrating from PHP 7.4.x to PHP 8.0.x, located at https://www.php.net/manual/en/migration80.php. This migration guide is broken down into four sections:

  • New Features
  • Backward Incompatible Changes
  • Deprecated Features
  • Other Changes

If you are migrating to PHP 8.0 from a version other than PHP 7.4, you should also review all of the past migration guides from your current PHP version, up to PHP 8. We'll now have a look at other recommended steps in the migration process.

Step 2 – Make sure the current code works

Before you start to make changes to the current code base to ensure it works in PHP 8, it's absolutely critical for you to make sure it's working. If the code isn't working now, it surely will not work once you migrate to PHP 8! Run any unit tests along with any black-box tests to ensure the code is functioning correctly in the current version of PHP.

If you make any changes to the current code before migration, be sure these changes are reflected in the main branch (often called the master branch) of your version control software.

Step 3 – Back up everything

The next step is to back up everything. This includes the database, source code, JavaScript, CSS, images, and so forth. Also, please do not forget to back up important configuration files such as the php.ini file, the webserver configuration, and any other configuration file associated with PHP and web communications.

Step 4 – Create a version control branch

In this step, you should create a new branch in your version control system and check out that branch. In the main branch, you should only have code that currently works.

This is how such a command might work using Git:

$ git branch php8_migration

$ git checkout php8_migration

Switched to branch 'php8_migration'

The first command shown creates a branch called php8_migration. The second command causes git to switch to the new branch. In the process, all of your existing code gets ported to the new branch. The main branch is now safe and preserved from any changes made while in the new branch.

For more information on version control using Git, have a look here: https://git-scm.com/.

Step 5 – Scan for BC breaks

Now it's time to put the BreakScan class to good use. Run the calling program and supply as arguments the starting directory path for your project as well as a verbosity level (0, 1, or 2). You can also specify a CSV file as a third option, as shown earlier in Figure 11.1.

Step 6 – Fix incompatibilities

In this step, knowing where the breaks reside, you can proceed to fix the incompatibilities. You should be able to do so in such a way that the code continues to run in the current version of PHP but can also run in PHP 8. As we've pointed out consistently throughout this book, BC breaks, for the most part, stem from bad coding practices. By fixing the incompatibilities, you improve your code at the same time.

Step 7 – Repeat steps 5 and 6 as needed

There's a famous line, repeated in many Hollywood movies, where the doctor says to the anxious patient, take two aspirin and call me in the morning. The same advice applies to the process of addressing BC breaks. You must be patient, and continue to fix and scan, fix and scan. Keep on doing this until the scan reveals no more potential BC breaks.

Step 8 – Commit changes to the repository

Once you are relatively confident there are no further BC breaks, it's time to commit changes to the new PHP 8 migration branch you created in your version control software. Go ahead and push the changes at this point. You are then in a position to retrieve the updated code from this branch once you've sorted out the PHP update on the production server.

Remember this important point: your current working code is safely stored in the main branch. You are only saving to the PHP 8 migration branch at this stage, so you can always switch back.

Step 9 – Test in a simulated virtual environment

Think of this step as a dress rehearsal for the real thing. In this step, you create a virtual environment (for example, using a Docker container) that most closely simulates the production server. In this virtual environment, you then install PHP 8. Once the virtual environment has been created, you can open a command shell into it, and download your source code from the PHP 8 migration branch.

You can then run unit tests, and any other tests you deem necessary in order to test the updated code. Hopefully, this is where you'll trap any additional errors.

Step 10 – Return to step 5 if the test is unsuccessful

If the unit tests, black-box tests, or other testing performed in the virtual environment show that your application code fails, you must return to step 5. To proceed to the live production site in the face of certain failure would be extremely ill-advised!

Step 11 – Install PHP 8 on the staging environment

The next step is to install PHP 8 in the staging environment. As you might recall from our discussion in the first part of this chapter, the traditional flow is from the development environment, to staging, and then on to production. Once all testing has been completed on the staging environment, you can then clone staging to production.

PHP installation is well documented on the main php.net website, so there is no need for further detail here. Instead, in this section we give you a light overview of PHP installation, with a focus on the ability to switch between PHP 8 and your current PHP version.

Tip

For information on installing PHP in various environments, consult this documentation page: https://www.php.net/manual/en/install.php.

For the purpose of illustration, we choose to discuss PHP 8 installation on two of the main branches of Linux: Debian/Ubuntu and Red Hat/CentOS/Fedora. Let's start with Debian/Ubuntu Linux.

Installing PHP 8 on Debian/Ubuntu Linux

The best way to install PHP 8 is via the available set of pre-compiled binaries. Newer PHP versions tend to be made available much later than their release date, and PHP 8 is no exception. In this case, it's recommended that you resort to using a (Personal Package Archive(PPA). The PPA hosted at https://launchpad.net/~ondrej is the most extensive and widely used.

If you want to simulate the following steps on your own computer, run an Ubuntu Docker image with PHP 7.4 pre-installed using this command:

docker run -it

  unlikelysource/ubuntu_focal_with_php_7_4:latest /bin/bash

In order to install PHP 8 on Debian or Ubuntu Linux, open a command shell onto the production server (or demo container), and, as the root user, proceed as follows. Alternatively, if root user access isn't available, preface each command shown with sudo.

From the command shell, to install PHP 8, proceed as follows:

  1. Update and upgrade the current set of packages using the apt utility. Any package manager can be used; however, we show the use of apt to maintain consistency between the installation steps covered here:

    apt update

    apt upgrade

  2. Add the Ondrej PPA repository to your apt sources:

    add-apt-repository ppa:ondrej/php

  3. Install PHP 8. This installs only the PHP 8 core and basic extensions:

    apt install php8.0

  4. Use the following command to scan the repository for additional extensions and use apt to install them as needed:

    apt search php8.0-*

  5. Do a PHP version check to ensure you're now running PHP 8:

    php --version

Here is the version check output:

root@ec873e16ee93:/# php --version

PHP 8.0.7 (cli) (built: Jun  4 2021 21:26:10) ( NTS )

Copyright (c) The PHP Group

Zend Engine v4.0.7, Copyright (c) Zend Technologies

with Zend OPcache v8.0.7, Copyright (c), by Zend Technologies

Now that you have a basic idea of how a PHP 8 installation might proceed, let's have a look at how to switch between the current version and PHP 8. For the purposes of illustration, we assume that PHP 7.4 is the current PHP version prior to the PHP 8 installation.

Switching between PHP versions in Debian and Ubuntu Linux

If you check to see where PHP is located, you will note that PHP 7.4, the earlier version, still exists following the PHP 8 installation. You can use whereis php for this purpose. The output on our simulation Docker Ubuntu container appears as follows:

root@ec873e16ee93:/# whereis php

php: /usr/bin/php /usr/bin/php8.0 /usr/bin/php7.4 /usr/lib/php /etc/php /usr/share/php7.4-opcache /usr/share/php8.0-opcache /usr/share/php8.0-readline /usr/share/php7.4-readline /usr/share/php7.4-json /usr/share/php8.0-common /usr/share/php7.4-common

As you can see, we now have both the 7.4 and 8.0 versions of PHP installed. To switch between the two, use this command:

update-alternatives --config php

You are then presented with an option screen allowing you to choose which PHP version should be active. Here is how the output screen appears on the Ubuntu Docker image:

root@ec873e16ee93:/# update-alternatives --config php

There are 2 choices for the alternative php

(providing /usr/bin/php).

  Selection    Path             Priority   Status

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

* 0            /usr/bin/php8.0   80        auto mode

  1            /usr/bin/php7.4   74        manual mode

  2            /usr/bin/php8.0   80        manual mode

Press <enter> to keep the current choice[*], or type selection number:

After switching, you can execute php --version again to confirm that the other version of PHP is active.

Let's now turn our attention to PHP 8 installation on Red Hat Linux and its derivatives.

Installing PHP 8 on Red Hat, CentOS, or Fedora Linux

PHP installation on Red Hat, CentOS, or Fedora Linux follows a sequence of commands that are similar to the Debian/Ubuntu installation procedure. The main difference is that you would most likely use a combination of dnf and yum to install the pre-compiled PHP binaries.

If you care to follow along with the installation we outline in this section, you can use a Fedora Docker container with PHP 7.4 already installed. Here is the command to run the simulation:

docker run -it unlikelysource/fedora_34_with_php_7_4 /bin/bash

Much like the PPA environment described in the previous section, in the Red Hat world, the Remi's RPM Repository project (http://rpms.remirepo.net/) provides pre-compiled binaries in Red Hat Package Management (RPM) format.

To install PHP 8 on Red Hat, CentOS, or Fedora, open a command shell onto the production server (or demo environment) and, as the root user, and proceed as follows:

  1. First of all, it's a good idea to confirm the OS version and release you're using. For that purpose, the uname command is used, along with a simple cat command to view the release (stored as a text file in the /etc directory):

    [root@9d4e8c93d7b6 /]# uname -a

    Linux 9d4e8c93d7b6 5.8.0-55-generic #62~20.04.1-Ubuntu

    SMP Wed Jun 2 08:55:04 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

    [root@9d4e8c93d7b6 /]# cat /etc/fedora-release

    Fedora release 34 (Thirty Four)

  2. Before getting started, be sure to update dnf and install the configuration manager:

    dnf upgrade  

    dnf install 'dnf-command(config-manager)'

  3. You would then add Remi's repository to your package sources, using the version number you prefer in place of NN:

    dnf install

      https://rpms.remirepo.net/fedora/remi-release-NN.rpm

  4. At this point, you can confirm the versions of PHP installed using dnf module list. We also use grep to limit the list of modules shown to PHP only. The [e] designation indicates enabled:

    [root@56b9fbf499d6 /]# dnf module list |grep php

    php                    remi-7.4 [e]     common [d] [i],

    devel, minimal    PHP scripting language                        php                    remi-8.0         common [d], devel, minimal        PHP scripting language

  5. We then check the current version of PHP:

    [root@d044cbe477c8 /]# php --version

    PHP 7.4.20 (cli) (built: Jun  1 2021 15:41:56) (NTS)

    Copyright (c) The PHP Group

    Zend Engine v3.4.0, Copyright (c) Zend Technologies

  6. Next, we reset the PHP module, and install PHP 8:

    dnf -y module reset php

    dnf -y module install php:remi-8.0

  7. Another quick PHP version check shows us that we are now using PHP 8 instead of PHP 7:

    [root@56b9fbf499d6 /]# php -v

    PHP 8.0.7 (cli) (built: Jun  1 2021 18:43:05)

    ( NTS gcc x86_64 ) Copyright (c) The PHP Group

    Zend Engine v4.0.7, Copyright (c) Zend Technologies

  8. To switch back to the earlier version of PHP, proceed as follows, where X.Y is the version you plan to use:

    dnf -y module reset php

    dnf -y module install php:remi-X.Y

This completes the PHP installation instructions for Red Hat, CentOS, or Fedora. For this demonstration, we only showed you the PHP command-line installation. If you plan to use PHP with a web server, you also need to install either the appropriate PHP web server package, and/or install the PHP-FPM (FastCGI Processing Module) package.

Let's now have a look at the last step.

Step 12 – Test and clone the staging environment to production

In the last step, you download the source code from your PHP 8 migration branch onto the staging environment and run every imaginable test to make sure everything's working. Once you are assured of success, you then clone the staging environment onto the production environment.

If you are using virtualization, the clone procedure might simply involve creating an identical Docker container or virtual disk file. Otherwise, if actual hardware is involved, you will probably end up cloning the hard drive, or whatever method is appropriate for your setup.

This concludes our discussion of how to perform the migration. Let's now have a look at testing and troubleshooting.

Testing and troubleshooting the migration

In an ideal world, the migration troubleshooting will take place on the staging server, or simulated virtual environment, well before the actual move to production. However, as the seasoned developer well knows, we need to hope for the best, but prepare for the worst! In this section, we cover additional aspects of testing and troubleshooting that can be easily overlooked.

For the purposes of this section, you can exit the temporary shell if you were following the Debian/Ubuntu or the Red Hat/CentOS/Fedora installation process. Return to the Docker container used for this course and open a command shell into the PHP 8 container. Please refer to the Technical requirements section of Chapter 1, Introducing New PHP 8 OOP Features, for more information on how to do this if you are unsure.

Testing and troubleshooting tools

There are too many fine testing and troubleshooting tools available to document here, so we focus on a few open source tools to help with testing and troubleshooting.

Working with Xdebug

Xdebug is a tool that provides diagnostics, profiling, tracing, and step-debugging, among other features. It's a PHP extension, and is thus able to give you detailed information in case you run into problems that you cannot easily solve. The main website is https://xdebug.org/.

To enable the Xdebug extension, you can install it just as you would any other PHP extension: using the pecl command, or by downloading and compiling the source code from https://pecl.php.net/package/xdebug.

Also, the following /etc/php.ini settings should be set, at a minimum:

zend_extension=xdebug.so

xdebug.log=/repo/xdebug.log

xdebug.log_level=7

xdebug.mode=develop,profile

Figure 11.2 shows the output using the xdebug_info() command called from /repo/ch11/php8_xdebug.php:

Figure 11.2 – xdebug_info() output

Figure 11.2 – xdebug_info() output

Let's now have a look at another tool that checks your application from an outside perspective.

Using Apache JMeter

An extremely useful open source tool for testing web applications is Apache JMeter (https://jmeter.apache.org/). It allows you to develop a series of test plans that simulate requests from a browser. You can simulate hundreds of user requests, each with their own cookies and session. Although mainly designed for HTTP or HTTPS, it's also capable of a dozen other protocols as well. In addition to an excellent graphical UI, it also has a command-line mode that makes it possible to incorporate JMeter in an automated deployment process.

Installation is quite simple, involving a single download from https://jmeter.apache.org/download_jmeter.cgi. You must have the Java Virtual Machine (JVM) installed before JMeter will run. Test plan execution is beyond the scope of this book, but the documentation is quite extensive. Also, please bear in mind that JMeter is designed to be run from a client, not on the server. Accordingly, if you wish to test the website in the Docker container for this book, you'll need to install Apache JMeter on your local computer, and then build a test plan that points to the Docker container. Normally the IP address for the PHP 8 container is 172.16.0.88.

Figure 11.3 shows the opening screen for Apache JMeter running on a local computer:

Figure 11.3 – Apache JMeter

Figure 11.3 – Apache JMeter

From this screen you can develop one or more test plans, indicating the URL(s) to access, simulate GET and POST requests, set the number of users, and so forth.

Tip

If you encounter this error while trying to run jmeter: Can't load library: /usr/lib/jvm/java-11-openjdk-amd64/lib/ libawt_xawt.so, try installing OpenJDK 8. You can then use the techniques mentioned in the earlier section to switch between versions of Java.

Let's now have a look at potential issues with Composer following a PHP 8 upgrade.

Handling issues with Composer

One common issue developers face after the migration to PHP 8 has concluded is with third-party software. In this section, we discuss potential issues surrounding the use of the popular Composer package manager for PHP.

The first issue you might encounter has to do with versions of Composer itself. In the year 2020, Composer version 2 was released. Not all of the 300,000+ packages residing on the main packaging website (https://packagist.org/) have been updated to version 2, however. Accordingly, in order to install a given package, you might find yourself having to switch between Composer 2 and Composer 1. The latest releases of each version are available here:

Another, more serious, issue has to do with platform requirements of the various Composer packages you might be using. Each package has its own composer.json file, with its own requirements. In many cases, the package provider might add a PHP version requirement.

The problem is that while most Composer packages now work on PHP 7, the requirements were specified in such a manner as to exclude PHP 8. After a PHP 8 update, when you use Composer to update your third-party packages, an error occurs and the update fails. Ironically, most PHP 7 packages will also work on PHP 8!

As an example, we install a Composer project called laminas-api-tools. At the time of writing, although the package itself is ready for PHP 8, a number of its dependent packages are not. When running the command to install the API tools, the following error is encountered:

root@php8_tips_php8 [ /srv ]#

composer create-project laminas-api-tools/api-tools-skeleton

Creating a "laminas-api-tools/api-tools-skeleton" project at "./api-tools-skeleton"

Installing laminas-api-tools/api-tools-skeleton (1.3.1p1)

  - Downloading laminas-api-tools/api-tools-skeleton (1.3.1p1)

  - Installing laminas-api-tools/api-tools-skeleton (1.3.1p1):

Extracting archiveCreated project in /srv/api-tools-skeleton

Loading composer repositories with package information

Updating dependencies

Your requirements could not be resolved to an installable set of packages.

  Problem 1

    - Root composer.json requires laminas/laminas-developer-tools dev-master, found laminas/laminas-developer-tools[dev-release-1.3, 0.0.1, 0.0.2, 1.0.0alpha1, ..., 1.3.x-dev, 2.0.0, ..., 2.2.x-dev] but it does not match the constraint.

  Problem 2

    - zendframework/zendframework 2.5.3 requires php ^5.5     || ^7.0 -> your php version (8.1.0-dev) does not satisfy     that requirement.

The core problem, highlighted in the last portion of the output just shown, is that one of the dependent packages requires PHP ^7.0. In the composer.json file, this indicates a range of versions from PHP 7.0 through to and including PHP 8.0. In this particular example, the Docker container used runs PHP 8.1, so we have a problem.

Fortunately, in such cases, we are confident that if this package runs in PHP 8.0, it should also run in PHP 8.1. Accordingly, all we need to do is to add the --ignore-platform-reqs flag. When we retry the installation, as you can see from the following output, it is successful:

root@php8_tips_php8 [ /srv ]#

composer create-project --ignore-platform-reqs

    laminas-api-tools/api-tools-skeleton

Creating a "laminas-api-tools/api-tools-skeleton" project at "./api-tools-skeleton"

Installing laminas-api-tools/api-tools-skeleton (1.6.0)

  - Downloading laminas-api-tools/api-tools-skeleton (1.6.0)

  - Installing laminas-api-tools/api-tools-skeleton (1.6.0):

Extracting archive

Created project in /srv/api-tools-skeleton

Installing dependencies from lock file (including require-dev)

Verifying lock file contents can be installed on current

platform.

Package operations: 109 installs, 0 updates, 0 removals

- Downloading laminas/laminas-zendframework-bridge (1.3.0)

- Downloading laminas-api-tools/api-tools-asset-manager

(1.4.0)

- Downloading squizlabs/php_codesniffer (3.6.0)

- Downloading dealerdirect/phpcodesniffer-composer-installer

(v0.7.1)

- Downloading laminas/laminas-component-installer (2.5.0)

... not all output is shown

In the output just shown, no platform requirement errors appear and we are able to continue working with the application.

Let's now turn our attention to unit testing.

Working with unit tests

Unit testing using PHPUnit is a critical factor in the process of ensuring that an application will run after a new feature has been added, or after a PHP update. Most developers create a set of unit tests to at least perform the bare minimum required to prove that an application performs as expected. Tests are methods in a class that extends PHPUnitFrameworkTestCase. The core of the test is what is referred to as an assertion.

Tip

Instructions on how to create and run tests are beyond the scope of this book. However, you can go through the excellent documentation with plenty of examples at the main PHPUnit website: https://phpunit.de/.

The problem you might encounter after a PHP migration is that PHPUnit (https://phpunit.de/) itself might fail! The reason for this is because PHPUnit has a new release each year that corresponds to the version of PHP that is current for that year. The older versions of PHPUnit are based upon what versions of PHP are officially supported. Accordingly, it's entirely possible that the version of PHPUnit currently installed for your application is an older version that doesn't support PHP 8. The simplest solution is to use Composer to perform an update.

To illustrate the possible problem, let's assume that the testing directory for an application currently includes PHP unit 5. If we run a test in the Docker container that runs PHP 7.1, everything works as expected. Here is the output:

root@php8_tips_php7 [ /repo/test/phpunit5 ]# php --version

PHP 7.1.33 (cli) (built: May 16 2020 12:47:37) (NTS)

Copyright (c) 1997-2018 The PHP Group

Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies

    with Xdebug v2.9.1, Copyright (c) 2002-2020, by Derick

Rethans

root@php8_tips_php7 [ /repo/test/phpunit5 ]#

vendor/bin/phpunit

PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

........                                                          8 / 8 (100%)

Time: 27 ms, Memory: 4.00MB

OK (8 tests, 8 assertions)

However, if we run the same version but in the Docker container that's running PHP 8, the results are quite different:

root@php8_tips_php8 [ /repo/test/phpunit5 ]# php --version

PHP 8.1.0-dev (cli) (built: Dec 24 2020 00:13:50) (NTS)

Copyright (c) The PHP Group

Zend Engine v4.1.0-dev, Copyright (c) Zend Technologies

    with Zend OPcache v8.1.0-dev, Copyright (c),

    by Zend Technologies

root@php8_tips_php8 [ /repo/test/phpunit5 ]#

vendor/bin/phpunit

PHP Warning:  Private methods cannot be final as they are never overridden by other classes in /repo/test/phpunit5/vendor/ phpunit/phpunit/src/Util/Configuration.php on line 162

PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

........                                                            8 / 8 (100%)

Time: 33 ms, Memory: 2.00MB

OK (8 tests, 8 assertions)

As you can see from the output, PHPUnit itself reports an error. The simple solution, of course, is that after a PHP 8 upgrade, you also need to re-run Composer and update all third-party packages you use along with your application.

This concludes our discussion of testing and troubleshooting. You now have an idea of what additional tools can be brought to bear to assist you in testing and troubleshooting. Please note, however, that this is by no means a comprehensive list of all testing and troubleshooting tools. There are many many more, some free and open source, others that offer a free trial period, and still more that are only available by purchase.

Summary

In this chapter, you learned how the term environment is used rather than server because many websites these days use virtualized services. You then learned about three distinct environments used during the deployment phase: development, staging, and production.

An automated tool that is able to scan your application code for potential code breaks was introduced next. As you learned in that section, a break-scanning application might consist of a configuration file that addresses removed functionality, changes to method signatures, functions that no longer produce resources, and a set of callbacks for complex usage detection, a scanning class, and a calling program that gathers filenames.

Next, you were shown a typical twelve-step PHP 8 migration procedure that ensures a greater chance of success when you are finally ready to upgrade the production environment. Each step is designed to spot potential code breaks, with fallback procedures in case something goes wrong. You also learned how to install PHP 8 on two common platforms as well as how to easily revert to the older version. Finally, you learned about a number of free open source tools that can assist in testing and troubleshooting.

All in all, after carefully reading this chapter and studying the examples, you are now in a position to not only use existing testing and troubleshooting tools, but now have an idea of how to develop your own scanning tool that greatly reduces the risk of a potential code break after a PHP 8 migration. You also now have an excellent idea what is involved in a migration to PHP 8, and can carry out smoother transitions without fear of failure. Your new ability to anticipate and fix migration problems will ease any anxiety you might otherwise have experienced. You can also look forward to having happy and satisfied customers.

The next chapter introduces you to new and exciting trends in PHP programming that can improve performance even further.

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

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