Files
rspade_system/app/RSpade/Core/Ajax/Ajax.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

373 lines
13 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");
}
// 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::$action_name($request, $params);
}
// Handle special response types
if ($response instanceof \App\RSpade\Core\Response\Rsx_Response_Abstract) {
return static::_handle_browser_special_response($response);
}
// Wrap normal response in success JSON
$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 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;
}
/**
* 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);
}
}