Last active
February 20, 2026 22:15
-
-
Save daemondevin/fc9f95d9b633c8e1cebf935376ec754d to your computer and use it in GitHub Desktop.
Command line utility that will recursively scan a directory and does a full namespace-aware/class-name-only replacement (AST-safe replacement using token parsing).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| if (php_sapi_name() !== 'cli') { | |
| exit("Run from CLI only.\n"); | |
| } | |
| function is_true($val, $return_null=false){ | |
| $boolval = ( is_string($val) ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) : (bool) $val ); | |
| return ( $boolval===null && !$return_null ? false : $boolval ); | |
| } | |
| define('PHP_TAB', "\t"); | |
| define('CLI_RESET', "\033[0m"); | |
| define('CLI_BOLD', "\033[1m"); | |
| define('CLI_CLREOL', "\033[K"); | |
| define('CLI_BLACK', "\033[30m"); | |
| define('CLI_RED', "\033[31m"); | |
| define('CLI_GREEN', "\033[32m"); | |
| define('CLI_YELLOW', "\033[33m"); | |
| define('CLI_BLUE', "\033[34m"); | |
| define('CLI_MAGENTA', "\033[35m"); | |
| define('CLI_CYAN', "\033[36m"); | |
| define('CLI_WHITE', "\033[37m"); | |
| define('CLI_BLACKBG', "\033[40m"); | |
| define('CLI_REDBG', "\033[41m"); | |
| define('CLI_GREENBG', "\033[42m"); | |
| define('CLI_YELLOWBG', "\033[43m"); | |
| define('CLI_BLUEBG', "\033[44m"); | |
| define('CLI_MAGENTABG', "\033[45m"); | |
| define('CLI_CYANBG', "\033[46m"); | |
| define('CLI_WHITEBG', "\033[47m"); | |
| define('CLI_ERROR', "\033[41;30m" . CLI_CLREOL); | |
| define('CLI_WARNING', "\033[43;30m" . CLI_CLREOL); | |
| define('CLI_INFO', "\033[44;30m" . CLI_CLREOL); | |
| define('CLI_SUCCESS', "\033[42;30m" . CLI_CLREOL); | |
| function message($text, $status) { | |
| $out = ''; | |
| switch($status) { | |
| case 'SUCCESS': | |
| $out = CLI_SUCCESS.' SUCCESS: '.chr(27).'[0;32m '; //Green background | |
| break; | |
| case 'ERROR': | |
| $out = CLI_ERROR.' ERROR: '. chr(27).'[0;31m '; //Red | |
| break; | |
| case 'WARNING': | |
| $out = CLI_WARNING.' WARNING: '; //Yellow background | |
| break; | |
| case 'INFO': | |
| $out = CLI_INFO.' INFO: '. chr(27).'[0;34m '; //Blue | |
| break; | |
| case 'STDOUT': | |
| $out = CLI_WHITE.' '; | |
| break; | |
| case 'HEADER': | |
| $out = CLI_CYANBG.' '; | |
| break; | |
| case 'HELP': | |
| $out = CLI_GREENBG.' HELP: '. chr(27).'[0;32m '; //Green | |
| break; | |
| default: | |
| throw new Exception('Invalid status: ' . $status); | |
| } | |
| if ($status === 'STDOUT') { | |
| return "{$out}{$text}".CLI_RESET; | |
| } else { | |
| return "\n{$out}{$text}".CLI_RESET."\n\n"; | |
| } | |
| } | |
| function parse_args($args) { | |
| $defaults = array( | |
| 'find' => '', // The string to find | |
| 'replace' => '', // The string to replace it with | |
| 'path' => __DIR__, // The directory to search in (defaults to execution directory) | |
| 'apply' => false, | |
| 'backup' => false, | |
| 'dry-run' => false, | |
| 'help' => false, | |
| ); | |
| foreach($args as $a) { | |
| if (substr($a,0,2) == '--') { | |
| if ($equals_sign = strpos($a,'=',2)) { | |
| $key = substr($a, 2, $equals_sign-2); | |
| $val = substr($a, $equals_sign+1); | |
| $defaults[$key] = $val; | |
| } | |
| else { | |
| $flag = substr($a, 2); | |
| $defaults[$flag] = true; | |
| } | |
| } | |
| } | |
| return $defaults; | |
| } | |
| function show_header() { | |
| echo CLI_CYAN,PHP_TAB . " __________ _________ ___ __ _________ _____ ", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . " / /_____/ \__/ \__/| \ | || O \ \ ~ /_ ", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . " \___\%%%%%' _`%\_/%'_|____\_|__||_________/ >&&<.-' ", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . " `BB' `BBBBBBB' `BBBBBBB' `BBBBBBB' `BBBBL.=` ", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . " _________ __________ _________ ____ _______ __________ __________", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . "| _o___)/ /_____/| _o___)/ /_____ / O \ / /_____// /_____/", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . "|___|\____\\\\___\%%%%%'|___|%%%%%'\___\_____\/___/%\___\\\\___\%%%%%'\___\%%%%%'", CLI_RESET . PHP_EOL; | |
| echo CLI_CYAN,PHP_TAB . " `BB' `BBB' `BBBBBBBB' `B' `BBBBBBBB'`BB' `BB' `BBBBBBBB' `BBBBBBBB'", CLI_RESET . PHP_EOL; | |
| echo PHP_EOL; | |
| } | |
| function show_help() { | |
| show_header(); | |
| print message(basename(__FILE__),'HELP'); | |
| print "Command line utility that will recursively scan a directory and does a full | |
| namespace-aware/class-name-only replacement (AST-safe replacement using token parsing). | |
| This will safely handle: | |
| - ClassName | |
| - \ClassName | |
| - Foo\ClassName | |
| - \Vendor\Package\ClassName | |
| - use Foo\ClassName; | |
| - extends Foo\ClassName | |
| - new Foo\ClassName() | |
| ".message('PARAMETERS:','HEADER')." | |
| --path The directory to search in (default:__DIR__) | |
| --find The string to find (required) | |
| --replace The string to replace it with (required) | |
| --dry-run Only show any replacement changes (default behavior) | |
| --apply Commit any replacement changes | |
| --backup Backup any files before commiting changes (optional) | |
| --help Displays this help message. | |
| ".message('USAGE:','HEADER')."Replace MyClass with NewClass: | |
| php ".basename(__FILE__)." --path=/path/to/dir --find=MyClass --replace=NewClass --apply | |
| Replace MyClass with NewClass with backup: | |
| php ".basename(__FILE__)." --path=/path/to/dir --find=MyClass --replace=NewClass --apply --backup | |
| Do a dry-run showing MyClass to NewClass: | |
| php ".basename(__FILE__)." --path=/path/to/dir --find=MyClass --replace=NewClass | |
| "; | |
| } | |
| $params = parse_args($argv); | |
| if ($params['help']) { | |
| show_help(); | |
| exit; | |
| } | |
| show_header(); | |
| // Validate the args: | |
| if(!is_dir(realpath($params['path']))) { | |
| echo message('Invalid directory. Check --path parameter.','ERROR'); | |
| die(); | |
| } else { | |
| $directory = $params['path']; | |
| } | |
| if (empty($params['find'])) { | |
| echo message("Parameter --find cannot be empty.",'ERROR'); | |
| die(); | |
| } else { | |
| $searchClass = $params['find']; | |
| } | |
| if (empty($params['replace'])) { | |
| echo message("Parameter --replace cannot be empty.",'ERROR'); | |
| die(); | |
| } else { | |
| $replaceClass = $params['replace']; | |
| } | |
| $mode = (is_true($params['apply']) ? '--apply' : '--dry-run'); | |
| $backup = $params['backup']; | |
| $filesChanged = 0; | |
| $totalReplacements = 0; | |
| echo ' ', PHP_TAB, PHP_TAB; | |
| echo CLI_CYAN, 'Searching for: ', CLI_BOLD, CLI_YELLOW, $searchClass, PHP_TAB; | |
| echo CLI_CYAN, 'Replacing with: ', CLI_BOLD, CLI_YELLOW, $replaceClass, CLI_RESET . PHP_EOL; | |
| $iterator = new RecursiveIteratorIterator( | |
| new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS) | |
| ); | |
| foreach ($iterator as $file) { | |
| if ($file->getExtension() !== 'php') { | |
| continue; | |
| } | |
| $path = $file->getRealPath(); | |
| $code = file_get_contents($path); | |
| $tokens = token_get_all($code); | |
| $output = ''; | |
| $modified = false; | |
| $count = 0; | |
| $i = 0; | |
| $tokenCount = count($tokens); | |
| while ($i < $tokenCount) { | |
| $token = $tokens[$i]; | |
| if (is_array($token)) { | |
| [$id, $text] = $token; | |
| // Detect possible namespaced class sequence | |
| if ($id === T_STRING || $id === T_NAME_QUALIFIED || $id === T_NAME_FULLY_QUALIFIED) { | |
| // Replace if entire token ends with BaseComponent | |
| if ($text === $searchClass) { | |
| $text = $replaceClass; | |
| $modified = true; | |
| $count++; | |
| } | |
| // Handle qualified names like Foo\Bar\BaseComponent | |
| elseif (str_contains($text, '\\')) { | |
| $parts = explode('\\', $text); | |
| $last = array_pop($parts); | |
| if ($last === $searchClass) { | |
| $last = $replaceClass; | |
| $parts[] = $last; | |
| $text = implode('\\', $parts); | |
| $modified = true; | |
| $count++; | |
| } | |
| } | |
| $output .= $text; | |
| $i++; | |
| continue; | |
| } | |
| // Handle older PHP namespace token chains manually: | |
| if ($id === T_NS_SEPARATOR) { | |
| $nameBuffer = ''; | |
| $startIndex = $i; | |
| while ( | |
| $i < $tokenCount && | |
| ( | |
| (is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) | |
| || $tokens[$i] === '\\' | |
| || (is_array($tokens[$i]) && $tokens[$i][0] === T_NS_SEPARATOR) | |
| ) | |
| ) { | |
| $nameBuffer .= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; | |
| $i++; | |
| } | |
| $parts = explode('\\', ltrim($nameBuffer, '\\')); | |
| $last = array_pop($parts); | |
| if ($last === $searchClass) { | |
| $last = $replaceClass; | |
| $parts[] = $last; | |
| $nameBuffer = '\\' . implode('\\', $parts); | |
| $modified = true; | |
| $count++; | |
| } | |
| $output .= $nameBuffer; | |
| continue; | |
| } | |
| $output .= $text; | |
| } else { | |
| $output .= $token; | |
| } | |
| $i++; | |
| } | |
| if ($modified) { | |
| $filesChanged++; | |
| $totalReplacements += $count; | |
| echo message("Updated",'STDOUT'); | |
| echo message($count,'STDOUT'); | |
| echo message("reference(s) in: ",'STDOUT'); | |
| echo CLI_GREEN, CLI_BOLD, $path, CLI_RESET . PHP_EOL; | |
| if ($mode === '--apply') { | |
| if ($backup) { | |
| copy($path, $path . '.bak'); | |
| } | |
| file_put_contents($path, $output); | |
| } | |
| } | |
| } | |
| echo message("\n Files changed: ",'STDOUT'); | |
| echo CLI_GREEN, CLI_BOLD, $filesChanged, CLI_RESET . PHP_EOL; | |
| echo message("Total replacements: ",'STDOUT'); | |
| echo CLI_GREEN, CLI_BOLD, $totalReplacements, CLI_RESET . PHP_EOL; | |
| if ($mode === '--dry-run') { | |
| echo CLI_GREEN, CLI_BOLD, " Dry run complete. No files modified.", CLI_RESET . PHP_EOL; | |
| } else { | |
| echo CLI_GREEN, CLI_BOLD, " Replacement complete.", CLI_RESET . PHP_EOL; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment