Files
rspade_system/app/RSpade/Commands/Refactor/Php/MethodUpdater.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

460 lines
17 KiB
PHP

<?php
namespace App\RSpade\Commands\Refactor\Php;
use RuntimeException;
/**
* Updates method references in PHP and Blade files
*/
class MethodUpdater
{
/**
* Rename a method definition in a class file
*
* @param string $file_path Absolute path to class file
* @param string $old_method Old method name
* @param string $new_method New method name
* @return bool True if method was found and renamed
*/
public function rename_method_definition(string $file_path, string $old_method, string $new_method): bool
{
$content = file_get_contents($file_path);
$tokens = token_get_all($content);
$output = '';
$method_found = false;
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Look for function declarations
if ($token_type === T_FUNCTION) {
// Find the method name (next T_STRING token)
$method_name_index = null;
for ($j = $i + 1; $j < min(count($tokens), $i + 10); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
$method_name_index = $j;
break;
}
}
if ($method_name_index && $tokens[$method_name_index][1] === $old_method) {
// Verify this is a static method by looking backwards
$is_static = false;
for ($k = $i - 1; $k >= max(0, $i - 20); $k--) {
if (!is_array($tokens[$k])) continue;
if ($tokens[$k][0] === T_STATIC) {
$is_static = true;
break;
}
}
if ($is_static) {
$method_found = true;
// Output everything up to but not including the method name
$output .= $token_value;
// Skip to method name and replace it
for ($j = $i + 1; $j < $method_name_index; $j++) {
$output .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
// Output new method name
$output .= $new_method;
// Skip past the old method name
$i = $method_name_index;
continue;
}
}
}
$output .= $token_value;
}
if ($method_found) {
$this->write_file_atomically($file_path, $output);
}
return $method_found;
}
/**
* Update static::/self:: references in a class file
*
* @param string $file_path Absolute path to class file
* @param string $old_method Old method name
* @param string $new_method New method name
* @return int Number of replacements made
*/
public function update_static_self_references(string $file_path, string $old_method, string $new_method): int
{
$content = file_get_contents($file_path);
$tokens = token_get_all($content);
$output = '';
$replacement_count = 0;
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Look for static:: or self::
$is_static_or_self = ($token_type === T_STATIC) ||
($token_type === T_STRING && $token_value === 'self');
if ($is_static_or_self) {
// Check for :: followed by method name
$double_colon_index = null;
$method_name_index = null;
// Find :: - can be either T_DOUBLE_COLON token or string '::'
for ($j = $i + 1; $j < min(count($tokens), $i + 5); $j++) {
$is_double_colon_token = false;
if ($tokens[$j] === '::') {
$is_double_colon_token = true;
} elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_DOUBLE_COLON) {
$is_double_colon_token = true;
}
if ($is_double_colon_token) {
$double_colon_index = $j;
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
if ($double_colon_index) {
// Find method name after ::
for ($j = $double_colon_index + 1; $j < min(count($tokens), $double_colon_index + 5); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
if ($tokens[$j][1] === $old_method) {
$method_name_index = $j;
}
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
}
if ($method_name_index) {
// Output static/self
$output .= $token_value;
// Output everything between static/self and method name (whitespace, ::)
for ($j = $i + 1; $j < $method_name_index; $j++) {
$output .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
// Output new method name
$output .= $new_method;
// Skip to after method name
$i = $method_name_index;
$replacement_count++;
continue;
}
}
$output .= $token_value;
}
if ($replacement_count > 0) {
$this->write_file_atomically($file_path, $output);
}
return $replacement_count;
}
/**
* Update Class::method references across all files
*
* @param array $references Map of file paths to occurrences from MethodReferenceScanner
* @param string $class_name Class name
* @param string $class_fqcn Fully qualified class name
* @param string $old_method Old method name
* @param string $new_method New method name
* @return int Number of files updated
*/
public function update_method_references(array $references, string $class_name, string $class_fqcn, string $old_method, string $new_method): int
{
$updated_count = 0;
foreach ($references as $file_path => $occurrences) {
if ($this->update_file_method_references($file_path, $class_name, $class_fqcn, $old_method, $new_method)) {
$updated_count++;
}
}
return $updated_count;
}
/**
* Update method references in a single file
*/
protected function update_file_method_references(string $file_path, string $class_name, string $class_fqcn, string $old_method, string $new_method): bool
{
$content = file_get_contents($file_path);
// Determine if this is a Blade file
if (str_ends_with($file_path, '.blade.php')) {
$updated_content = $this->replace_method_in_blade($content, $class_name, $class_fqcn, $old_method, $new_method);
} else {
$updated_content = $this->replace_method_in_php($content, $class_name, $class_fqcn, $old_method, $new_method);
}
// Check if any changes were made
if ($updated_content === $content) {
return false;
}
$this->write_file_atomically($file_path, $updated_content);
return true;
}
/**
* Replace method references in PHP content
*/
protected function replace_method_in_php(string $content, string $class_name, string $class_fqcn, string $old_method, string $new_method): string
{
$tokens = token_get_all($content);
$output = '';
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Look for simple class name (T_STRING) or FQCN (T_NAME_FULLY_QUALIFIED) followed by :: and method name
$is_match = false;
if ($token_type === T_STRING && $token_value === $class_name) {
$is_match = true;
} elseif (defined('T_NAME_FULLY_QUALIFIED') && $token_type === T_NAME_FULLY_QUALIFIED) {
// Strip leading backslash for comparison
$normalized_token = ltrim($token_value, '\\');
$normalized_fqcn = ltrim($class_fqcn, '\\');
if ($normalized_token === $normalized_fqcn) {
$is_match = true;
}
}
if ($is_match) {
$double_colon_index = null;
$method_name_index = null;
// Find :: - can be either T_DOUBLE_COLON token or string '::'
for ($j = $i + 1; $j < min(count($tokens), $i + 5); $j++) {
$is_double_colon_token = false;
if ($tokens[$j] === '::') {
$is_double_colon_token = true;
} elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_DOUBLE_COLON) {
$is_double_colon_token = true;
}
if ($is_double_colon_token) {
$double_colon_index = $j;
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
if ($double_colon_index) {
// Find method name after ::
for ($j = $double_colon_index + 1; $j < min(count($tokens), $double_colon_index + 5); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
if ($tokens[$j][1] === $old_method) {
$method_name_index = $j;
}
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
}
if ($method_name_index) {
// Output class name (simple or FQCN)
$output .= $token_value;
// Output everything between class and method name (whitespace, ::)
for ($j = $i + 1; $j < $method_name_index; $j++) {
$output .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
// Output new method name
$output .= $new_method;
// Skip to after method name
$i = $method_name_index;
continue;
}
}
$output .= $token_value;
}
return $output;
}
/**
* Replace method references in Blade content
*/
protected function replace_method_in_blade(string $content, string $class_name, string $class_fqcn, string $old_method, string $new_method): string
{
// Pattern to match simple Class::method with word boundaries
$pattern1 = '/\b' . preg_quote($class_name, '/') . '\s*::\s*' . preg_quote($old_method, '/') . '\b/';
$content = preg_replace($pattern1, $class_name . '::' . $new_method, $content);
// Pattern to match FQCN \Namespace\Class::method
$escaped_fqcn = preg_quote($class_fqcn, '/');
$pattern2 = '/\\\\?' . $escaped_fqcn . '\s*::\s*' . preg_quote($old_method, '/') . '\b/';
$content = preg_replace($pattern2, '\\' . $class_fqcn . '::' . $new_method, $content);
return $content;
}
/**
* Update controller action references in Route() calls and Ajax endpoints
* Only processes files in rsx/ directory that are in the manifest
*
* @param string $class_name Controller class name
* @param string $old_action Old action/method name
* @param string $new_action New action/method name
* @return int Number of files updated
*/
public function update_controller_action_route_references(string $class_name, string $old_action, string $new_action): int
{
$updated_count = 0;
// Get all files from manifest in rsx/ directory
$manifest = \App\RSpade\Core\Manifest\Manifest::get_all();
foreach ($manifest as $relative_path => $metadata) {
// Only process files in rsx/ directory
if (!str_starts_with($relative_path, 'rsx/')) {
continue;
}
$extension = $metadata['extension'] ?? '';
$file_path = base_path($relative_path);
if (!file_exists($file_path)) {
continue;
}
$content = file_get_contents($file_path);
$updated_content = $content;
// Apply replacements based on file type
if ($extension === 'js' || $extension === 'jqhtml') {
// Replace Rsx.Route('CLASS', 'OLD_ACTION'
// Pattern matches with optional whitespace between parameters
$updated_content = preg_replace(
'/\bRsx\.Route\(\s*[\'"]' . preg_quote($class_name, '/') . '[\'"]\s*,\s*[\'"]' . preg_quote($old_action, '/') . '[\'"]/',
'Rsx.Route(\'' . $class_name . '\', \'' . $new_action . '\'',
$updated_content
);
}
if ($extension === 'jqhtml' || $extension === 'blade.php') {
// Replace $action="OLD_ACTION" when $controller="CLASS_NAME" is on same line
// This uses a callback to check both attributes are present
$updated_content = preg_replace_callback(
'/^.*$/m',
function ($matches) use ($class_name, $old_action, $new_action) {
$line = $matches[0];
// Check if line has both $controller="CLASS_NAME" and $action="OLD_ACTION"
$has_controller = preg_match('/\$controller=["\']' . preg_quote($class_name, '/') . '["\']/', $line);
$has_action = preg_match('/\$action=["\']' . preg_quote($old_action, '/') . '["\']/', $line);
if ($has_controller && $has_action) {
// Replace the action
return preg_replace(
'/(\$action=["\'])' . preg_quote($old_action, '/') . '(["\'])/',
'$1' . $new_action . '$2',
$line
);
}
return $line;
},
$updated_content
);
}
if ($extension === 'js') {
// Replace Ajax endpoint calls: CLASS.OLD_ACTION(
// Pattern: (non-alnum)(CLASS).(METHOD)(non-alnum)
$updated_content = preg_replace(
'/(?<=[^a-zA-Z0-9_])' . preg_quote($class_name, '/') . '\.' . preg_quote($old_action, '/') . '(?=[^a-zA-Z0-9_])/',
$class_name . '.' . $new_action,
$updated_content
);
}
if ($extension === 'php' || $extension === 'blade.php') {
// Replace Rsx::Route('CLASS', 'OLD_ACTION'
// Pattern matches with optional whitespace between parameters
$updated_content = preg_replace(
'/\bRsx::Route\(\s*[\'"]' . preg_quote($class_name, '/') . '[\'"]\s*,\s*[\'"]' . preg_quote($old_action, '/') . '[\'"]/',
'Rsx::Route(\'' . $class_name . '\', \'' . $new_action . '\'',
$updated_content
);
}
// Write if changed
if ($updated_content !== $content) {
file_put_contents($file_path, $updated_content);
$updated_count++;
}
}
return $updated_count;
}
/**
* Write file atomically using temp file
*/
protected function write_file_atomically(string $file_path, string $content): void
{
$temp_file = $file_path . '.refactor-temp';
if (file_put_contents($temp_file, $content) === false) {
throw new RuntimeException("Failed to write temp file: {$temp_file}");
}
if (!rename($temp_file, $file_path)) {
@unlink($temp_file);
throw new RuntimeException("Failed to replace file: {$file_path}");
}
}
}