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>
669 lines
15 KiB
Markdown
Executable File
669 lines
15 KiB
Markdown
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
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
#[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:
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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
|
|
) {}
|
|
}
|
|
```
|
|
|
|
2. Create the processor:
|
|
|
|
```php
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
3. Register the processor:
|
|
|
|
```php
|
|
// In a service provider
|
|
$attribute_processor->register_processor(new AuditProcessor());
|
|
```
|
|
|
|
## API Handler
|
|
|
|
### JSON-Only Responses
|
|
|
|
The API handler enforces JSON-only responses (opinionated design):
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
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)
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```html
|
|
<!-- 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:
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// Clear specific cache tags
|
|
Cache::tags(['users'])->flush();
|
|
|
|
// Clear all RSX caches
|
|
php artisan rsx:manifest:clear
|
|
php artisan rsx:routes:clear
|
|
```
|
|
|
|
### Production Optimization
|
|
|
|
```bash
|
|
# Build and cache for production
|
|
php artisan rsx:manifest:build
|
|
php artisan rsx:routes:cache
|
|
php artisan config:cache
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit Testing Dispatching
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
// In .env
|
|
RSX_DEBUG=true
|
|
RSX_LOG_DISPATCH=true
|
|
```
|
|
|
|
### Viewing Routes
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```php
|
|
// In Dispatcher
|
|
Log::debug('Dispatch', [
|
|
'url' => $url,
|
|
'method' => $method,
|
|
'route' => $route_match,
|
|
'attributes' => $attributes,
|
|
'params' => $params
|
|
]);
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### From Laravel Routes
|
|
|
|
Before (Laravel):
|
|
```php
|
|
// routes/web.php
|
|
Route::get('/users', [UserController::class, 'index']);
|
|
Route::post('/users', [UserController::class, 'store']);
|
|
Route::get('/users/:id', [UserController::class, 'show']);
|
|
```
|
|
|
|
After (RSX):
|
|
```php
|
|
// /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:
|
|
```php
|
|
Route::middleware(['auth', 'verified'])->group(function () {
|
|
Route::get('/dashboard', [DashboardController::class, 'index']);
|
|
});
|
|
```
|
|
|
|
After:
|
|
```php
|
|
#[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:
|
|
|
|
```php
|
|
#[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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
#[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:
|
|
|
|
```php
|
|
// 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. |