Files
rspade_system/app/RSpade/Core/Dispatch/ResponseBuilder.php
root 77b4d10af8 Refactor filename naming system and apply convention-based renames
Standardize settings file naming and relocate documentation files
Fix code quality violations from rsx:check
Reorganize user_management directory into logical subdirectories
Move Quill Bundle to core and align with Tom Select pattern
Simplify Site Settings page to focus on core site information
Complete Phase 5: Multi-tenant authentication with login flow and site selection
Add route query parameter rule and synchronize filename validation logic
Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs
Implement filename convention rule and resolve VS Code auto-rename conflict
Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns
Implement RPC server architecture for JavaScript parsing
WIP: Add RPC server infrastructure for JS parsing (partial implementation)
Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation
Add JQHTML-CLASS-01 rule and fix redundant class names
Improve code quality rules and resolve violations
Remove legacy fatal error format in favor of unified 'fatal' error type
Filter internal keys from window.rsxapp output
Update button styling and comprehensive form/modal documentation
Add conditional fly-in animation for modals
Fix non-deterministic bundle compilation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 19:10:02 +00:00

469 lines
14 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\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\RSpade\Core\Dispatch\HandlerFactory;
/**
* ResponseBuilder builds appropriate HTTP responses based on handler results
*
* Supports response types:
* - view: Render Laravel view
* - json: JSON response
* - redirect: HTTP redirect
* - file: File download/stream
* - stream: Streamed response
* - error: Error response
* - empty: Empty response
* - raw: Raw text/html response
*/
class ResponseBuilder
{
/**
* Response type constants
*/
const TYPE_VIEW = 'view';
const TYPE_JSON = 'json';
const TYPE_REDIRECT = 'redirect';
const TYPE_FILE = 'file';
const TYPE_STREAM = 'stream';
const TYPE_ERROR = 'error';
const TYPE_EMPTY = 'empty';
const TYPE_RAW = 'raw';
/**
* Default response headers by type
* @var array
*/
protected static $default_headers = [
self::TYPE_JSON => ['Content-Type' => 'application/json'],
self::TYPE_RAW => ['Content-Type' => 'text/html; charset=UTF-8'],
];
/**
* Build response from handler result
*
* @param mixed $result Handler result
* @param string $handler_type Handler type from HandlerFactory
* @param array $attributes Method attributes
* @return Response
*/
public static function build($result, $handler_type = null, $attributes = [])
{
// If already a Response object, return as-is
if ($result instanceof Response || $result instanceof JsonResponse ||
$result instanceof RedirectResponse || $result instanceof BinaryFileResponse ||
$result instanceof StreamedResponse) {
return $result;
}
// Handle array responses with type hints
if (is_array($result) && isset($result['type'])) {
return static::__build_typed_response($result);
}
// Handle based on handler type
if ($handler_type !== null) {
return static::__build_by_handler_type($result, $handler_type, $attributes);
}
// Default: return as JSON
return static::__build_json_response($result);
}
/**
* Build response from typed result array
*
* @param array $result
* @return Response
*/
protected static function __build_typed_response($result)
{
$type = $result['type'];
switch ($type) {
case self::TYPE_VIEW:
return static::__build_view_response($result);
case self::TYPE_JSON:
return static::__build_json_response(
$result['data'] ?? $result,
$result['status'] ?? 200,
$result['headers'] ?? []
);
case self::TYPE_REDIRECT:
return static::__build_redirect_response($result);
case self::TYPE_FILE:
return static::__build_file_response($result);
case self::TYPE_STREAM:
return static::__build_stream_response($result);
case self::TYPE_ERROR:
return static::__build_error_response($result);
case self::TYPE_EMPTY:
return static::__build_empty_response(
$result['status'] ?? 204,
$result['headers'] ?? []
);
case self::TYPE_RAW:
return static::__build_raw_response($result);
default:
// Unknown type, return as JSON
return static::__build_json_response($result);
}
}
/**
* Build response based on handler type
*
* @param mixed $result
* @param string $handler_type
* @param array $attributes
* @return Response
*/
protected static function __build_by_handler_type($result, $handler_type, $attributes)
{
switch ($handler_type) {
case HandlerFactory::TYPE_CONTROLLER:
// Controllers default to view responses
if (is_array($result) && !isset($result['type'])) {
$result['type'] = self::TYPE_VIEW;
return static::__build_typed_response($result);
}
break;
case HandlerFactory::TYPE_API:
// API handlers default to JSON
if (!is_array($result) || !isset($result['type'])) {
return static::__build_json_response($result);
}
break;
case HandlerFactory::TYPE_FILE:
// File handlers default to file responses
if (is_array($result) && !isset($result['type'])) {
$result['type'] = self::TYPE_FILE;
return static::__build_typed_response($result);
}
break;
}
// If we have a typed response, build it
if (is_array($result) && isset($result['type'])) {
return static::__build_typed_response($result);
}
// Default to JSON
return static::__build_json_response($result);
}
/**
* Build view response
*
* @param array $result
* @return Response
*/
protected static function __build_view_response($result)
{
$view = $result['view'] ?? 'welcome';
$data = $result['data'] ?? [];
$status = $result['status'] ?? 200;
$headers = $result['headers'] ?? [];
// Check if view exists
if (!View::exists($view)) {
// Return error response if view not found
return static::__build_error_response([
'code' => 500,
'message' => "View not found: {$view}"
]);
}
return response()->view($view, $data, $status)->withHeaders($headers);
}
/**
* Build JSON response
*
* @param mixed $data
* @param int $status
* @param array $headers
* @return JsonResponse
*/
protected static function __build_json_response($data, $status = 200, $headers = [])
{
// Merge with default JSON headers
$headers = array_merge(static::$default_headers[self::TYPE_JSON], $headers);
return response()->json($data, $status, $headers);
}
/**
* Build redirect response
*
* @param array $result
* @return Response
*/
protected static function __build_redirect_response($result)
{
$url = $result['url'] ?? '/';
$status = $result['status'] ?? 302;
$headers = $result['headers'] ?? [];
$secure = $result['secure'] ?? null;
// Use Laravel's redirect helper for proper URL generation
$response = redirect($url, $status, $headers, $secure);
return $response;
}
/**
* Build file response
*
* @param array $result
* @return BinaryFileResponse|Response
*/
protected static function __build_file_response($result)
{
$path = $result['path'] ?? null;
if (!$path || !file_exists($path)) {
return static::__build_error_response([
'code' => 404,
'message' => 'File not found'
]);
}
$headers = $result['headers'] ?? [];
// Create file response
$response = response()->file($path, $headers);
// Set download name if provided
if (isset($result['name'])) {
$disposition = $result['disposition'] ?? 'attachment';
$response->headers->set(
'Content-Disposition',
"{$disposition}; filename=\"" . $result['name'] . "\""
);
}
// Set MIME type if provided
if (isset($result['mime'])) {
$response->headers->set('Content-Type', $result['mime']);
}
return $response;
}
/**
* Build stream response
*
* @param array $result
* @return StreamedResponse
*/
protected static function __build_stream_response($result)
{
$callback = $result['callback'] ?? function() { echo ''; };
$status = $result['status'] ?? 200;
$headers = $result['headers'] ?? [];
return response()->stream($callback, $status, $headers);
}
/**
* Build error response
*
* @param array $result
* @return Response
*/
protected static function __build_error_response($result)
{
$code = $result['code'] ?? 500;
$message = $result['message'] ?? 'Server Error';
$headers = $result['headers'] ?? [];
// Use Laravel's abort for proper error handling
abort($code, $message);
}
/**
* Build empty response
*
* @param int $status
* @param array $headers
* @return Response
*/
protected static function __build_empty_response($status = 204, $headers = [])
{
return response('', $status, $headers);
}
/**
* Build raw response
*
* @param array $result
* @return Response
*/
protected static function __build_raw_response($result)
{
$content = $result['content'] ?? '';
$status = $result['status'] ?? 200;
$headers = array_merge(
static::$default_headers[self::TYPE_RAW],
$result['headers'] ?? []
);
return response($content, $status, $headers);
}
/**
* Set CORS headers for response
*
* @param Response $response
* @param array $cors_config
* @return Response
*/
public static function set_cors_headers($response, $cors_config = [])
{
$default_cors = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
'Access-Control-Max-Age' => '86400',
];
$cors = array_merge($default_cors, $cors_config);
foreach ($cors as $header => $value) {
$response->headers->set($header, $value);
}
return $response;
}
/**
* Set cache headers for response
*
* @param Response $response
* @param int $ttl Time to live in seconds
* @param array $options Additional cache options
* @return Response
*/
public static function set_cache_headers($response, $ttl = 3600, $options = [])
{
if ($ttl > 0) {
$response->headers->set('Cache-Control', "public, max-age={$ttl}");
// Set expires header
$expires = gmdate('D, d M Y H:i:s', time() + $ttl) . ' GMT';
$response->headers->set('Expires', $expires);
// Set ETag if provided
if (isset($options['etag'])) {
$response->headers->set('ETag', $options['etag']);
}
// Set Last-Modified if provided
if (isset($options['last_modified'])) {
$response->headers->set('Last-Modified', $options['last_modified']);
}
} else {
// No cache
$response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', '0');
}
return $response;
}
/**
* Apply response attributes
*
* @param Response $response
* @param array $attributes
* @return Response
*/
public static function apply_attributes($response, $attributes)
{
// Apply CORS if specified
if (isset($attributes['cors'])) {
static::set_cors_headers($response, $attributes['cors']);
}
// Apply cache if specified
if (isset($attributes['cache'])) {
$cache_config = $attributes['cache'];
$ttl = $cache_config['ttl'] ?? 3600;
static::set_cache_headers($response, $ttl, $cache_config);
}
// Apply custom headers
if (isset($attributes['headers'])) {
foreach ($attributes['headers'] as $header => $value) {
$response->headers->set($header, $value);
}
}
return $response;
}
/**
* Build response for asset file
*
* @param string $path
* @param string $base_path
* @return Response
*/
public static function build_asset_response($path, $base_path = null)
{
// Resolve full path
if ($base_path) {
$full_path = rtrim($base_path, '/') . '/' . ltrim($path, '/');
} else {
$full_path = public_path($path);
}
if (!file_exists($full_path)) {
return static::__build_error_response([
'code' => 404,
'message' => 'Asset not found'
]);
}
// Determine MIME type
$mime = mime_content_type($full_path);
// Build response
$response = response()->file($full_path, [
'Content-Type' => $mime
]);
// Set cache headers for assets (1 year for production)
if (app()->environment('production')) {
static::set_cache_headers($response, 31536000); // 1 year
} else {
static::set_cache_headers($response, 3600); // 1 hour for dev
}
return $response;
}
}