$info) { // Skip non-PHP files or files without classes if (!isset($info['class']) || !isset($info['fqcn'])) { continue; } // Check if class name matches exactly (without namespace) $class_basename = basename(str_replace('\\', '/', $info['fqcn'])); if ($class_basename === $rsx_controller) { $controller_class = $info['fqcn']; $file_info = $info; break; } } if (!$controller_class) { throw new Exception("Controller class not found: {$rsx_controller}"); } // Check if class exists if (!class_exists($controller_class)) { throw new Exception("Controller class does not exist: {$controller_class}"); } // Check if it's a subclass of Rsx_Controller_Abstract if (!Manifest::php_is_subclass_of($controller_class, \App\RSpade\Core\Controller\Rsx_Controller_Abstract::class)) { throw new Exception("Controller {$controller_class} must extend Rsx_Controller_Abstract"); } // Check if method exists and has Ajax_Endpoint attribute if (!isset($file_info['public_static_methods'][$rsx_action])) { throw new Exception("Method {$rsx_action} not found in controller {$controller_class}"); } $method_info = $file_info['public_static_methods'][$rsx_action]; $has_ajax_endpoint = false; // Check for Ajax_Endpoint attribute in method metadata if (isset($method_info['attributes'])) { foreach ($method_info['attributes'] as $attr_name => $attr_data) { // Check for Ajax_Endpoint with or without namespace if ($attr_name === 'Ajax_Endpoint' || basename(str_replace('\\', '/', $attr_name)) === 'Ajax_Endpoint') { $has_ajax_endpoint = true; break; } } } if (!$has_ajax_endpoint) { throw new Exception("Method {$rsx_action} in {$controller_class} must have Ajax_Endpoint annotation"); } // Create a request object with the params $request = Request::create('/_ajax/' . $rsx_controller . '/' . $rsx_action, 'POST', $params); $request->setMethod('POST'); // Call pre_dispatch if it exists $response = null; if (method_exists($controller_class, 'pre_dispatch')) { $response = $controller_class::pre_dispatch($request, $params); } // If pre_dispatch returned something, use that as response if ($response === null) { // Call the actual method $response = $controller_class::$rsx_action($request, $params); } // Handle special response types if ($response instanceof Rsx_Response_Abstract) { return static::_handle_special_response($response); } // For normal responses, filter through JSON encode/decode to remove PHP objects // This ensures we only return plain data structures $filtered_response = json_decode(json_encode($response), true); return $filtered_response; } /** * Handle special response types by throwing appropriate exceptions * * @param Rsx_Response_Abstract $response * @throws AjaxAuthRequiredException * @throws AjaxUnauthorizedException * @throws AjaxFormErrorException * @throws AjaxFatalErrorException */ protected static function _handle_special_response(Rsx_Response_Abstract $response) { $type = $response->get_type(); $reason = $response->get_reason(); $details = $response->get_details(); switch ($type) { case 'response_auth_required': throw new AjaxAuthRequiredException($reason); case 'response_unauthorized': throw new AjaxUnauthorizedException($reason); case 'response_form_error': throw new AjaxFormErrorException($reason, $details); case 'fatal': $message = $reason; if (!empty($details)) { $message .= ' - ' . json_encode($details); } throw new AjaxFatalErrorException($message); default: throw new Exception("Unknown RSX response type: {$type}"); } } /** * Handle incoming AJAX requests from the browser * * This method processes /_ajax/:controller/:action requests and returns JSON responses. * It validates Ajax_Endpoint annotations and handles special response types. * * @param Request $request The incoming HTTP request * @param array $params Route parameters including controller and action * @return \Illuminate\Http\JsonResponse * @throws Exception */ public static function handle_browser_request(Request $request, array $params = []) { // Enable AJAX response mode for error handlers static::set_ajax_response_mode(true); // Disable console debug HTML output for AJAX requests \App\RSpade\Core\Debug\Debugger::disable_console_html_output(); // Get controller and action from params $controller_name = $params['controller'] ?? null; $action_name = $params['action'] ?? null; if (!$controller_name || !$action_name) { throw new Exception('Missing controller or action parameter'); } // Use manifest to find the controller class $manifest = \App\RSpade\Core\Manifest\Manifest::get_all(); $controller_class = null; $file_info = null; // Search for controller class in manifest foreach ($manifest as $file_path => $info) { // Skip non-PHP files or files without classes if (!isset($info['class']) || !isset($info['fqcn'])) { continue; } // Check if class name matches exactly (without namespace) $class_basename = basename(str_replace('\\', '/', $info['fqcn'])); if ($class_basename === $controller_name) { $controller_class = $info['fqcn']; $file_info = $info; break; } } if (!$controller_class) { throw new Exception("Controller class not found: {$controller_name}"); } // Check if class exists if (!class_exists($controller_class)) { throw new Exception("Controller class does not exist: {$controller_class}"); } // Check if it's a subclass of Rsx_Controller_Abstract if (!Manifest::php_is_subclass_of($controller_class, \App\RSpade\Core\Controller\Rsx_Controller_Abstract::class)) { throw new Exception("Controller {$controller_class} must extend Rsx_Controller_Abstract"); } // Check if method exists and has Ajax_Endpoint attribute if (!isset($file_info['public_static_methods'][$action_name])) { throw new Exception("Method {$action_name} not found in controller {$controller_class} public static methods"); } $method_info = $file_info['public_static_methods'][$action_name]; $has_ajax_endpoint = false; // Check for Ajax_Endpoint attribute in method metadata if (isset($method_info['attributes'])) { foreach ($method_info['attributes'] as $attr_name => $attr_data) { // Check for Ajax_Endpoint with or without namespace if ($attr_name === 'Ajax_Endpoint' || basename(str_replace('\\', '/', $attr_name)) === 'Ajax_Endpoint') { $has_ajax_endpoint = true; break; } } } if (!$has_ajax_endpoint) { throw new Exception("Method {$action_name} in {$controller_class} must have Ajax_Endpoint annotation"); } // Extract params from request body $ajax_params = []; // First check for JSON-encoded params field (old format) $params_json = $request->input('params'); if ($params_json) { $ajax_params = json_decode($params_json, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON in params: ' . json_last_error_msg()); } } else { // Get all request input (form-urlencoded or JSON body) $all_input = $request->all(); // Remove route parameters (controller, action, etc) $ajax_params = array_diff_key($all_input, array_flip(['controller', 'action', '_method', '_route', '_handler'])); } // Call pre_dispatch if it exists $response = null; if (method_exists($controller_class, 'pre_dispatch')) { $response = $controller_class::pre_dispatch($request, $ajax_params); } // If pre_dispatch returned something, use that as response if ($response === null) { // Call the actual method $response = $controller_class::$action_name($request, $ajax_params); } // Handle special response types if ($response instanceof \App\RSpade\Core\Response\Rsx_Response_Abstract) { return static::_handle_browser_special_response($response); } // Wrap response using shared formatting method $json_response = static::format_ajax_response($response); return response()->json($json_response); } /** * Handle special response types for browser AJAX requests * * @param \App\RSpade\Core\Response\Rsx_Response_Abstract $response * @return \Illuminate\Http\JsonResponse */ protected static function _handle_browser_special_response(\App\RSpade\Core\Response\Rsx_Response_Abstract $response) { $type = $response->get_type(); // Handle fatal error - always throw exception if ($type === 'fatal') { $details = $response->get_details(); $message = $response->get_reason(); if (!empty($details)) { $message .= ' - ' . json_encode($details); } throw new Exception($message); } // Build error response based on type $json_response = [ '_success' => false, 'error_type' => $type, 'reason' => $response->get_reason(), ]; // Add details for form errors if ($type === 'response_form_error') { $json_response['details'] = $response->get_details(); } // Add console debug messages if any $console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages(); if (!empty($console_messages)) { // Messages are now structured as [channel, [arguments]] $json_response['console_debug'] = $console_messages; } // Add flash_alerts if any pending for current session $flash_messages = \App\RSpade\Lib\Flash\Flash_Alert::get_pending_messages(); if (!empty($flash_messages)) { $json_response['flash_alerts'] = $flash_messages; } return response()->json($json_response); } /** * Set AJAX response mode for error handlers * * When enabled, error handlers will return JSON instead of HTML * * @param bool $enabled * @return void */ public static function set_ajax_response_mode(bool $enabled): void { static::$ajax_response_mode = $enabled; } /** * Check if AJAX response mode is enabled * * @return bool */ public static function is_ajax_response_mode(): bool { return static::$ajax_response_mode; } /** * Validate Ajax endpoint return value * * Ensures developers don't manually add 'success' field to their responses. * The framework automatically wraps responses with success/failure metadata. * * @param mixed $response The Ajax method return value * @throws Exception If response contains manual success field */ protected static function _validate_ajax_response($response): void { // Only validate if response is an array if (!is_array($response)) { return; } // Check if response contains '_success' or 'success' key with boolean value if ((array_key_exists('_success', $response) && is_bool($response['_success'])) || (array_key_exists('success', $response) && is_bool($response['success']))) { $wrong_way = "return ['_success' => false, 'message' => 'Error'];"; $right_way_validation = "return response_form_error('Validation failed', ['email' => 'Invalid']);"; $right_way_success = "return ['user_id' => 123, 'data' => [...]];\n// Framework wraps: {_success: true, _ajax_return_value: {...}}"; $right_way_exception = "// Let exceptions bubble - framework handles them\n\$user->save(); // Don't wrap in try/catch"; throw new Exception( "YOU'RE DOING IT WRONG: Ajax endpoints must not return arrays with '_success' or 'success' boolean key.\n\n" . "The '_success' field is reserved for framework-level Ajax response wrapping.\n\n" . "WRONG WAY:\n" . " {$wrong_way}\n\n" . "RIGHT WAY (validation errors):\n" . " {$right_way_validation}\n\n" . "RIGHT WAY (success):\n" . " {$right_way_success}\n\n" . "RIGHT WAY (exceptions):\n" . " {$right_way_exception}\n\n" . "See: php artisan rsx:man ajax_error_handling" ); } } /** * Format Ajax response with standard wrapper structure * * This method creates the standardized response format used by both * HTTP Ajax endpoints and CLI Ajax commands. Ensures consistency across * all Ajax response types. * * @param mixed $response The Ajax method return value * @return array Formatted response array with success, _ajax_return_value, and optional console_debug */ public static function format_ajax_response($response): array { // Validate response before formatting static::_validate_ajax_response($response); $json_response = [ '_success' => true, '_ajax_return_value' => $response, ]; // Add console debug messages if any $console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages(); if (!empty($console_messages)) { // Messages are now structured as [channel, [arguments]] $json_response['console_debug'] = $console_messages; } // Add flash_alerts if any pending for current session $flash_messages = \App\RSpade\Lib\Flash\Flash_Alert::get_pending_messages(); if (!empty($flash_messages)) { $json_response['flash_alerts'] = $flash_messages; } return $json_response; } /** * Backward compatibility alias for internal() * @deprecated Use internal() instead */ public static function call($rsx_controller, $rsx_action, $params = [], $auth = []) { return static::internal($rsx_controller, $rsx_action, $params, $auth); } }