Creating a PHP 5 to PHP 7 code converter
For the most part, PHP 5.x code can run unchanged on PHP 7. There are a few changes, however, that are classified as backwards incompatible. What this means is that if your PHP 5 code is written in a certain way, or uses functions that have been removed, your code will break, and you'll have a nasty error on your hands.
Getting ready
The PHP 5 to PHP 7 Code Converter does two things:
- Scans your code file and converts PHP 5 functionality that has been removed to its equivalent in PHP 7
- Adds comments with
//
WARNING
where changes in language usage have occurred, but where a re-write is not possibleNote
Please note that after running the converter, your code is not guaranteed to work in PHP 7. You will still have to review the
//
WARNING
tags added. At the least, this recipe will give you a good head start converting your PHP 5 code to work in PHP 7.
The core of this recipe is the new PHP 7 preg_replace_callback_array()
function. What this amazing function allows you to do is to present an array of regular expressions as keys, with the value representing an independent callback. You can then pass the string through a series of transformations. Not only that, the subject of the array of callbacks can itself be an array.
How to do it...
- In a new class
Application\Parse\Convert
, we begin with ascan()
method, which accepts a filename as an argument. It checks to see if the file exists. If so, it calls the PHPfile()
function, which loads the file into an array, with each array element representing one line:public function scan($filename) { if (!file_exists($filename)) { throw new Exception( self::EXCEPTION_FILE_NOT_EXISTS); } $contents = file($filename); echo 'Processing: ' . $filename . PHP_EOL; $result = preg_replace_callback_array( [
- Next, we start passing a series of key/value pairs. The key is a regular expression, which is processed against the string. Any matches are passed to the callback, which is represented as the value part of the key/value pair. We check for opening and closing tags that have been removed from PHP 7:
// replace no-longer-supported opening tags '!^\<\%(\n| )!' => function ($match) { return '<?php' . $match[1]; }, // replace no-longer-supported opening tags '!^\<\%=(\n| )!' => function ($match) { return '<?php echo ' . $match[1]; }, // replace no-longer-supported closing tag '!\%\>!' => function ($match) { return '?>'; },
- Next is a series of warnings when certain operations are detected and there is a potential code-break between how they're handled in PHP 5 versus PHP 7. In all these cases, the code is not re-written. Instead, an inline comment with the word
WARNING
is added:// changes in how $$xxx interpretation is handled '!(.*?)\$\$!' => function ($match) { return '// WARNING: variable interpolation . ' now occurs left-to-right' . PHP_EOL . '// see: http://php.net/manual/en/' . '// migration70.incompatible.php' . $match[0]; }, // changes in how the list() operator is handled '!(.*?)list(\s*?)?\(!' => function ($match) { return '// WARNING: changes have been made ' . 'in list() operator handling.' . 'See: http://php.net/manual/en/' . 'migration70.incompatible.php' . $match[0]; }, // instances of \u{ '!(.*?)\\\u\{!' => function ($match) { return '// WARNING: \\u{xxx} is now considered ' . 'unicode escape syntax' . PHP_EOL . '// see: http://php.net/manual/en/' . 'migration70.new-features.php' . '#migration70.new-features.unicode-' . 'codepoint-escape-syntax' . PHP_EOL . $match[0]; }, // relying upon set_error_handler() '!(.*?)set_error_handler(\s*?)?.*\(!' => function ($match) { return '// WARNING: might not ' . 'catch all errors' . '// see: http://php.net/manual/en/' . '// language.errors.php7.php' . $match[0]; }, // session_set_save_handler(xxx) '!(.*?)session_set_save_handler(\s*?)?\((.*?)\)!' => function ($match) { if (isset($match[3])) { return '// WARNING: a bug introduced in' . 'PHP 5.4 which ' . 'affects the handler assigned by ' . 'session_set_save_handler() and ' . 'where ignore_user_abort() is TRUE . 'has been fixed in PHP 7.' . 'This could potentially break ' . 'your code under ' . 'certain circumstances.' . PHP_EOL . 'See: http://php.net/manual/en/' . 'migration70.incompatible.php' . $match[0]; } else { return $match[0]; } },
- Any attempts to use
<<
or>>
with a negative operator, or beyond 64, is wrapped in atry { xxx } catch() { xxx }
block, looking for anArithmeticError
to be thrown:// wraps bit shift operations in try / catch '!^(.*?)(\d+\s*(\<\<|\>\>)\s*-?\d+)(.*?)$!' => function ($match) { return '// WARNING: negative and ' . 'out-of-range bitwise ' . 'shift operations will now . 'throw an ArithmeticError' . PHP_EOL . 'See: http://php.net/manual/en/' . 'migration70.incompatible.php' . 'try {' . PHP_EOL . "\t" . $match[0] . PHP_EOL . '} catch (\\ArithmeticError $e) {' . "\t" . 'error_log("File:" . $e->getFile() . " Message:" . $e->getMessage());' . '}' . PHP_EOL; },
Note
PHP 7 has changed how errors are handled. In some cases, errors are moved into a similar classification as exceptions, and can be caught! Both the
Error
and theException
class implement theThrowable
interface. If you want to catch either anError
or anException
, catchThrowable
. - Next, the converter rewrites any usage of
call_user_method*()
, which has been removed in PHP 7. These are replaced with the equivalent usingcall_user_func*()
:// replaces "call_user_method()" with // "call_user_func()" '!call_user_method\((.*?),(.*?)(,.*?)\)(\b|;)!' => function ($match) { $params = $match[3] ?? ''; return '// WARNING: call_user_method() has ' . 'been removed from PHP 7' . PHP_EOL . 'call_user_func(['. trim($match[2]) . ',' . trim($match[1]) . ']' . $params . ');'; }, // replaces "call_user_method_array()" // with "call_user_func_array()" '!call_user_method_array\((.*?),(.*?),(.*?)\)(\b|;)!' => function ($match) { return '// WARNING: call_user_method_array()' . 'has been removed from PHP 7' . PHP_EOL . 'call_user_func_array([' . trim($match[2]) . ',' . trim($match[1]) . '], ' . $match[3] . ');'; },
- Finally, any attempt to use
preg_replace()
with the/e
modifier is rewritten using apreg_replace_callback()
:'!^(.*?)preg_replace.*?/e(.*?)$!' => function ($match) { $last = strrchr($match[2], ','); $arg2 = substr($match[2], 2, -1 * (strlen($last))); $arg1 = substr($match[0], strlen($match[1]) + 12, -1 * (strlen($arg2) + strlen($last))); $arg1 = trim($arg1, '('); $arg1 = str_replace('/e', '/', $arg1); $arg3 = '// WARNING: preg_replace() "/e" modifier . 'has been removed from PHP 7' . PHP_EOL . $match[1] . 'preg_replace_callback(' . $arg1 . 'function ($m) { return ' . str_replace('$1','$m', $match[1]) . trim($arg2, '"\'') . '; }, ' . trim($last, ','); return str_replace('$1', '$m', $arg3); }, // end array ], // this is the target of the transformations $contents ); // return the result as a string return implode('', $result); }
How it works...
To use the converter, run the following code from the command line. You'll need to supply the filename of the PHP 5 code to be scanned as an argument.
This block of code, chap_01_php5_to_php7_code_converter.php
, run from the command line, calls the converter:
<?php // get filename to scan from command line $filename = $argv[1] ?? ''; if (!$filename) { echo 'No filename provided' . PHP_EOL; echo 'Usage: ' . PHP_EOL; echo __FILE__ . ' <filename>' . PHP_EOL; exit; } // setup class autoloading require __DIR__ . '/../Application/Autoload/Loader.php'; // add current directory to the path Application\Autoload\Loader::init(__DIR__ . '/..'); // get "deep scan" class $convert = new Application\Parse\Convert(); echo $convert->scan($filename); echo PHP_EOL;
See also
For more information on backwards incompatible changes, please refer to http://php.net/manual/en/migration70.incompatible.php.