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:
To examine and run the code examples provided in this chapter, the minimum recommended hardware is the following:
In addition, you will need to install the following software:
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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:
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.
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.
In this section, we have a look at the initial part of the BreakScan class. We also cover methods that perform infrastructure-related activities:
// /repo/src/Php8/Migration/BreakScan.php
declare(strict_types=1);
namespace Php8Migration;
use InvalidArgumentException;
use UnexpectedValueException;
class BreakScan {
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
const KEY_REMOVED = 'removed';
const KEY_CALLBACK = 'callbacks';
const KEY_MAGIC = 'magic';
const KEY_RESOURCE = 'resource';
public $config = [];
public $contents = '';
public $messages = [];
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);
}
}
}
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;
}
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',';').
public function clearMessages() : void {
$this->messages = [];
}
public function getMessages(bool $clear = FALSE) {
$messages = $this->messages;
if ($clear) $this->clearMessages();
return $messages;
}
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.
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:
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()!
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;
}
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;
}
public function scanMagicSignatures() : int {
$found = 0;
$matches = [];
$result = preg_match_all(
'/function __(.+?)/',
$this->contents, $matches);
if (!empty($matches[1])) {
$config = $this->config[self::KEY_MAGIC] ?? NULL;
foreach ($matches[1] as $name) {
$key = '__' . $name;
if (empty($config[$key])) continue;
if ($pos = strpos($this->contents, $key)) {
$end = strpos($this->contents,
'{', $pos);
$sub = (empty($sub) || !is_string($sub))
? '' : trim($sub);
$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.
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:
// /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);
if (empty($path)) {
if (!empty($_SERVER['REQUEST_URI']))
echo '<pre>' . $usage . '</pre>';
else
echo $usage;
exit;
}
$config = include __DIR__
. '/php8_bc_break_scanner_config.php';
$scanner = new BreakScan($config);
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path));
$filter = new class ($iter) extends FilterIterator {
public function accept() {
$obj = $this->current();
return ($obj->getExtension() === 'php');
}
};
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;
};
$dir = '';
$total = 0;
foreach ($filter as $name => $obj) {
$found = 0;
$scanner->clearMessages();
if (dirname($name) !== $dir) {
$dir = dirname($name);
echo "Processing Directory: $name ";
}
if ($obj->isDir()) continue;
$fn = basename($name);
$scanner->getFileContents($name);
$found = $scanner->runAllScans();
$messages = implode(" ", $scanner->getMessages());
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);
}
$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.
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:
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 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:
Let's now look at each step in turn.
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:
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.
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.
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.
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/.
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.
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.
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.
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.
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.
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!
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.
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:
apt update
apt upgrade
add-apt-repository ppa:ondrej/php
apt install php8.0
apt search php8.0-*
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.
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.
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:
[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)
dnf upgrade
dnf install 'dnf-command(config-manager)'
dnf install
https://rpms.remirepo.net/fedora/remi-release-NN.rpm
[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
[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
dnf -y module reset php
dnf -y module install php:remi-8.0
[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
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.
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.
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.
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.
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:
Let's now have a look at another tool that checks your application from an outside perspective.
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:
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.
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.
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.
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.
44.200.210.43