$value) { if ($key === 'file' && is_string($value)) { $data[$key] = normalize_ide_path($value); } elseif (is_array($value)) { $data[$key] = normalize_response_paths($value); } } } return $data; } // JSON response helper function json_response($data, $code = 200) { // Normalize all file paths in response for IDE consumption $data = normalize_response_paths($data); http_response_code($code); header('Content-Type: application/json'); $json = json_encode($data); // Sign response if we have auth global $auth_data; if (!empty($auth_data['server_key'])) { $signature = sha1($json . $auth_data['server_key']); header('X-Signature: ' . $signature); } echo $json; exit; } // Error response helper function error_response($message, $code = 400) { json_response(['success' => false, 'error' => $message], $code); } // Authentication already handled by auth.php before this file is loaded // Retrieve auth data from constants set by auth.php if (!defined('IDE_AUTH_PASSED')) { error_response('Authentication check did not run - this should never happen', 500); } $auth_data = json_decode(IDE_AUTH_DATA, true); // Parse request URI to get service $request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $service_path = str_replace('/_ide/service', '', $request_uri); $service_path = trim($service_path, '/'); // Get request body $request_body = file_get_contents('php://input'); $request_data = json_decode($request_body, true); // Merge GET parameters (for backward compatibility with /_idehelper endpoints) if ($_SERVER['REQUEST_METHOD'] === 'GET') { $request_data = array_merge($request_data ?? [], $_GET); } // Route to appropriate service handler // This handler handles lightweight services that don't need Laravel bootstrap // All services requiring Manifest/Laravel are handled by laravel_handler.php // Note: auth/create is handled in auth.php before routing switch ($service_path) { case 'format': handle_format_service($request_data); break; case 'definition': handle_definition_service($request_data); break; case 'complete': handle_complete_service($request_data); break; case 'exec': case 'command': handle_exec_service($request_data); break; case 'resolve_class': handle_resolve_class_service($request_data); break; case 'git': handle_git_service($request_data); break; case 'git/diff': handle_git_diff_service($request_data); break; case 'health': json_response(['success' => true, 'service' => 'ide-standalone', 'version' => '1.0.0']); break; default: error_response('Unknown service: ' . $service_path . ' (standalone handler supports: format, definition, complete, exec, command, resolve_class, git, git/diff)', 404); } /** * Handle format service - formats PHP files */ function handle_format_service($data) { $file = $data['file'] ?? null; if (!$file) { error_response('No file provided'); } // Normalize path separators (Windows sends backslashes, convert to forward slashes) $file = str_replace('\\', '/', $file); // Build full path $full_path = IDE_BASE_PATH . '/' . ltrim($file, '/'); // Normalize path without requiring file to exist (for .formatting.tmp files) // Use realpath on the directory, then append the filename $dir = dirname($full_path); $filename = basename($full_path); // Get real directory path if (is_dir($dir)) { // @REALPATH-EXCEPTION - IDE service: runs standalone without framework helpers $real_dir = realpath($dir); if ($real_dir) { $full_path = $real_dir . '/' . $filename; } } // Security check - path must be within project if (strpos($full_path, IDE_BASE_PATH) !== 0) { error_response('Invalid file path - must be within project'); } if (!file_exists($full_path)) { error_response('File not found', 404); } // Execute formatter $formatter_path = ide_framework_path('bin/rsx-format'); if (!file_exists($formatter_path)) { error_response('Formatter not found', 500); } // Log formatting request $log_file = ide_framework_path('storage/logs/ide-formatter.log'); $log_dir = dirname($log_file); if (!is_dir($log_dir)) { mkdir($log_dir, 0755, true); } $log_entry = sprintf( "[%s] Formatting file: %s\n Full path: %s\n CWD before: %s\n File exists: %s\n File size: %d bytes\n", date('Y-m-d H:i:s'), $file, $full_path, getcwd(), file_exists($full_path) ? 'yes' : 'no', file_exists($full_path) ? filesize($full_path) : 0 ); file_put_contents($log_file, $log_entry, FILE_APPEND); // Change to project root so formatter can find config files $old_cwd = getcwd(); chdir(IDE_BASE_PATH); $command = sprintf('php %s %s 2>&1', escapeshellarg($formatter_path), escapeshellarg($full_path) ); file_put_contents($log_file, " CWD after chdir: " . getcwd() . "\n Command: $command\n", FILE_APPEND); $output = []; $return_var = 0; \exec_safe($command, $output, $return_var); // Restore working directory chdir($old_cwd); // Log result file_put_contents($log_file, sprintf( " Return code: %d\n Output lines: %d\n Output: %s\n File size after: %d bytes\n\n", $return_var, count($output), implode("\n ", $output), file_exists($full_path) ? filesize($full_path) : 0 ), FILE_APPEND); if ($return_var === 0) { // Read formatted content if requested $content = null; if (!empty($data['return_content'])) { $content = file_get_contents($full_path); } json_response([ 'success' => true, 'message' => 'Formatting completed', 'content' => $content ]); } else { error_response('Formatting failed: ' . implode("\n", $output), 500); } } /** * Handle definition service - find symbol definitions * Supports all the types from the VS Code definition provider */ function handle_definition_service($data) { $identifier = $data['identifier'] ?? $data['symbol'] ?? null; $method = $data['method'] ?? null; $type = $data['type'] ?? null; if (!$identifier) { error_response('No identifier provided'); } // Load manifest data $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found - run php artisan rsx:manifest:build', 500); } $manifest_raw = include $manifest_file; // The manifest structure has the data under 'data' key $manifest = $manifest_raw['data'] ?? $manifest_raw; // Search based on type $result = null; switch ($type) { case 'class': // Search PHP classes with optional method if (isset($manifest['php']['classes'][$identifier])) { $class_data = $manifest['php']['classes'][$identifier]; $file = $class_data['file']; $line = $class_data['line'] ?? 1; // If method specified, try to find method line number if ($method && isset($class_data['methods'][$method])) { $line = $class_data['methods'][$method]['line'] ?? $line; } $result = [ 'found' => true, 'file' => $file, 'line' => $line ]; } break; case 'view': // Search Blade views by name or rsx_id foreach ($manifest['blade']['views'] ?? [] as $view) { if ($view['name'] === $identifier || $view['rsx_id'] === $identifier) { $result = [ 'found' => true, 'file' => $view['file'], 'line' => 1 ]; break; } } break; case 'bundle_alias': // Search bundle aliases defined in config/rsx.php $config_file = ide_framework_path('config/rsx.php'); if (file_exists($config_file)) { $config = include $config_file; if (isset($config['bundle_aliases'][$identifier])) { $bundle_class = $config['bundle_aliases'][$identifier]; // Extract class name from namespace $class_name = str_replace('\\', '_', str_replace('App\\RSpade\\', '', $bundle_class)); // Find the bundle class file if (isset($manifest['php']['classes'][$class_name])) { $result = [ 'found' => true, 'file' => $manifest['php']['classes'][$class_name]['file'], 'line' => $manifest['php']['classes'][$class_name]['line'] ?? 1 ]; } } } break; case 'jqhtml_template': // Search for .jqhtml template files foreach ($manifest['jqhtml']['components'] ?? [] as $component) { if ($component['name'] === $identifier) { $result = [ 'found' => true, 'file' => $component['template_file'], 'line' => 1 ]; break; } } break; case 'jqhtml_class': // Search for JavaScript class extending Jqhtml_Component foreach ($manifest['jqhtml']['components'] ?? [] as $component) { if ($component['name'] === $identifier && isset($component['js_file'])) { $result = [ 'found' => true, 'file' => $component['js_file'], 'line' => $component['js_line'] ?? 1 ]; break; } } break; case 'jqhtml_class_method': // Search for method in jqhtml component JavaScript class foreach ($manifest['jqhtml']['components'] ?? [] as $component) { if ($component['name'] === $identifier && isset($component['js_file'])) { $js_file = IDE_BASE_PATH . '/' . $component['js_file']; $line = 1; // Try to find the method line number if ($method && file_exists($js_file)) { $content = file_get_contents($js_file); $lines = explode("\n", $content); foreach ($lines as $index => $line_content) { // Look for method definition patterns if (preg_match('/^\s*' . preg_quote($method) . '\s*\(/', $line_content) || preg_match('/^\s*async\s+' . preg_quote($method) . '\s*\(/', $line_content) || preg_match('/^\s*static\s+' . preg_quote($method) . '\s*\(/', $line_content)) { $line = $index + 1; break; } } } $result = [ 'found' => true, 'file' => $component['js_file'], 'line' => $line ]; break; } } break; case 'route': // Search routes for controller::method foreach ($manifest['php']['routes'] ?? [] as $route) { if ($route['class'] . '::' . $route['method'] === $identifier) { $result = [ 'found' => true, 'file' => $route['file'], 'line' => $route['line'] ?? 1 ]; break; } } break; default: // Try to auto-detect type // First try as class if (isset($manifest['php']['classes'][$identifier])) { $class_data = $manifest['php']['classes'][$identifier]; $result = [ 'found' => true, 'file' => $class_data['file'], 'line' => $class_data['line'] ?? 1 ]; } // Try as view if (!$result) { foreach ($manifest['blade']['views'] ?? [] as $view) { if ($view['name'] === $identifier || $view['rsx_id'] === $identifier) { $result = [ 'found' => true, 'file' => $view['file'], 'line' => 1 ]; break; } } } // Try as jqhtml component if (!$result) { foreach ($manifest['jqhtml']['components'] ?? [] as $component) { if ($component['name'] === $identifier) { $result = [ 'found' => true, 'file' => $component['template_file'] ?? $component['js_file'], 'line' => 1 ]; break; } } } } if ($result) { json_response($result); } else { json_response(['found' => false]); } } /** * Handle complete service - provide autocomplete suggestions */ function handle_complete_service($data) { $prefix = $data['prefix'] ?? ''; $context = $data['context'] ?? null; // Load manifest data $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found', 500); } $manifest = include $manifest_file; $suggestions = []; // Search PHP classes foreach ($manifest['php']['classes'] ?? [] as $class_name => $class_data) { if (stripos($class_name, $prefix) === 0) { $suggestions[] = [ 'label' => $class_name, 'kind' => 'class', 'detail' => $class_data['file'] ]; } } // Limit results $suggestions = array_slice($suggestions, 0, 100); json_response([ 'success' => true, 'suggestions' => $suggestions ]); } /** * Handle exec service - execute artisan commands */ function handle_exec_service($data) { $command = $data['command'] ?? null; if (!$command) { error_response('No command provided'); } // Whitelist of allowed commands $allowed_commands = [ 'rsx:check', 'rsx:manifest:build', 'rsx:clean', 'rsx:bundle:compile', 'rsx:routes' ]; // Extract base command $parts = explode(' ', $command); $base_command = $parts[0]; if (!in_array($base_command, $allowed_commands)) { error_response('Command not allowed: ' . $base_command, 403); } // Build full artisan command $artisan_path = IDE_BASE_PATH . '/artisan'; $full_command = sprintf('php %s %s 2>&1', escapeshellarg($artisan_path), $command ); $output = []; $return_var = 0; \exec_safe($full_command, $output, $return_var); json_response([ 'success' => $return_var === 0, 'output' => implode("\n", $output), 'exit_code' => $return_var ]); } /** * Handle git service - get git status */ function handle_git_service($data) { $output = []; $return_var = 0; \exec_safe('cd ' . escapeshellarg(IDE_BASE_PATH) . ' && git status --porcelain 2>&1', $output, $return_var); if ($return_var !== 0) { error_response('Git command failed: ' . implode("\n", $output), 500); } $modified = []; $added = []; $conflicts = []; foreach ($output as $line) { if (strlen($line) < 4) continue; $status = substr($line, 0, 2); $file = trim(substr($line, 3)); // Parse git status codes if (str_contains($status, 'U') || ($status[0] === 'A' && $status[1] === 'A')) { $conflicts[] = $file; } elseif ($status[0] === 'A' || $status[1] === 'A' || $status === '??') { $added[] = $file; } elseif ($status[0] === 'M' || $status[1] === 'M' || $status[0] === 'R') { $modified[] = $file; } } json_response([ 'success' => true, 'modified' => $modified, 'added' => $added, 'conflicts' => $conflicts ]); } /** * Handle git diff service - get line-level changes for a file */ function handle_git_diff_service($data) { $file = $data['file'] ?? null; if (!$file) { error_response('No file provided'); } // Normalize path separators (Windows sends backslashes, convert to forward slashes) $file = str_replace('\\', '/', $file); // Security check - file must be within project $full_path = IDE_BASE_PATH . '/' . ltrim($file, '/'); // Normalize path without requiring file to exist (handles deleted files in git) $dir = dirname($full_path); $filename = basename($full_path); if (is_dir($dir)) { // @REALPATH-EXCEPTION - IDE service: runs standalone without framework helpers $real_dir = realpath($dir); if ($real_dir) { $full_path = $real_dir . '/' . $filename; } } // Validate path is within project if (strpos($full_path, IDE_BASE_PATH) !== 0) { error_response('Invalid file path'); } // Note: File existence not required for git diff (might be deleted) $output = []; $return_var = 0; // Get diff with line numbers, no context \exec_safe('cd ' . escapeshellarg(IDE_BASE_PATH) . ' && git diff HEAD --unified=0 ' . escapeshellarg($file) . ' 2>&1', $output, $return_var); if ($return_var !== 0) { error_response('Git diff command failed: ' . implode("\n", $output), 500); } $added = []; $modified = []; $deleted = []; foreach ($output as $line) { // Parse unified diff format: @@ -old_start,old_count +new_start,new_count @@ if (preg_match('/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/', $line, $matches)) { $old_start = (int)$matches[1]; $old_count = isset($matches[2]) ? (int)$matches[2] : 1; $new_start = (int)$matches[3]; $new_count = isset($matches[4]) ? (int)$matches[4] : 1; // Deletion: old_count > 0, new_count = 0 if ($old_count > 0 && $new_count === 0) { $deleted[] = [$new_start, $new_start]; } // Addition: old_count = 0, new_count > 0 elseif ($old_count === 0 && $new_count > 0) { $added[] = [$new_start, $new_start + $new_count - 1]; } // Modification: both > 0 else { $modified[] = [$new_start, $new_start + $new_count - 1]; } } } json_response([ 'success' => true, 'added' => $added, 'modified' => $modified, 'deleted' => $deleted ]); } /** * Handle command service - execute artisan commands with output capture */ function handle_command_service($data) { $command = $data['command'] ?? null; $arguments = $data['arguments'] ?? []; if (!$command) { error_response('No command provided'); } // Whitelist of allowed commands $allowed_commands = [ 'rsx:check', 'rsx:manifest:build', 'rsx:clean', 'rsx:bundle:compile', 'rsx:routes', 'rsx:refactor:rename_php_class', 'rsx:refactor:rename_php_class_function', 'rsx:refactor:sort_php_class_functions' ]; if (!in_array($command, $allowed_commands)) { error_response('Command not allowed: ' . $command, 403); } // Build artisan command with arguments $artisan_path = IDE_BASE_PATH . '/artisan'; $command_parts = [ 'php', escapeshellarg($artisan_path), $command ]; // Add arguments foreach ($arguments as $arg) { $command_parts[] = escapeshellarg($arg); } $full_command = implode(' ', $command_parts) . ' 2>&1'; // Change to project root $old_cwd = getcwd(); chdir(IDE_BASE_PATH); // Capture output using ob_start ob_start(); $output = []; $return_var = 0; \exec_safe($full_command, $output, $return_var); $ob_output = ob_get_clean(); // Restore working directory chdir($old_cwd); $combined_output = trim($ob_output . "\n" . implode("\n", $output)); json_response([ 'success' => $return_var === 0, 'output' => $combined_output, 'exit_code' => $return_var ]); } /** * Try to resolve identifier as a PHP class */ function try_resolve_php_class($identifier, $method_name, $find_php_class) { $class_data = $find_php_class($identifier); if (!$class_data) { return null; } $file_path = $class_data['file']; $line_number = 1; $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); $lines = explode("\n", $content); if ($method_name) { // Check if method metadata exists if (isset($class_data['public_static_methods'][$method_name])) { $method_metadata = $class_data['public_static_methods'][$method_name]; if (isset($method_metadata['line'])) { $line_number = $method_metadata['line']; } } else if (isset($class_data['public_instance_methods'][$method_name])) { $method_metadata = $class_data['public_instance_methods'][$method_name]; if (isset($method_metadata['line'])) { $line_number = $method_metadata['line']; } } // If we don't have line number from metadata, search manually if ($line_number === 1 && !empty($lines)) { $in_class_or_trait = false; $brace_count = 0; foreach ($lines as $index => $line) { if (preg_match('/^\s*(class|trait)\s+\w+(\s|$)/', $line)) { $in_class_or_trait = true; } if ($in_class_or_trait) { $brace_count += substr_count($line, '{'); $brace_count -= substr_count($line, '}'); if (preg_match('/^\s*(public|protected|private)?\s*(static\s+)?function\s+' . preg_quote($method_name, '/') . '\s*\(/', $line)) { $line_number = $index + 1; break; } if ($brace_count === 0 && strpos($line, '}') !== false) { break; } } } } } else { // Find the line with the class definition foreach ($lines as $index => $line) { if (preg_match('/^\s*class\s+' . preg_quote($identifier, '/') . '(\s|$)/', $line)) { $line_number = $index + 1; break; } } } } $response_data = [ 'found' => true, 'type' => 'php_class', 'file' => $file_path, 'line' => $line_number, 'metadata' => [ 'namespace' => $class_data['namespace'] ?? null, 'extends' => $class_data['extends'] ?? null, 'fqcn' => $class_data['fqcn'] ?? null, ], ]; if ($method_name) { $response_data['method'] = $method_name; } return $response_data; } /** * Try to resolve identifier as a JavaScript class */ function try_resolve_js_class($identifier, $method_name, $files) { foreach ($files as $file_path => $file_data) { // Only check .js files (not .jqhtml) if (str_ends_with($file_path, '.js') && !str_ends_with($file_path, '.jqhtml')) { $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); // Check for JavaScript class (not specifically Jqhtml_Component) if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) { $lines = explode("\n", $content); $line_number = 1; foreach ($lines as $index => $line) { if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) { $line_number = $index + 1; break; } } return [ 'found' => true, 'type' => 'js_class', 'file' => $file_path, 'line' => $line_number, 'identifier' => $identifier, 'extends' => $matches[1] ?? null, ]; } } } } return null; } /** * Try to resolve identifier as a jqhtml component class * * Note: This resolver does NOT check inheritance chains. It's a simple file lookup. * VS Code extension should use js_is_subclass_of() RPC to determine if a class * is actually a Jqhtml_Component before choosing this resolver type. */ function try_resolve_jqhtml_class($identifier, $method_name, $files) { // Load manifest to get js_classes index $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { return null; } $manifest_raw = include $manifest_file; $manifest_data = $manifest_raw['data'] ?? $manifest_raw; $js_classes = $manifest_data['js_classes'] ?? []; // Check if this class exists in js_classes if (!isset($js_classes[$identifier])) { return null; } // Get the file path $file_path = $js_classes[$identifier]; // Find the line number $absolute_path = IDE_BASE_PATH . '/' . $file_path; $line_number = 1; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); $lines = explode("\n", $content); foreach ($lines as $index => $line) { if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) { $line_number = $index + 1; break; } } } return [ 'found' => true, 'type' => 'jqhtml_class', 'file' => $file_path, 'line' => $line_number, 'identifier' => $identifier, ]; } /** * Try to resolve identifier as a view */ function try_resolve_view($identifier, $find_view) { $view_data = $find_view($identifier); if (!$view_data) { return null; } $file_path = $view_data['file']; $line_number = 1; $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); $lines = explode("\n", $content); // Look for @rsx_id('identifier') foreach ($lines as $index => $line) { if (preg_match('/@rsx_id\s*\(\s*[\'"]' . preg_quote($identifier, '/') . '[\'"]\s*\)/', $line)) { $line_number = $index + 1; break; } } } return [ 'found' => true, 'type' => 'view', 'file' => $file_path, 'line' => $line_number, 'identifier' => $identifier, ]; } /** * Try to resolve identifier as a bundle alias */ function try_resolve_bundle_alias($identifier, $find_php_class) { $config_path = IDE_SYSTEM_PATH . '/config/rsx.php'; if (!file_exists($config_path)) { return null; } $config = include $config_path; if (!isset($config['bundle_aliases'][$identifier])) { return null; } $bundle_class = $config['bundle_aliases'][$identifier]; $class_parts = explode('\\', $bundle_class); $class_name = end($class_parts); $class_data = $find_php_class($class_name); if (!$class_data) { return null; } $file_path = $class_data['file']; $line_number = 1; $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); $lines = explode("\n", $content); foreach ($lines as $index => $line) { if (preg_match('/^\s*class\s+' . preg_quote($class_name, '/') . '(\s|$)/', $line)) { $line_number = $index + 1; break; } } } return [ 'found' => true, 'type' => 'bundle_alias', 'file' => $file_path, 'line' => $line_number, 'identifier' => $identifier, 'resolved_class' => $bundle_class, ]; } /** * Try to resolve identifier as a jqhtml template */ function try_resolve_jqhtml_template($identifier, $files, $camel_to_snake, $snake_to_pascal) { $component_snake = $camel_to_snake($identifier); foreach ($files as $file_path => $file_data) { if (str_ends_with($file_path, '.jqhtml')) { $basename = basename($file_path, '.jqhtml'); if ($basename === $component_snake || $snake_to_pascal($basename) === $identifier) { $line_number = 1; $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); $lines = explode("\n", $content); foreach ($lines as $index => $line) { if (preg_match('/ true, 'type' => 'jqhtml_template', 'file' => $file_path, 'line' => $line_number, 'identifier' => $identifier, ]; } } } return null; } /** * Try to resolve method in jqhtml component */ function try_resolve_jqhtml_method($identifier, $method_name, $files) { $search_method = ($method_name === 'data') ? 'on_load' : $method_name; foreach ($files as $file_path => $file_data) { if (str_ends_with($file_path, '.js')) { $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Jqhtml_Component/', $content)) { $lines = explode("\n", $content); $line_number = 1; $in_class = false; $brace_count = 0; foreach ($lines as $index => $line) { if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) { $in_class = true; } if ($in_class) { $brace_count += substr_count($line, '{'); $brace_count -= substr_count($line, '}'); if (preg_match('/(?:async\s+)?' . preg_quote($search_method, '/') . '\s*\(/', $line)) { $line_number = $index + 1; break; } if ($brace_count === 0 && strpos($line, '}') !== false) { break; } } } return [ 'found' => true, 'type' => 'jqhtml_class_method', 'file' => $file_path, 'line' => $line_number, 'identifier' => $identifier, 'method' => $method_name, ]; } } } } return null; } /** * Handle resolve_class service - resolve class/view/component definitions * * RESOLUTION TYPE SYSTEM * ====================== * * This endpoint resolves identifiers to file locations based on type priority. * It supports CSV type lists for priority-based resolution. * * AVAILABLE TYPES: * * 1. php_class - PHP class files (*.php) * - Searches manifest for PHP classes * - Supports method lookup with fallback to class * - Returns: file path, line number, namespace, extends, fqcn * * 2. js_class - JavaScript class files (*.js, not .jqhtml) * - Searches for JS classes with extends clause * - Does NOT require extending Jqhtml_Component * - Returns: file path, line number, extends * * 3. jqhtml_class - jqhtml component JavaScript files (*.js) * - Searches for classes extending Jqhtml_Component * - Returns: file path, line number * * 4. view - Blade view templates (*.blade.php) * - Searches for @rsx_id directives * - Returns: file path, line number * * 5. bundle_alias - Bundle configuration aliases * - Resolves bundle aliases from config/rsx.php * - Returns: bundle class file location * * 6. jqhtml_template - jqhtml template files (*.jqhtml) * - Searches for tags * - Returns: template file path, line number * * 7. jqhtml_class_method - Methods in jqhtml component classes * - Requires both identifier and method parameters * - Searches within Jqhtml_Component subclasses * - Returns: file path, method line number * * CSV TYPE LISTS: * =============== * * The type parameter accepts comma-separated lists for priority-based resolution. * The server tries each type in order and returns the first match found. * * Examples: * - type=php_class,js_class * Try PHP classes first, then JavaScript classes * Use case: From PHP files (always want PHP), fallback to JS * * - type=js_class,php_class * Try JavaScript classes first, then PHP classes * Use case: From JS files (component inheritance), fallback to PHP controllers * * - type=jqhtml_class,js_class * Try jqhtml components first, then general JS classes * Use case: jqhtml extends="" attribute resolution * * METHOD RESOLUTION: * ================== * * When the method parameter is provided, the resolver attempts to find the specific * method within the class. If the method exists, returns its line number. If the * method doesn't exist but the class does, returns the class line number as fallback. * * This allows graceful degradation: clicking on Controller.missing_method() will * navigate to the Controller class even if missing_method() doesn't exist. * * USAGE FROM VS CODE: * =================== * * The VS Code extension determines which types to use based on context: * * - Route patterns → php_class (routes are always PHP) * - PHP class references → php_class (PHP files only reference PHP) * - JS class references → js_class,php_class (try JS first, then PHP controllers) * - jqhtml extends="" → jqhtml_class,js_class (component inheritance) * - jqhtml $xxx=Class.method → js_class,php_class (components or controllers) * - jqhtml $xxx=this.method → jqhtml_class_method (current component only) * * PARAMETERS: * - identifier/class (required): The class/view/component name to resolve * - method (optional): Method name within the class * - type (optional): Single type or CSV list of types to try in order */ function handle_resolve_class_service($data) { $identifier = $data['class'] ?? $data['identifier'] ?? null; $method_name = $data['method'] ?? null; $type = $data['type'] ?? null; if (!$identifier) { json_response([ 'error' => 'Missing required parameter: class or identifier', 'found' => false, ], 400); } // Support CSV type list for priority ordering $type_list = []; if ($type) { $type_list = array_map('trim', explode(',', $type)); } // Load manifest data $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found - run php artisan rsx:manifest:build', 500); } $manifest_raw = include $manifest_file; $manifest_data = $manifest_raw['data'] ?? $manifest_raw; $files = $manifest_data['files'] ?? []; // Helper function to find PHP class in manifest $find_php_class = function($class_name) use ($files) { foreach ($files as $file_path => $file_data) { // Only match PHP files - check file extension and presence of PHP-specific metadata if (isset($file_data['class']) && $file_data['class'] === $class_name) { // Must be a PHP file (not .js, not .jqhtml) if (str_ends_with($file_path, '.php')) { // Add file path to the data so caller knows which file it came from $file_data['file'] = $file_path; return $file_data; } } } return null; }; // Helper function to find view in manifest $find_view = function($view_name) use ($files) { foreach ($files as $file_path => $file_data) { if (isset($file_data['id']) && $file_data['id'] === $view_name) { return $file_data; } } return null; }; // Helper function to convert PascalCase to snake_case $camel_to_snake = function($input) { $result = preg_replace('/(? false, 'error' => 'Identifier not found in manifest', 'identifier' => $identifier, 'searched_types' => $type_list, ]); } /** * OBSOLETE LEGACY CODE REMOVED * ============================= * The code below (approximately lines 1410-1685) was the old resolution system * that tried each type individually without CSV support. It has been replaced * by the new CSV type list system above with dedicated resolver functions. * * Removed sections: * - Legacy PHP class fallback (duplicate of try_resolve_php_class) * - Legacy view resolution (duplicate of try_resolve_view) * - Legacy bundle alias resolution (duplicate of try_resolve_bundle_alias) * - Legacy jqhtml template resolution (duplicate of try_resolve_jqhtml_template) * - Legacy jqhtml class resolution (duplicate of try_resolve_jqhtml_class) * - Legacy jqhtml method resolution (duplicate of try_resolve_jqhtml_method) */ /** * Handle js_lineage service - get JavaScript class inheritance chain * This is a port of the Ide_Helper_Controller::js_lineage() method */ function handle_js_lineage_service($data) { $class_name = $data['class'] ?? null; if (!$class_name) { json_response([ 'error' => 'Missing required parameter: class', ], 400); } // Load manifest data $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found - run php artisan rsx:manifest:build', 500); } $manifest_raw = include $manifest_file; $manifest_data = $manifest_raw['data'] ?? $manifest_raw; $files = $manifest_data['files'] ?? []; // Find the JavaScript class and trace its lineage $lineage = []; $current_class = $class_name; // Helper to find extends clause in JS file $find_extends = function($file_path) { $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); if (preg_match('/class\s+\w+\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) { return $matches[1]; } } return null; }; // Trace up to 10 levels to prevent infinite loops for ($i = 0; $i < 10; $i++) { $found = false; foreach ($files as $file_path => $file_data) { if (str_ends_with($file_path, '.js')) { $absolute_path = IDE_BASE_PATH . '/' . $file_path; if (file_exists($absolute_path)) { $content = file_get_contents($absolute_path); // Check if this file defines the current class if (preg_match('/class\s+' . preg_quote($current_class, '/') . '\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) { $parent_class = $matches[1]; $lineage[] = $parent_class; $current_class = $parent_class; $found = true; break; } } } } if (!$found) { break; } } json_response([ 'class' => $class_name, 'lineage' => $lineage, ]); } /** * Handle js_is_subclass_of service - check if JS class extends another * * This is a direct RPC wrapper around Manifest::js_is_subclass_of() * * PARAMETERS: * - subclass (required): The potential subclass name * - superclass (required): The potential superclass name * * RETURNS: * - is_subclass: boolean - true if subclass extends superclass anywhere in chain */ function handle_js_is_subclass_of_service($data) { $subclass = $data['subclass'] ?? null; $superclass = $data['superclass'] ?? null; if (!$subclass || !$superclass) { json_response([ 'error' => 'Missing required parameters: subclass and superclass', ], 400); } // Load manifest $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found - run php artisan rsx:manifest:build', 500); } $manifest_raw = include $manifest_file; $manifest_data = $manifest_raw['data'] ?? $manifest_raw; $js_classes = $manifest_data['js_classes'] ?? []; $files = $manifest_data['files'] ?? []; // Implement same logic as Manifest::js_is_subclass_of $current_class = $subclass; $visited = []; while ($current_class) { // Prevent infinite loops if (in_array($current_class, $visited)) { json_response(['is_subclass' => false]); } $visited[] = $current_class; // Find the current class in the manifest if (!isset($js_classes[$current_class])) { json_response(['is_subclass' => false]); } // Get file metadata $file_path = $js_classes[$current_class]; $metadata = $files[$file_path] ?? null; if (!$metadata || empty($metadata['extends'])) { json_response(['is_subclass' => false]); } if ($metadata['extends'] == $superclass) { json_response(['is_subclass' => true]); } // Move up the chain to the parent class $current_class = $metadata['extends']; } json_response(['is_subclass' => false]); } /** * Handle php_is_subclass_of service - check if PHP class extends another * * This is a direct RPC wrapper around Manifest::php_is_subclass_of() * * PARAMETERS: * - subclass (required): The potential subclass name * - superclass (required): The potential superclass name * * RETURNS: * - is_subclass: boolean - true if subclass extends superclass anywhere in chain */ function handle_php_is_subclass_of_service($data) { $subclass = $data['subclass'] ?? null; $superclass = $data['superclass'] ?? null; if (!$subclass || !$superclass) { json_response([ 'error' => 'Missing required parameters: subclass and superclass', ], 400); } // Load manifest $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found - run php artisan rsx:manifest:build', 500); } $manifest_raw = include $manifest_file; $manifest_data = $manifest_raw['data'] ?? $manifest_raw; $files = $manifest_data['files'] ?? []; // Implement same logic as Manifest::php_is_subclass_of $current_class = $subclass; $visited = []; while ($current_class) { // Prevent infinite loops if (in_array($current_class, $visited)) { json_response(['is_subclass' => false]); } $visited[] = $current_class; // Find the current class in files $found = false; foreach ($files as $file_path => $file_data) { if (isset($file_data['class']) && $file_data['class'] === $current_class) { // Must be a PHP file if (str_ends_with($file_path, '.php')) { $extends = $file_data['extends'] ?? null; if (!$extends) { json_response(['is_subclass' => false]); } // Normalize the extends class name (strip namespace) $extends_parts = explode('\\', $extends); $extends_simple = end($extends_parts); if ($extends_simple == $superclass) { json_response(['is_subclass' => true]); } // Move up the chain $current_class = $extends_simple; $found = true; break; } } } if (!$found) { json_response(['is_subclass' => false]); } } json_response(['is_subclass' => false]); } /** * Handle resolve_url service - resolve URL to controller/method * Takes a URL path and returns the controller and method that handles it */ function handle_resolve_url_service($data) { $url = $data['url'] ?? null; if (!$url) { json_response([ 'error' => 'Missing required parameter: url', 'found' => false, ], 400); } // Load manifest to get routes $manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php'); if (!file_exists($manifest_file)) { error_response('Manifest not found - run php artisan rsx:manifest:build', 500); } $manifest_raw = include $manifest_file; $manifest_data = $manifest_raw['data'] ?? $manifest_raw; // Get routes from manifest $routes = $manifest_data['php']['routes'] ?? []; // Try to find matching route foreach ($routes as $route) { $pattern = $route['pattern'] ?? ''; $controller = $route['class'] ?? ''; $method = $route['method'] ?? ''; // Simple pattern matching - exact match for now // TODO: Support route parameters like /users/:id if ($pattern === $url) { json_response([ 'found' => true, 'controller' => $controller, 'method' => $method, 'pattern' => $pattern, ]); } } // Not found json_response([ 'found' => false, 'url' => $url, ]); }