Table of Contents
Here's one of the most common issues when upgrading from PHP5.x to PHP7:
Warning: preg_replace(): The /e modifier is no longer supported, use preg_replace_callback instead
Despite being a well-documented issue in PHP manual (deprecated since v5.5 and then unsupported since v7.0.0), the above warning is easily one of the most annoying backward-incompatible changes a developer could face when performing the upgrade: adopting the suggested fix - reimplement the code using the newer and more robust preg_replace_callback function - is not always easy, because the preg_replace usage together with the /e modifier was quite common among PHP-based scripts, apps and interfaces until few years ago.
In the following post we're sharing three methods we can use to work around the problem: feel free to pick the one that is most suited for your specific scenario.
Fix #1: Stick with the Plan
The first thing we should do is to check if we can effectively use the updated preg_replace_callback function, changing our code accordingly. Needless to say, this is the most proper way to address the issue, simply because it strictly follows the improved PHP 7 approach to regex-based string replacements: ditch the underlying eval (that was the meaning of the /e switch) and use a proper delegate method instead.
Here's a "old" preg_replace implementation sample, which can be used to turn all lowercase letters into uppercase in a given string:
1 |
$str = preg_replace("/([a-z]*)/e", "strtoupper('\\1')", $str); |
And here's the corresponding, PHP7-valid preg_replace_callback version (which also works in PHP5):
1 2 3 4 5 6 7 8 9 |
$str = preg_replace_callback( "/([a-z]*)/", function($matches){ foreach($matches as $match){ return strtoupper($match); } }, $str ); |
As we can see, there are basically three things to do:
- Use preg_replace_callback instead of preg_replace.
- Remove the /e modifier to the end of the lookup regex string, keeping all the other ones: /e should be replaced with /, /uise with /uis, #ise with #is, and so on (syntax might vary depending of the PHP script).
- Change the second parameter from a mere string (that will be evaluated into a function) to a more robust callback function returning the replacement string itself: the only real difference between the old string and the one returned by the callback function is the way to insert the matched values: instead of using ugly placeholders such as \\1, \\2, \\3 (and so on), which we also have to double-escape, we can use the handy $matches input parameter accepted by our function as an array. The above code shows how we can do that.
Fix #2: Trick the System
There are many scenarios where re-implementing the replacement string into a callback function simply cannot be done: an easy example is the popular PHP-based bulletin board system known as phpBB up to v3.1.x, which uses a template-based approach to handle the BBCODE-to-HTML conversion within their /includes/bbcode.php script. Although they effectively changed the implementation in v3.2.x, there are still tons of forums around the web which proudly uses an outdated version which would be affected by this as soon as PHP gets upgraded to 7.
Here's the relevant parts of the PHP code (/includes/bbcode.php file, lines 113 and 494 for phpBB 3.1.x but might vary upon different releases):
1 |
$message = preg_replace($preg['search'], $preg['replace'], $message); |
1 2 3 |
$tpl = preg_replace('/{L_([A-Z0-9_]+)}/e', "(!empty(\$user->lang['\$1'])) ? \$user->lang['\$1'] : ucwords(strtolower(str_replace('_', ' ', '\$1')))", $tpl); |
The second line could be easily upgraded with the method #1 by adding a use($user) statement right after the callback function, in the following way:
1 2 3 4 5 |
$tpl = preg_replace_callback('/{L_([A-Z0-9_]+)}/e', function($m) use ($user) { /* replace & return with $user->lang available */ }, $tpl); |
However, the first one is not as easy, since the parameter containing the replacement regex(s) is an array filled with template-based rules that might come from various places: filesystem, forum database... It would be quite hard to change them all into functions or to find a way to programmatically transform them.
If you're facing these kind of scenarios and you desperately need a way out, the "best" thing you can do is to replace the unsupported /e modifier with an actual eval() call in the following way:
1 2 3 4 5 6 7 8 9 10 11 |
$message = preg_replace_callback($preg['search'], function($m) use($preg) { $rep = $preg['replace'][1]; for ($i = 1; $i<count($m); $i++) { $rep = str_replace('\\'.$i, '$m['.$i.']', $rep); $rep = str_replace('\$'.$i, '$m['.$i.']', $rep); } eval('$str='.$rep); return $str; }, $message); |
We know, this is almost as bad as stealing... and yet it gets the job done, assuming you can use the eval() function (which is disabled by most providers for obvious security reasons).
Fix #3: Dig it under the carpet
If the method #2 was bad, this is even worse. The only reason we're suggesting this is that we were asked to provide a quick-and-dirty solution for those who cannot perform substantial changes to the script source code and just need to temporarily hide the warning even if they're unable to fix it.
1 2 3 4 |
$current_error_reporting = error_reporting(); // backup current error reporting settings error_reporting($current_error_reporting ^ ( E_WARNING )); // prevent E_WARNING messages from being shown $message = preg_replace($preg['search'], $preg['replace'], $message); // perform the (flawed) preg_replace call error_reporting($current_error_reporting); // restore the former error reporting settings |
This fix can also be very useful for those who are upgrading from PHP <= 5.4 to PHP 5.5 - which still supports the /e modifier but outputs a notice about it being deprecated - without entirely shutting down the E_DEPRECATED and/or E_STRICT level messages. Needless to say, if you plan to use it for PHP 5.5.x, be sure to replace E_WARNING with E_DEPRECATED | E_STRICT instead in line 2.
If you want to exploit this "carpet-based" strategy even further, for example to prevent PHP from logging some errors to the PHP_errors.log file programmatically, we strongly suggest you to read this post. However, it's very important to understand that hiding your script errors is almost always the worst thing you can do: be sure to understand all the implications and potential consequences of what you're doing before proceeding.
Useful references
- preg_replace function (from PHP official docs).
- preg_replace_callback function (from PHP official docs).