Files
rspade_system/app/RSpade/Core/Dispatch/ResponseBuilder.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

469 lines
14 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\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;
}
}