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>
352 lines
11 KiB
PHP
Executable File
352 lines
11 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\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;
|
|
}
|
|
|
|
|
|
|
|
} |