-
-
Save theodorejb/763b83a43522b0fc1755a537663b1863 to your computer and use it in GitHub Desktop.
<?php | |
error_reporting(E_ALL); | |
$opts = getopt('f:d:rb:', ['ext:', 'php:', 'diff::']); | |
if ((int)isset($opts['d']) + (int)isset($opts['f']) !== 1) { | |
$self = basename(__FILE__); | |
echo <<<EOF | |
Usage: | |
php $self -f<php_script> [-b] [--php <path_to_php>] [ --diff [<file>]] | |
php $self -d<dir_name> [-b] [-r] [--php <path_to_php>] [--ext <extension>[,<extension>...] [ --diff [<file>]] | |
Where: | |
-f<php_script> Convert single file <php_script>. | |
-d<dir_name> Convert all ".php"(see "--ext") files inside <dir_name> directory. | |
-r Walk through directories recursively. Without this flag only concrete directory will be processed. | |
-b<backup_dir> Backup converted files into <backup_dir>. | |
--ext Comma separated list of file extensions for conversion. If set then ".php" will not be added automatically. | |
--php <path_to_php> Path to PHP interpreter to migrate. | |
--diff[=<file>] Redirect diff-info into <file>. Write to stdout if <file> does not specified. | |
Examples: | |
php $self -f./index.php -b --diff | |
Convert file ./index.php and make backup to ./index.php_backup and show diff | |
php $self -d/srv/http/api -r --ext phpt,php | |
Convert all ".php" and ".phpt" files inside whole /srv/http/api directory tree | |
php $self -d/srv/http/api --php /root/sapi/bin/php --diff=./diff.out | |
Convert all ".php" files inside /srv/http/api directory and write diff to ./diff.out | |
using /root/sapi/bin/php for check for deprecation | |
EOF; | |
exit(0); | |
} | |
$converter = new Converter($opts); | |
if (isset($opts['f'])) { | |
$converter->convertFile($opts['f']); | |
} elseif (isset($opts['d'])) { | |
$converter->convertDirectory($opts['d'], isset($opts['r'])); | |
} | |
class Converter | |
{ | |
private string $php; | |
private string $backupDir; | |
private array $ext; | |
/** | |
* @var bool|string | |
*/ | |
private $diff; | |
private string $dir = ''; | |
private string $diffContent = ''; | |
public function __construct(array $opts) | |
{ | |
$this->php = isset($opts['php']) ? $opts['php'] : PHP_BINARY; | |
$this->backupDir = isset($opts['b']) ? rtrim($opts['b'], "/\\") . DIRECTORY_SEPARATOR : ''; | |
$this->ext = isset($opts['ext']) ? explode(',', $opts['ext']) : ['php']; | |
$this->diff = isset($opts['diff']) ? ($opts['diff'] !== false ? $opts['diff'] : true) : false; | |
if ($this->backupDir && !is_dir($this->backupDir)) { | |
$this->fatalError("Backup directory $this->backupDir not found"); | |
} | |
} | |
public function convertDirectory(string $dir, bool $recursively): void | |
{ | |
if (!is_dir($dir)) { | |
$this->fatalError("Target directory $dir not found"); | |
} | |
$this->dir = rtrim($dir, "/\\") . DIRECTORY_SEPARATOR; | |
$regex = '/^.+\.(' . implode('|', $this->ext) . ')$/'; | |
if ($recursively) { | |
$dirIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); | |
$path = ''; | |
} else { | |
$dirIterator = new DirectoryIterator($dir); | |
$path = $this->dir; | |
} | |
$iterator = new RegexIterator($dirIterator, $regex, RegexIterator::GET_MATCH); | |
foreach ($iterator as $file) { | |
$this->convertFile($path . $file[0]); | |
} | |
} | |
public function convertFile(string $file): void | |
{ | |
if (!is_file($file)) { | |
$this->fatalError("Target file $file not found"); | |
} | |
$deprecated = $this->getDeprecatedLines($file); | |
if (empty($deprecated)) { | |
return; | |
} | |
$tokens = token_get_all(file_get_contents($file)); | |
$actualTokens = $this->actualTokens($tokens); | |
while ($braces = $this->findPair($actualTokens)) { | |
if ( | |
$actualTokens[$braces[0]] !== '${' // Complex string. | |
&& $this->validNext($tokens, $braces[0], $braces[1]) | |
) { | |
$tokens[$braces[0]] = '['; | |
$tokens[$braces[1]] = ']'; | |
} | |
unset($actualTokens[$braces[0]], $actualTokens[$braces[1]]); | |
} | |
$convertedFile = $this->writeConverted($tokens, $file); | |
$this->diff($file, $convertedFile); | |
$this->backup($file, $convertedFile); | |
$notConverted = $this->getDeprecatedLines($convertedFile); | |
if (!empty($notConverted)) { | |
foreach ($notConverted as $line) { | |
echo " - Failed to convert line $line in $file" . PHP_EOL; | |
} | |
echo " ? $file is not fully converted." . PHP_EOL; | |
} else { | |
echo "+ $file successfully converted." . PHP_EOL; | |
} | |
if (!rename($convertedFile, $file)) { | |
$this->fatalError("Failed to replace $file with $convertedFile", $convertedFile); | |
} | |
} | |
private function getDeprecatedLines(string $file): array | |
{ | |
$output = []; | |
$escapedFile = escapeshellarg($file); | |
exec("$this->php -d error_reporting=E_ALL -l $escapedFile 2>&1", $output, $ret); | |
if ($ret !== 0) { | |
foreach ($output as $string) { | |
echo '> ' . $string . PHP_EOL; | |
} | |
$this->fatalError("Processing of file $file is failed"); | |
} | |
$output = array_filter($output, fn($line) => preg_match('/Deprecated: +Array and string/i', $line) !== 0); | |
array_walk($output, function (&$line) { $line = (int)trim(strrchr($line, ' ')); }); | |
return $output; | |
} | |
private function writeConverted(array $tokens, string $original): string | |
{ | |
do { | |
$outputFileName = $original . mt_rand(0, 1000); | |
} while (is_file($outputFileName)); | |
$outputFile = fopen($outputFileName, 'wb'); | |
if (!$outputFile) { | |
$this->fatalError("Cannot create temp file $outputFileName"); | |
} | |
foreach ($tokens as $token) { | |
if (is_array($token)) { | |
fwrite($outputFile, $token[1]); | |
} else { | |
fwrite($outputFile, $token); | |
} | |
} | |
fclose($outputFile); | |
return $outputFileName; | |
} | |
private function backup(string $file, string $convertedFile): void | |
{ | |
if ($this->backupDir) { | |
$backupFileName = $this->backupDir . str_replace($this->dir, '', $file); | |
if (!is_dir(pathinfo($backupFileName, PATHINFO_DIRNAME))) { | |
if (!mkdir(pathinfo($backupFileName, PATHINFO_DIRNAME), 0777, true)) { | |
$this->fatalError("Failed to create directory tree for $backupFileName", $convertedFile); | |
} | |
} | |
if (!rename($file, $backupFileName)) { | |
$this->fatalError("Failed to backup $file into $backupFileName", $convertedFile); | |
} | |
} | |
} | |
private function actualTokens(array $tokens): array | |
{ | |
$result = []; | |
$inString = false; | |
$depth = 0; | |
foreach ($tokens as $key => $token) { | |
if ($token === '"') { | |
$inString = !$inString; | |
} | |
$tokenStr = null; | |
if ($token === '{' || $token === '}') { | |
$tokenStr = $token; | |
} else if (is_array($token) && ($token[1] === '${' || $token[1] === '{' || $token[1] === '->')) { | |
$tokenStr = $token[1]; | |
} | |
if ($tokenStr !== null) { | |
if ($inString) { | |
if ($tokenStr === '}') { | |
$depth--; | |
if ($depth === 0) { | |
continue; // ignore outer closing brace | |
} | |
} else { | |
$depth++; | |
if ($depth === 1) { | |
continue; // ignore outer opening brace | |
} | |
} | |
} | |
$result[$key] = $tokenStr; | |
} | |
} | |
return $result; | |
} | |
/** | |
* @return int[] | null | |
*/ | |
private function findPair(array $tokens): ?array | |
{ | |
if (count($tokens) < 2) { | |
return null; | |
} | |
$last = ''; | |
$lastKey = 0; | |
foreach ($tokens as $key => $token) { | |
if ($token === '{' && $last === '->') { | |
continue; | |
} | |
if (($last === '{' || $last === '${') && $token === '}') { | |
return [$lastKey, $key]; | |
} else { | |
$last = $token; | |
$lastKey = $key; | |
} | |
} | |
return null; | |
} | |
private function validNext(array $tokens, int $index, int $endIndex): bool | |
{ | |
$index++; | |
if ($tokens[$index] === '}') { | |
return false; // empty block | |
} | |
$trivialTokens = [T_COMMENT, T_DOC_COMMENT, T_WHITESPACE]; | |
$hasNonTrivialTokens = false; | |
while ($index < $endIndex) { | |
if ($tokens[$index] === ';') { | |
return false; // array/string offset accesses can't contain semicolon | |
} | |
if (!is_array($tokens[$index]) || !in_array($tokens[$index][0], $trivialTokens, true)) { | |
$hasNonTrivialTokens = true; | |
} | |
$index++; | |
} | |
return $hasNonTrivialTokens; | |
} | |
private function fatalError(string $text, string $file = ''): void | |
{ | |
if ($file) { | |
unlink($file); | |
} | |
if ($this->diffContent !== '') { | |
$this->writeDiff(); | |
} | |
die($text . PHP_EOL); | |
} | |
private function diff(string $file, string $convertedFile): void | |
{ | |
if (!$this->diff) { | |
return; | |
} | |
$original = fopen($file, 'rb'); | |
$converted = fopen($convertedFile, 'rb'); | |
$i = 1; | |
$this->diffContent .= "file: $file" . PHP_EOL; | |
while (($line = fgets($original)) !== false) { | |
$newLine = fgets($converted); | |
if ($line !== $newLine) { | |
$this->diffContent .= $i . 'c' . $i . PHP_EOL; | |
$this->diffContent .= "< $line"; | |
$this->diffContent .= '---' . PHP_EOL; | |
$this->diffContent .= "> $newLine" . PHP_EOL; | |
} | |
$i++; | |
} | |
fclose($converted); | |
fclose($original); | |
} | |
private function writeDiff(): void | |
{ | |
if ($this->diff === true) { | |
echo $this->diffContent; | |
} else if ($this->diff !== false) { | |
file_put_contents($this->diff, $this->diffContent); | |
} | |
} | |
public function __destruct() | |
{ | |
$this->writeDiff(); | |
} | |
} |
@mdawaffe Thanks! This is fixed now in the gist code.
@theodorejb - awesome - thanks!
Hello,
how to use on Windows ? I mean, how to specify a directory path and recursion?
If I target a single file, it works. Eg. php convert_array_access_braces.php -fC:\\Reports\\phpLDAPadmin-1.2.5\\lib\\functions.php
but if I try to use it on all files into lib subir with recursion, nothing happens
php convert_array_access_braces.php -dC:\Reports\phpLDAPadmin-1.2.5 -r NOT WORKING
php convert_array_access_braces.php -dC:\\Reports\\phpLDAPadmin-1.2.5 -r NOT WORKING
Regards,
Red.
@red-erik Running php convert_array_access_braces.php -d C:\Reports\phpLDAPadmin-1.2.5 -r
works fine for me on Windows.
What do you mean "nothing happens"? Maybe there just aren't any deprecated usages in that directory that need to be converted.
Hello,
I had not considered the possibility because I was sure there were. I'll check again file by file but it is strange the only 1 file results modified with current date while all others not.
Red.
Thanks for the script - it seems to fail on paths with spaces in them, I believe the issue is when getDeprecatedLines() shells out to PHP - would it be possible to fix that?
@legoktm Thanks for reporting the issue. It should be fixed now.
I had to update the getDeprecatedLines function to redirect stderr into stdout.
exec("$this->php -d error_reporting=E_ALL -l $escapedFile 2>&1", $output, $ret);
I tried it on Ubuntu 19.04 with PHP 7.4, but it failed - only reports deprecations, but doesn't change the code 😢 :
Then I tried: https://github.com/FriendsOfPHP/PHP-CS-Fixer - normalize_index_brace
Works! 🎉
@timwhitlock Thank you! I updated the gist now to include those changes.
Thank you !, updated 3 files, No error now with PHP 7.4 in Centos 7 with OpenLdap 2.5
[root@localhost sathish]# php convert_array_access_braces.php -d phpldapadmin -r
- phpldapadmin/lib/TemplateRender.php successfully converted.
- phpldapadmin/lib/export_functions.php successfully converted.
- phpldapadmin/lib/functions.php successfully converted.
Hello @theodorejb,
first: thank you for the script, in most cases it works well.
Unfortunately it crashes when entering things like
$this->{$key} = null;
(which should still be allowed?) and converting to
$this->[$key] = null;
which is invalid and causes the error (I think).
I tried some changes to catch and exclude the construct from converting but with no success. Would you have a look for this, please?
Thanks in advance
Knut
@knulo Thanks for the bug report - I updated the script to fix this case. Let me know if you run into any other issues.
Hello for windows 10 this work fine: open cmd change directory to folder php for example:
C:\php>php convert_array_access_braces.php -f String.php -b C:\php\backup\ --diff
How I can run this script on my PHP Site which is hosted with Bluehost?
You can just update your files locally then send them to the server.
You can just update your files locally then send them to the server.
I've never worked with PHP please give me more details about what to do...
I know how to use ftp but I don't know how to run those commands locally. I'm know how to use NodeJS and powershell for example so you can provide details based on that level.
First install php you can find the steps here on How to execute PHP code using command line; https://www.geeksforgeeks.org/how-to-execute-php-code-using-command-line/
Then execute the script like this; open CMD change directory to folder php for example:
C:\php>php convert_array_access_braces.php -f test.php -b C:\php\backup\ --diff
Convert file test.php and make backup to C:\php\backup\test.php_backup and show diff
@hassanbouaakab thank you so much. I can't be happier.
Can you please do another favor?
See the error_log below from the Bluehost public_html/error_log
file.
Apart from the errors Array and string offset access syntax with curly braces is no longer supported
can you shed some light on how to troubleshoot and resolve the other errors? I manually fixed all curly braces references. But with PHP 8.0 and 8.1 I get all the other creepy errors as per the log below. They all seem due to compatibility errors with PHP version 8.x as I am using CherryFramework and Doing Business WordPress Template which seems not compatible with PHP 8.x. I contacted the developer but looks like they are going to charge me for this modification. I am checking if there is a free option. I am a solid developer and I can deal with PHP even though I never worked with it. All I am asking is to give me some pointers.
[14-Dec-2022 00:18:17 UTC] PHP Warning: The magic method MPCERevisionManager::__wakeup() must have public visibility in /path/to/root/public_html/wp-content/plugins/motopress-content-editor/includes/ce/MPCERevisionManager.php on line 299
[24-Dec-2022 06:08:31 UTC] PHP Warning: Use of undefined constant CHERRY_PLUGIN_URL - assumed 'CHERRY_PLUGIN_URL' (this will throw an Error in a future version of PHP) in /path/to/root/public_html/wp-content/themes/theme50603/includes/custom-function.php on line 1114
[24-Dec-2022 07:02:50 UTC] PHP Fatal error: Array and string offset access syntax with curly braces is no longer supported in /path/to/root/public_html/wp-content/themes/CherryFramework/includes/lessc.inc.php on line 657
[24-Dec-2022 07:45:48 UTC] PHP Warning: array_merge(): Expected parameter 2 to be an array, null given in /path/to/root/public_html/wp-content/plugins/ultimate-product-catalogue/includes/Product.class.php on line 483
[24-Dec-2022 07:45:48 UTC] PHP Warning: Invalid argument supplied for foreach() in /path/to/root/public_html/wp-content/plugins/ultimate-product-catalogue/ewd-upcp-templates/single-product-additional-images.php on line 3
[24-Dec-2022 08:11:10 UTC] PHP Fatal error: Array and string offset access syntax with curly braces is no longer supported in /path/to/root/public_html/wp-content/themes/CherryFramework/includes/lessc.inc.php on line 1624
[24-Dec-2022 08:12:23 UTC] PHP Fatal error: Array and string offset access syntax with curly braces is no longer supported in /path/to/root/public_html/wp-content/themes/CherryFramework/includes/lessc.inc.php on line 2281
[24-Dec-2022 08:13:30 UTC] PHP Fatal error: Array and string offset access syntax with curly braces is no longer supported in /path/to/root/public_html/wp-content/themes/CherryFramework/includes/lessc.inc.php on line 2335
[24-Dec-2022 08:14:13 UTC] PHP Fatal error: Array and string offset access syntax with curly braces is no longer supported in /path/to/root/public_html/wp-content/themes/CherryFramework/includes/lessc.inc.php on line 3065
[24-Dec-2022 08:15:23 UTC] PHP Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported. Use either `(a ? b : c) ? d : e` or `a ? b : (c ? d : e)` in /path/to/root/public_html/wp-content/plugins/cherry-plugin/includes/plugin-assets.php on line 102
[24-Dec-2022 08:21:19 UTC] PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function WP_Widget::__construct(), 0 passed in /path/to/root/public_html/wp-includes/class-wp-widget-factory.php on line 61 and at least 2 expected in /path/to/root/public_html/wp-includes/class-wp-widget.php:162
Stack trace:
#0 /path/to/root/public_html/wp-includes/class-wp-widget-factory.php(61): WP_Widget->__construct()
#1 /path/to/root/public_html/wp-includes/widgets.php(115): WP_Widget_Factory->register('My_SocialNetwor...')
#2 /path/to/root/public_html/wp-content/plugins/cherry-plugin/includes/widgets/register-widgets.php(26): register_widget('My_SocialNetwor...')
#3 /path/to/root/public_html/wp-includes/class-wp-hook.php(307): cherry_load_widgets('')
#4 /path/to/root/public_html/wp-includes/class-wp-hook.php(331): WP_Hook->apply_filters(NULL, Array)
#5 /path/to/root/public_html/wp-includes/plugin.php(474): WP_Hook->do_action(Array)
#6 /path/to/root/public_html/wp-includes/widgets.php(1854): do_action('widgets_init')
#7 /path/to/root/public_html/wp-includes/class-wp-hook.php(307): wp_widgets_init('')
#8 /path/to/root/public_html/wp-includes/class-wp-hook.php(331): WP_Hook->apply_filters(NULL, Array)
#9 /path/to/root/public_html/wp-includes/plugin.php(474): WP_Hook->do_action(Array)
#10 /path/to/root/public_html/wp-settings.php(587): do_action('init')
#11 /path/to/root/public_html/wp-config.php(100): require_once('/path/to/root...')
#12 /path/to/root/public_html/wp-load.php(50): require_once('/path/to/root...')
#13 /path/to/root/public_html/wp-blog-header.php(13): require_once('/path/to/root...')
#14 /path/to/root/public_html/index.php(17): require('/path/to/root...')
#15 {main}
thrown in /path/to/root/public_html/wp-includes/class-wp-widget.php on line 162
When you update to php 8 you get a lot of error i had this problem i tried to fix them manually but every time a get new ones you can use rector to upgrade your project: https://getrector.org/blog/2020/11/30/smooth-upgrade-to-php-8-in-diffs
You can downgrade your site in bluehost to php 7.4 temporarily but it's best to use the new wordpress version an migrate your projet.
This script saved me a lot of work, thanks for sharing!
One special case needed manual work, though, $ab->cd{123}
.
This broke for me when processing a file with "trivial" blocks (empty except for whitespace and comments).
For example, processing the following file:
This patch was my attempt to address the issue: