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>
688 lines
15 KiB
Plaintext
Executable File
688 lines
15 KiB
Plaintext
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
|
|
|
|
The `$params` array passed to controllers contains ONLY:
|
|
|
|
1. **URL Route Parameters** - Values extracted from the route pattern (e.g., `:id`)
|
|
2. **GET Parameters** - Query string parameters
|
|
|
|
POST data is NOT included in `$params` and must be accessed via `$request`:
|
|
|
|
```php
|
|
// Route: /users/:id
|
|
// URL: /users/123?sort=name
|
|
// POST body: name=John&email=john@example.com
|
|
|
|
public static function update(Request $request, array $params = [])
|
|
{
|
|
// From URL route pattern
|
|
$user_id = $params['id']; // '123'
|
|
|
|
// From query string (GET)
|
|
$sort = $params['sort']; // 'name'
|
|
|
|
// From POST body (NOT in $params)
|
|
$name = $request->input('name'); // 'John'
|
|
$email = $request->post('email'); // 'john@example.com'
|
|
}
|
|
```
|
|
|
|
If the same parameter name exists in both route and query string, route parameter wins:
|
|
|
|
```php
|
|
// Route: /users/:id
|
|
// URL: /users/123?id=456
|
|
|
|
// 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. |