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>
15 KiB
Executable File
RSX Dispatch System Documentation
Overview
The RSX Dispatch System is the core request handling mechanism that processes HTTP requests through an attribute-driven, extensible pipeline. It replaces traditional Laravel routing with a more flexible, discoverable system based on PHP 8 attributes.
Architecture
Core Components
Request → Dispatcher → AssetHandler
→ RouteResolver
→ AttributeProcessor
→ HandlerFactory
→ Response
Dispatcher
The central hub that coordinates request processing.
Key Responsibilities
- Request Routing - Finds matching routes from the manifest
- Asset Serving - Delegates static file requests to AssetHandler
- Attribute Processing - Coordinates before/after attribute processors
- Response Building - Converts handler results to HTTP responses
Usage
// In RsxServiceProvider
$dispatcher = new Dispatcher(
$manifest,
$route_resolver,
$attribute_processor,
$asset_handler,
$api_handler
);
// Handle request
$response = $dispatcher->dispatch($url, $method, $params);
Handler Priority
The dispatcher processes handlers in this order:
- Assets (Priority: 10) - Static files from
/rsx/*/public/ - API (Priority: 20) - API endpoints
- Controllers (Priority: 30) - Web controllers
- Files (Priority: 40) - File download endpoints
Route Resolution
Route Patterns
Routes support various pattern types:
#[Route('/users')] // Static route
#[Route('/users/:id')] // Required parameter
#[Route('/users/:id?')] // Optional parameter
#[Route('/posts/:category/:slug')] // Multiple parameters
#[Route('/files/*')] // Wildcard (captures rest of URL)
Route Matching
Routes are matched based on specificity:
- Exact static matches
- Routes with fewer parameters
- Routes with required parameters before optional
- Wildcard routes last
Parameter Extraction
Routes use :param syntax for parameters:
// Route: /users/:id/posts/:post_id?
// URL: /users/123/posts/456?sort=date
$params = [
'id' => '123', // URL route parameter
'post_id' => '456', // URL route parameter
'sort' => 'date', // GET parameter
'_route' => '/users/:id/posts/:post_id?',
'_method' => 'GET'
];
Parameter Priority
Parameters are merged with the following priority order (earlier takes precedence):
- URL Route Parameters - Values extracted from the route pattern (e.g.,
:id) - GET Parameters - Query string parameters
- POST Parameters - Request body parameters
If the same parameter name exists in multiple sources, the higher priority value is used:
// Route: /users/:id
// URL: /users/123?id=456
// POST body: id=789
// Result: $params['id'] = '123' (URL route parameter wins)
Attribute System
Available Attributes
Route Attribute
Defines HTTP endpoints:
use App\RSpade\Core\Attributes\Route;
class UserController extends Rsx_Controller
{
#[Route('/users', methods: ['GET', 'POST'])]
public function index($params) { }
#[Route('/users/:id', methods: ['GET'], name: 'user.show')]
public function show($params) { }
}
Cache Attribute
Caches responses:
use App\RSpade\Core\Attributes\Cache;
#[Cache(ttl: 3600, key: 'user-list', tags: ['users'])]
public function list_users($params)
{
// Expensive operation cached for 1 hour
return User::with('posts')->get();
}
RateLimit Attribute
Throttles requests:
use App\RSpade\Core\Attributes\RateLimit;
#[RateLimit(
max_attempts: 60,
decay_minutes: 1,
key: 'ip' // or 'user', 'api_key'
)]
public function api_endpoint($params) { }
Middleware Attribute
Applies Laravel middleware:
use App\RSpade\Core\Attributes\Middleware;
#[Middleware(['auth', 'verified'])]
public function protected_action($params) { }
#[Middleware(['auth'], except: ['index', 'show'])]
class PostController { }
Cors Attribute
Configures Cross-Origin Resource Sharing:
use App\RSpade\Core\Attributes\Cors;
#[Cors(
allowed_origins: ['https://app.example.com', 'https://*.example.com'],
allowed_methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowed_headers: ['Content-Type', 'Authorization'],
exposed_headers: ['X-Total-Count'],
max_age: 3600,
allow_credentials: true
)]
class ApiController { }
ApiVersion Attribute
Manages API versioning:
use App\RSpade\Core\Attributes\ApiVersion;
#[ApiVersion(
version: 'v2',
deprecated: true,
deprecation_message: 'Use /api/v3/users instead',
sunset_date: '2025-01-01'
)]
public function legacy_endpoint($params) { }
Attribute Processing Pipeline
Attributes are processed in a specific order based on processor priority:
- RateLimit (Priority: 90) - Blocks excessive requests early
- Middleware (Priority: 80) - Authentication/authorization
- Cache (Priority: 70) - Returns cached responses
- Cors (Priority: 60) - Adds CORS headers
Creating Custom Attributes
- Create the attribute class:
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class Audit
{
public function __construct(
public string $action,
public bool $log_params = false
) {}
}
- Create the processor:
namespace App\Processors;
use App\RSpade\Core\Dispatch\Processors\ProcessorInterface;
class AuditProcessor implements ProcessorInterface
{
public function can_handle($attribute): bool
{
return $attribute instanceof Audit;
}
public function process_before($attribute, Request $request, array &$context)
{
Log::info('Audit: ' . $attribute->action, [
'user' => $request->user()?->id,
'params' => $attribute->log_params ? $request->all() : null
]);
}
public function process_after($attribute, Response $response, array $context): ?Response
{
// Log response if needed
return $response;
}
public function get_priority(): int
{
return 50;
}
}
- Register the processor:
// In a service provider
$attribute_processor->register_processor(new AuditProcessor());
API Handler
JSON-Only Responses
The API handler enforces JSON-only responses (opinionated design):
// Always returns JSON, regardless of Accept header
#[Route('/api/users', methods: ['GET'])]
public function get_users($params)
{
return ['users' => User::all()]; // Automatically converted to JSON
}
API Parameters
The API handler automatically extracts common parameters:
public function list_items($params)
{
// Pagination
$page = $params['_page'] ?? 1; // ?page=2
$per_page = $params['_per_page'] ?? 25; // ?per_page=50
// Sorting
$sort = $params['_sort'] ?? 'id'; // ?sort=name
$order = $params['_order'] ?? 'asc'; // ?order=desc
// Searching
$search = $params['_search'] ?? null; // ?q=term or ?search=term
// Field filtering
$fields = $params['_fields'] ?? null; // ?fields=id,name,email
// API key
$api_key = $params['_api_key'] ?? null; // ?api_key=secret or X-API-Key header
}
Internal API Execution (Future)
// Call API methods internally without HTTP overhead
$result = $api_handler->execute_internal(
'UserApi.get_profile',
['user_id' => 123]
);
// Returns PHP array/object, not JSON response
Asset Handler
Static File Serving
Automatically serves files from /rsx/*/public/ directories:
/rsx/shop/public/logo.png → Accessible at /rsx/shop/public/logo.png
/rsx/blog/public/styles.css → Accessible at /rsx/blog/public/styles.css
Security Features
- Path traversal protection
- MIME type detection
- Cache headers (1 day default)
- Only serves from designated public directories
Usage
Files are automatically served without configuration:
<!-- In your views -->
<img src="/rsx/shop/public/logo.png" alt="Shop Logo">
<link rel="stylesheet" href="/rsx/theme/public/styles.css">
Error Handling
Built-in Error Responses
The dispatcher handles common errors:
// 404 Not Found
if (!$route_match) {
return $this->handle_not_found($url, $method);
}
// 405 Method Not Allowed
if (!in_array($method, $route['methods'])) {
return $this->handle_method_not_allowed($method, $route['methods']);
}
// 500 Internal Server Error
try {
// ... dispatch logic
} catch (Throwable $e) {
return $this->handle_exception($e);
}
Custom Error Handling
Override error methods in a custom dispatcher:
class CustomDispatcher extends Dispatcher
{
protected function handle_not_found($url, $method)
{
return response()->json([
'error' => 'Endpoint not found',
'url' => $url,
'method' => $method
], 404);
}
}
Performance Optimization
Caching Strategy
- Memory Cache - In-process array cache for current request
- File Cache - Persistent cache using Laravel's cache system
- Route Cache - Pre-compiled routes for production
Cache Invalidation
// Clear specific cache tags
Cache::tags(['users'])->flush();
// Clear all RSX caches
php artisan rsx:manifest:clear
php artisan rsx:routes:clear
Production Optimization
# Build and cache for production
php artisan rsx:manifest:build
php artisan rsx:routes:cache
php artisan config:cache
Testing
Unit Testing Dispatching
use Tests\TestCase;
use App\RSpade\Core\Dispatch\Dispatcher;
class DispatcherTest extends TestCase
{
public function test_routes_to_correct_handler()
{
$dispatcher = $this->create_test_dispatcher();
$response = $dispatcher->dispatch('/users', 'GET');
$this->assertEquals(200, $response->getStatusCode());
$this->assertJson($response->getContent());
}
public function test_applies_rate_limiting()
{
$dispatcher = $this->create_test_dispatcher();
// Make requests up to limit
for ($i = 0; $i < 60; $i++) {
$dispatcher->dispatch('/api/limited', 'GET');
}
// Next request should fail
$this->expectException(TooManyRequestsHttpException::class);
$dispatcher->dispatch('/api/limited', 'GET');
}
}
Integration Testing
class ApiIntegrationTest extends TestCase
{
public function test_complete_api_flow()
{
$response = $this->get('/api/users', [
'Accept' => 'application/xml' // Should still return JSON
]);
$response->assertStatus(200)
->assertHeader('Content-Type', 'application/json')
->assertJsonStructure(['users']);
}
}
Debugging
Debug Mode
Enable detailed logging:
// In .env
RSX_DEBUG=true
RSX_LOG_DISPATCH=true
Viewing Routes
# List all discovered routes
php artisan rsx:routes:list
# Search for specific route
php artisan rsx:routes:list | grep "users"
# Show route details
php artisan rsx:routes:show /users/:id
Tracing Requests
// In Dispatcher
Log::debug('Dispatch', [
'url' => $url,
'method' => $method,
'route' => $route_match,
'attributes' => $attributes,
'params' => $params
]);
Migration Guide
From Laravel Routes
Before (Laravel):
// routes/web.php
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
Route::get('/users/:id', [UserController::class, 'show']);
After (RSX):
// /rsx/controllers/UserController.php
class UserController extends Rsx_Controller
{
#[Route('/users', methods: ['GET'])]
public function index($params) { }
#[Route('/users', methods: ['POST'])]
public function store($params) { }
#[Route('/users/:id', methods: ['GET'])]
public function show($params) { }
}
From Laravel Middleware
Before:
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
After:
#[Middleware(['auth', 'verified'])]
class DashboardController extends Rsx_Controller
{
#[Route('/dashboard', methods: ['GET'])]
public function index($params) { }
}
Best Practices
1. Attribute Organization
Place related attributes together:
#[Route('/api/users', methods: ['GET'])]
#[Cache(ttl: 300, tags: ['users'])]
#[RateLimit(max_attempts: 100)]
#[Middleware(['auth:api'])]
public function list_users($params) { }
2. Parameter Validation
Validate parameters early:
public function get_user($params)
{
$user_id = $params['id'] ?? null;
if (!$user_id || !is_numeric($user_id)) {
return response()->json(['error' => 'Invalid user ID'], 400);
}
// Process request...
}
3. Error Handling
Use consistent error responses:
public function process_order($params)
{
try {
$result = $this->order_service->process($params);
return ['success' => true, 'order' => $result];
} catch (ValidationException $e) {
return response()->json(['error' => $e->getMessage()], 422);
} catch (Exception $e) {
Log::error('Order processing failed', ['error' => $e]);
return response()->json(['error' => 'Processing failed'], 500);
}
}
4. Cache Strategy
Use appropriate cache TTLs and tags:
#[Cache(ttl: 3600, tags: ['products', 'catalog'])] // 1 hour, tagged
public function get_product_catalog($params) { }
#[Cache(ttl: 60, key: 'hot-deals')] // 1 minute for frequently changing
public function get_hot_deals($params) { }
#[Cache(ttl: 86400, tags: ['static'])] // 1 day for static content
public function get_terms_of_service($params) { }
5. Rate Limiting
Apply appropriate limits:
// Public endpoints - strict limits
#[RateLimit(max_attempts: 10, decay_minutes: 1)]
public function public_search($params) { }
// Authenticated - higher limits
#[RateLimit(max_attempts: 100, decay_minutes: 1, key: 'user')]
public function user_search($params) { }
// Internal APIs - very high limits
#[RateLimit(max_attempts: 1000, decay_minutes: 1, key: 'api_key')]
public function internal_api($params) { }
Summary
The RSX Dispatch System provides:
- Attribute-driven routing without route files
- Automatic discovery of controllers and routes
- Extensible processing through attribute processors
- Built-in features like caching, rate limiting, CORS
- Static asset serving without configuration
- JSON-only APIs for consistency
- Performance optimization through caching
- Easy testing with comprehensive test helpers
It replaces traditional Laravel routing with a more flexible, discoverable system that keeps route definitions with the code they control.