Files
rspade_system/app/RSpade/Core/Dispatch/ApiHandler.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

352 lines
11 KiB
PHP

<?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\Dispatch;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\RSpade\Core\Manifest\Manifest;
/**
* ApiHandler manages API request processing for RSX dispatch system
*
* OPINIONATED API DESIGN PRINCIPLES:
* ===================================
* 1. ALL external API requests return JSON with application/json content-type
* 2. NO content negotiation - APIs always return JSON, period
* 3. Internal API calls return PHP objects/arrays directly
* 4. This is non-negotiable framework behavior
*
* INTERNAL API EXECUTION ARCHITECTURE (Future Implementation):
* =============================================================
*
* This handler will support internal API execution, allowing PHP code to call
* API endpoints directly without HTTP requests. The architecture will work as follows:
*
* 1. INVOCATION METHOD:
* API::execute("ControllerClass.method", $params)
* - Directly calls API controller methods
* - Returns PHP objects/arrays instead of JSON
* - Uses same code path as external requests
*
* 2. SESSION STACK MANAGEMENT:
* When an internal API call is made:
* a. Current user session is preserved on a stack
* b. New session context is created (starts unauthenticated)
* c. API executes with fresh session scope
* d. Session is popped and original restored after completion
*
* 3. AUTHENTICATION FLOW:
* Internal API calls authenticate via:
* - Explicit API token/key in parameters
* - Inherited from current session (if no token provided)
* - Each nested call maintains its own auth context
*
* 4. NESTED CALL SUPPORT:
* - API A can call API B which can call API C
* - Each maintains separate session scope
* - Stack ensures proper restoration on unwind
* - Prevents session pollution between calls
*
* 5. IMPLEMENTATION BENEFITS:
* - Reuse API logic for internal operations
* - Consistent behavior internal/external
* - Proper session isolation
* - Service-oriented architecture in monolith
*
* EXAMPLE USAGE (Future):
* ```php
* // Internal call with current user context
* $result = API::execute('UserApi.get_profile', ['user_id' => 123]);
*
* // Internal call with specific API key
* $result = API::execute('DataApi.export', [
* '_api_key' => 'secret_key',
* 'format' => 'csv'
* ]);
*
* // Nested internal calls work automatically
* // UserApi.get_full_profile might internally call:
* // - PostApi.get_user_posts
* // - CommentApi.get_user_comments
* // Each with proper session isolation
* ```
*
* TODO: Implement session stack when authentication system is ready
* TODO: Add API token/key authentication mechanism
* TODO: Create API::execute() static helper method
* TODO: Add request/response interceptors for internal calls
* TODO: Implement circuit breaker for recursive call protection
*/
class ApiHandler
{
/**
* API version header name
*
* @var string
*/
protected static $version_header = 'X-API-Version';
/**
* Handle API request
*
* @param mixed $handler The handler (controller/method)
* @param Request $request
* @param array $params
* @param array $attributes Route attributes
* @return \Illuminate\Http\Response
*/
public static function handle($handler, Request $request, array $params = [], array $attributes = [])
{
// Add API-specific parameters
$params = static::__prepare_api_params($params, $request);
// Check API version if specified
$version = static::__get_api_version($request, $attributes);
if ($version) {
$params['_api_version'] = $version;
}
// Execute the handler
$result = static::__execute_handler($handler, $request, $params);
// ALWAYS return JSON for external API requests
return static::__create_json_response($result, $request);
}
/**
* Execute API handler internally (for future internal API calls)
*
* @param string $endpoint Format: "ControllerClass.method"
* @param array $params
* @param array $options
* @return mixed Raw PHP response (not JSON)
*/
public static function execute_internal($endpoint, array $params = [], array $options = [])
{
// Parse endpoint
list($class, $method) = static::__parse_endpoint($endpoint);
// TODO: Push new session context onto stack
// $this->push_session_context($options);
try {
// Build handler callable
if (!class_exists($class)) {
throw new \InvalidArgumentException("API class not found: {$class}");
}
$handler = [$class, $method];
if (!is_callable($handler)) {
throw new \InvalidArgumentException("API method not callable: {$endpoint}");
}
// Add internal execution flag
$params['_internal_call'] = true;
$params['_caller_session'] = $options['session'] ?? null;
// Execute handler directly
$result = call_user_func($handler, $params);
// For internal calls, return raw data (not JSON response)
if ($result instanceof JsonResponse) {
return json_decode($result->getContent(), true);
}
return $result;
} finally {
// TODO: Pop session context from stack
// $this->pop_session_context();
}
}
/**
* Parse endpoint string into class and method
*
* @param string $endpoint
* @return array [class, method]
*/
protected static function __parse_endpoint($endpoint)
{
$parts = explode('.', $endpoint);
if (count($parts) !== 2) {
throw new \InvalidArgumentException("Invalid endpoint format. Expected: ControllerClass.method");
}
$class = $parts[0];
$method = $parts[1];
// Add namespace if not present
if (!str_contains($class, '\\')) {
// Check common API namespaces using Manifest
$namespaces = [
'App\\Http\\Controllers\\Api\\',
'App\\RSpade\\Api\\',
'App\\Api\\'
];
$found = false;
foreach ($namespaces as $namespace) {
try {
$full_class = $namespace . $class;
$metadata = Manifest::php_get_metadata_by_fqcn($full_class);
$class = $metadata['fqcn'];
$found = true;
break;
} catch (\RuntimeException $e) {
// Try next namespace
}
}
if (!$found) {
// Try without namespace prefix
try {
$metadata = Manifest::php_get_metadata_by_class($class);
$class = $metadata['fqcn'];
} catch (\RuntimeException $e) {
// Class will be used as-is, may fail later
}
}
}
return [$class, $method];
}
/**
* Execute the API handler
*
* @param mixed $handler
* @param Request $request
* @param array $params
* @return mixed
*/
protected static function __execute_handler($handler, Request $request, array $params)
{
// Handle different handler types
if (is_string($handler) && str_contains($handler, '@')) {
// Laravel style "Controller@method"
list($class, $method) = explode('@', $handler);
$handler = [new $class, $method];
}
if (!is_callable($handler)) {
throw new \RuntimeException("API handler is not callable");
}
// Call handler with request and params
$result = call_user_func($handler, $request, $params);
// Ensure we have a result
if ($result === null) {
$result = ['success' => true];
}
return $result;
}
/**
* Create JSON response for API result
*
* @param mixed $data
* @param Request $request
* @return JsonResponse
*/
protected static function __create_json_response($data, Request $request)
{
$response = new JsonResponse($data);
// Pretty print in development for debugging
if (app()->environment('local', 'development')) {
$response->setEncodingOptions(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
// Always set JSON content type explicitly
$response->headers->set('Content-Type', 'application/json');
return $response;
}
/**
* Prepare API-specific parameters
*
* @param array $params
* @param Request $request
* @return array
*/
protected static function __prepare_api_params(array $params, Request $request)
{
// Add pagination parameters
if ($request->has('page')) {
$params['_page'] = (int) $request->get('page', 1);
$params['_per_page'] = (int) $request->get('per_page', 25);
}
// Add sorting parameters
if ($request->has('sort')) {
$params['_sort'] = $request->get('sort');
$params['_order'] = $request->get('order', 'asc');
}
// Add field filtering
if ($request->has('fields')) {
$params['_fields'] = explode(',', $request->get('fields'));
}
// Add search parameter
if ($request->has('q') || $request->has('search')) {
$params['_search'] = $request->get('q') ?: $request->get('search');
}
// Add API key if present
if ($request->has('api_key')) {
$params['_api_key'] = $request->get('api_key');
} elseif ($request->header('X-API-Key')) {
$params['_api_key'] = $request->header('X-API-Key');
}
return $params;
}
/**
* Get API version from request or attributes
*
* @param Request $request
* @param array $attributes
* @return string|null
*/
protected static function __get_api_version(Request $request, array $attributes)
{
// Check route attribute
foreach ($attributes as $attr) {
if ($attr instanceof \App\RSpade\Core\Attributes\ApiVersion) {
return $attr->version;
}
}
// Check header
if ($request->hasHeader(static::$version_header)) {
return $request->header(static::$version_header);
}
// Check URL path (e.g., /api/v2/...)
$path = $request->path();
if (preg_match('#/v(\d+)/#', $path, $matches)) {
return $matches[1];
}
return null;
}
}