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).
Hi,
Thanks for descrition.
But where (which file) do I have to edit with the new code.
Can’t find the preg_replace function
In the article there’s a typo:
$str = preg_replace(“/([a-z]*)/e”, “strtoupper(‘\1’)”, $str);
Should be
$str = preg_replace(“/([a-z]*)/e”, “strtoupper(‘\1’)”, $input);
Fixed: truth to be told, that string was OK, the typo was in the next one. Anyway, thanks!
function category_get_tree($prefix = ”, $tpl = ‘{name}’, $no_prefix = true, $id = 0, $level = 0){
global $sql, $PHP_SELF;
static $johnny_left_teat;
$level++;
foreach ($sql->select(array(‘table’ => ‘categories’, ‘where’ => array(“parent = $id”), ‘orderby’ => array(‘id’, ‘ASC’))) as $row){
$find = array(‘/{id}/i’, ‘/{name}/i’, ‘/{url}/i’, ‘/{icon}/i’, ‘/{template}/i’, ‘/{prefix}/i’, ‘/[php](.*?)[\/php]/ie’);
$repl = array($row[‘id’], $row[‘name’], $row[‘url’], ($row[‘icon’] ? ” : ”), $row[‘template’], (($row[‘parent’] or !$no_prefix) ? $prefix : ”), ‘\1’);
$johnny_left_teat .= ($no_prefix ? preg_replace(‘/(‘.$prefix.'{1})$/i’, ”, str_repeat($prefix, $level)) : str_repeat($prefix, $level));
$johnny_left_teat .= preg_replace($find, $repl, $tpl);
category_get_tree($prefix, $tpl, $no_prefix, $row[‘id’], $level);
}
return $johnny_left_teat;
}
I receive the following error when run the above code:
Warning: preg_replace(): The /e modifier is no longer supported, use preg_replace_callback instead in \functions.inc.php on line 726
Line 726:
$johnny_left_teat .= preg_replace($find, $repl, $tpl);
Please!!!!
Just follow the post and rewrite your code accordingly: unfortunately we cannot do the job for you. Maybe use StackOverflow to seek this kind of help.
I’m trying to implement the suggestion in the latter part of #2, but I think there’s a problem with it. In particular:
$rep = $preg[‘replace’][1];
If the code you’re replacing had 2 matched arrays of searches and replaces, this code uses just one of the replacement strings for every search match. Unless I’m missing something, I don’t think this can be fixed because preg_replace_callback() doesn’t seem to have a facility to tell the callback what the index of the search array is that produced the match it was called on.
Given that, I think my only option is to unroll this into a loop of preg_replace_callback() calls, one for each item in the search[]/replace[] arrays.
Have I missed anything here?
Hello there,
I think you’re right, that solution would only work if we’re dealing with a single search/replace pattern.
However, If you have multiple patterns, I think you can use
preg_replace_callback_array
and define a separate callback for each pattern: inside the callback you could then call a single, separate external function passing the right pattern (or just return the proper replacement string).ref.: https://www.php.net/manual/en/function.preg-replace-callback-array.php
More specifically, this example should put you on the right track:
https://www.php.net/manual/en/function.preg-replace-callback-array.php#118455
It contains a multiple
preg_replace_callback
implementation approach (for php < 7) and a singlepreg_replace_callback_array
implementation approach (for php >= 7): both of them should be viable enough to work around the “multiple search/replace patterns” issue.I’ve updated the post accordingly adding these suggestions: many thanks for your findings!