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>
469 lines
14 KiB
PHP
Executable File
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;
|
|
}
|
|
} |