Files
rspade_system/docs/rsx-dispatch-system.md
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
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>
2025-10-21 02:08:33 +00:00

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

  1. Request Routing - Finds matching routes from the manifest
  2. Asset Serving - Delegates static file requests to AssetHandler
  3. Attribute Processing - Coordinates before/after attribute processors
  4. 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:

  1. Assets (Priority: 10) - Static files from /rsx/*/public/
  2. API (Priority: 20) - API endpoints
  3. Controllers (Priority: 30) - Web controllers
  4. 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:

  1. Exact static matches
  2. Routes with fewer parameters
  3. Routes with required parameters before optional
  4. 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):

  1. URL Route Parameters - Values extracted from the route pattern (e.g., :id)
  2. GET Parameters - Query string parameters
  3. 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:

  1. RateLimit (Priority: 90) - Blocks excessive requests early
  2. Middleware (Priority: 80) - Authentication/authorization
  3. Cache (Priority: 70) - Returns cached responses
  4. Cors (Priority: 60) - Adds CORS headers

Creating Custom Attributes

  1. 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
    ) {}
}
  1. 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;
    }
}
  1. 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

  1. Memory Cache - In-process array cache for current request
  2. File Cache - Persistent cache using Laravel's cache system
  3. 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.