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>
408 lines
15 KiB
PHP
Executable File
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);
|
|
}
|
|
}
|