Files
rspade_system/app/RSpade/Core/Ajax/Ajax.php
root f6ac36c632 Enhance refactor commands with controller-aware Route() updates and fix code quality violations
Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 06:21:56 +00:00

408 lines
15 KiB
PHP
Executable File

<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Ajax;
use Exception;
use Illuminate\Http\Request;
use App\RSpade\Core\Ajax\Exceptions\AjaxAuthRequiredException;
use App\RSpade\Core\Ajax\Exceptions\AjaxFatalErrorException;
use App\RSpade\Core\Ajax\Exceptions\AjaxFormErrorException;
use App\RSpade\Core\Ajax\Exceptions\AjaxUnauthorizedException;
use App\RSpade\Core\Manifest\Manifest;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
// @FILE-SUBCLASS-01-EXCEPTION
/**
* Ajax - Unified AJAX handling system
*
* Consolidates all AJAX-related functionality:
* - Internal server-side API calls (internal method)
* - Browser AJAX request handling (handle_browser_request method)
* - Response mode management for error handlers
* - Future: External HTTP calls, WebSocket handling
*/
class Ajax
{
/**
* Flag to indicate AJAX response mode for error handlers
*/
protected static bool $ajax_response_mode = false;
/**
* Call an internal API method directly from PHP code
*
* Used for server-side code to invoke API methods without HTTP overhead.
* This is useful for internal service calls, background jobs, and testing.
*
* @param string $rsx_controller Controller name (e.g., 'User_Controller')
* @param string $rsx_action Action/method name (e.g., 'get_profile')
* @param array $params Parameters to pass to the method
* @param array $auth Authentication context (not yet implemented)
* @return mixed The filtered response from the API method
* @throws AjaxAuthRequiredException
* @throws AjaxUnauthorizedException
* @throws AjaxFormErrorException
* @throws AjaxFatalErrorException
* @throws Exception
*/
public static function internal($rsx_controller, $rsx_action, $params = [], $auth = [])
{
// Get manifest to find controller
$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 === $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 'response_fatal_error':
$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 === 'response_fatal_error') {
$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;
}
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;
}
/**
* 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
{
$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;
}
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);
}
}