Skip to content

Instantly share code, notes, and snippets.

@theodorejb
Last active March 27, 2024 18:22
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save theodorejb/763b83a43522b0fc1755a537663b1863 to your computer and use it in GitHub Desktop.
Save theodorejb/763b83a43522b0fc1755a537663b1863 to your computer and use it in GitHub Desktop.
Migrate deprecated curly brace array access syntax to bracket syntax. Requires PHP 7.4.
<?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
Copy link

mdawaffe commented Aug 26, 2019

This broke for me when processing a file with "trivial" blocks (empty except for whitespace and comments).

For example, processing the following file:

<?php

if ( $a{0} ) {
	// sic
} else {
	echo "Hi";
}
This patch was my attempt to address the issue:
diff --git a/convert_array_access_braces.php b/convert_array_access_braces.php
index 0deab22..86a99c8 100644
--- a/convert_array_access_braces.php
+++ b/convert_array_access_braces.php
@@ -52,2 +52,4 @@ class Converter
 
+    private static $trivialTokens = [ T_COMMENT, T_DOC_COMMENT, T_WHITESPACE ];
+
     public function __construct($opts) {
@@ -252,4 +254,11 @@ class Converter
 
+	$hasNonTrivialTokens = false;
+
         do {
             $index++;
+
+            if ( $index < $endIndex && ( ! is_array( $tokens[ $index ] ) || ! in_array( $tokens[ $index ][0], self::$trivialTokens, true ) ) ) {
+                $hasNonTrivialTokens = true;
+            }
+
             if (!isset($tokens[ $index ])) {
@@ -261,3 +270,3 @@ class Converter
 
-        return true;
+        return $hasNonTrivialTokens;
     }

@theodorejb
Copy link
Author

@mdawaffe Thanks! This is fixed now in the gist code.

@mdawaffe
Copy link

@theodorejb - awesome - thanks!

@red-erik
Copy link

red-erik commented Feb 27, 2020

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.

@theodorejb
Copy link
Author

@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.

@red-erik
Copy link

red-erik commented Feb 28, 2020

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.

@legoktm
Copy link

legoktm commented Mar 23, 2020

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?

@theodorejb
Copy link
Author

@legoktm Thanks for reporting the issue. It should be fixed now.

@mooyah
Copy link

mooyah commented Apr 2, 2020

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);

@TomasVotruba
Copy link

TomasVotruba commented Apr 14, 2020

I tried it on Ubuntu 19.04 with PHP 7.4, but it failed - only reports deprecations, but doesn't change the code 😢 :

image



Then I tried: https://github.com/FriendsOfPHP/PHP-CS-Fixer - normalize_index_brace

Works! 🎉

image

@timwhitlock
Copy link

I had to make a couple of fixes (one as mentioned by @mooyah) and also the deprecation message seems to have more spaces now. Probably a change in PHP7.4 binary since this was written? Anyway, I can't seem to submit a PR for a gist so my change is here

@theodorejb
Copy link
Author

@timwhitlock Thank you! I updated the gist now to include those changes.

@sathishnit
Copy link

sathishnit commented Jun 8, 2021

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.

@knulo
Copy link

knulo commented Sep 30, 2021

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

@theodorejb
Copy link
Author

@knulo Thanks for the bug report - I updated the script to fix this case. Let me know if you run into any other issues.

@hassanbouaakab
Copy link

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

@tarekahf
Copy link

How I can run this script on my PHP Site which is hosted with Bluehost?

@hassanbouaakab
Copy link

hassanbouaakab commented Dec 26, 2022

You can just update your files locally then send them to the server.

@tarekahf
Copy link

tarekahf commented Dec 26, 2022

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.

@hassanbouaakab
Copy link

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

@tarekahf
Copy link

@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

@hassanbouaakab
Copy link

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.

@zwerch
Copy link

zwerch commented Dec 12, 2023

This script saved me a lot of work, thanks for sharing!

One special case needed manual work, though, $ab->cd{123}.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment