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>
669 lines
22 KiB
Plaintext
Executable File
669 lines
22 KiB
Plaintext
Executable File
NAME
|
|
event_hooks - PHP event system with filters, gates, and actions
|
|
|
|
SYNOPSIS
|
|
Register event handlers with attributes:
|
|
|
|
#[OnEvent('event.name', priority: 10)]
|
|
public static function handler_method($data) {
|
|
// Process event
|
|
return $data;
|
|
}
|
|
|
|
Trigger events from code:
|
|
|
|
// Filter: Transform data through handler chain
|
|
$result = Rsx::trigger_filter('event.name', $data);
|
|
|
|
// Gate: First non-true response halts execution
|
|
$result = Rsx::trigger_gate('event.name', $data);
|
|
|
|
// Action: Fire and forget side effects
|
|
Rsx::trigger_action('event.name', $data);
|
|
|
|
DESCRIPTION
|
|
The RSX event system provides attribute-based event handling similar to
|
|
WordPress hooks or JavaScript event systems, allowing applications to
|
|
modify framework behavior without changing core code.
|
|
|
|
Unlike Laravel's event/listener system which requires manual registration
|
|
in service providers, RSX events use #[OnEvent] attributes that are
|
|
automatically discovered during manifest compilation.
|
|
|
|
RSX vs Laravel Events:
|
|
- Laravel: Register listeners in EventServiceProvider or listener classes
|
|
- RSX: Mark methods with #[OnEvent], automatic manifest discovery
|
|
|
|
The system provides three event types optimized for different use cases:
|
|
- Filters: Transform data through multiple handlers
|
|
- Gates: Authorization checks where first denial wins
|
|
- Actions: Side effects without return values
|
|
|
|
All handlers execute in priority order (lowest number first).
|
|
|
|
BASIC USAGE
|
|
|
|
Creating Event Handlers
|
|
|
|
Create a class in /rsx/handlers/ with static methods marked with
|
|
#[OnEvent]:
|
|
|
|
namespace Rsx\Handlers;
|
|
|
|
class Upload_Handlers
|
|
{
|
|
#[OnEvent('file.upload.authorize', priority: 10)]
|
|
public static function require_auth($data)
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response()->json([
|
|
'error' => 'Authentication required'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#[OnEvent('file.upload.params', priority: 20)]
|
|
public static function add_metadata($params)
|
|
{
|
|
$params['uploaded_by'] = Session::id();
|
|
return $params;
|
|
}
|
|
}
|
|
|
|
Handlers are automatically discovered by the manifest system. No
|
|
manual registration required.
|
|
|
|
Triggering Events
|
|
|
|
Use Rsx:: methods to trigger events from your code:
|
|
|
|
// In a controller or service
|
|
$upload_params = Rsx::trigger_filter('file.upload.params', [
|
|
'site_id' => 1,
|
|
'filename' => 'document.pdf'
|
|
]);
|
|
|
|
// Returns modified params after all handlers run
|
|
|
|
EVENT TYPES
|
|
|
|
Filter Events (trigger_filter)
|
|
|
|
Filters pass data through a chain of handlers, with each handler
|
|
modifying and returning the data for the next handler.
|
|
|
|
Usage:
|
|
$result = Rsx::trigger_filter('event.name', $data);
|
|
|
|
Handler signature:
|
|
public static function handler($data) {
|
|
// Modify $data
|
|
return $data; // Must return modified data
|
|
}
|
|
|
|
Flow:
|
|
$data = ['value' => 1];
|
|
Handler 1 (priority 10): receives ['value' => 1], returns ['value' => 2]
|
|
Handler 2 (priority 20): receives ['value' => 2], returns ['value' => 3]
|
|
Final result: ['value' => 3]
|
|
|
|
Use cases:
|
|
- Transforming request parameters
|
|
- Modifying response data
|
|
- Adding fields to data structures
|
|
- Sanitizing or validating input
|
|
|
|
Example:
|
|
#[OnEvent('post.content.filter', priority: 10)]
|
|
public static function sanitize_html($content) {
|
|
return strip_tags($content, '<p><a><strong><em>');
|
|
}
|
|
|
|
#[OnEvent('post.content.filter', priority: 20)]
|
|
public static function add_signature($content) {
|
|
return $content . "\n\nPosted via RSX";
|
|
}
|
|
|
|
// Usage:
|
|
$clean = Rsx::trigger_filter('post.content.filter', $raw_content);
|
|
|
|
Gate Events (trigger_gate)
|
|
|
|
Gates check authorization or validation. Handlers return true to allow
|
|
or any other value to deny. The first non-true response immediately
|
|
halts execution and is returned.
|
|
|
|
Usage:
|
|
$result = Rsx::trigger_gate('event.name', $data);
|
|
if ($result !== true) {
|
|
// Access denied, $result contains denial response
|
|
return $result;
|
|
}
|
|
|
|
Handler signature:
|
|
public static function handler($data) {
|
|
if (authorized) {
|
|
return true; // Allow, continue to next handler
|
|
}
|
|
return response()->json(['error' => 'Denied'], 403);
|
|
}
|
|
|
|
Flow:
|
|
Handler 1 (priority 10): returns true (continue)
|
|
Handler 2 (priority 20): returns JsonResponse (STOP)
|
|
Handler 3 (priority 30): never executes
|
|
Final result: JsonResponse from Handler 2
|
|
|
|
Use cases:
|
|
- Authorization checks
|
|
- Permission validation
|
|
- Rate limiting
|
|
- Blacklist checks
|
|
|
|
Example:
|
|
#[OnEvent('api.access.authorize', priority: 10)]
|
|
public static function check_authentication($data) {
|
|
if (!Session::is_logged_in()) {
|
|
return response()->json(['error' => 'Login required'], 401);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#[OnEvent('api.access.authorize', priority: 20)]
|
|
public static function check_permissions($data) {
|
|
if (!$data['user']->can($data['permission'])) {
|
|
return response()->json(['error' => 'Insufficient permissions'], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Usage:
|
|
$auth = Rsx::trigger_gate('api.access.authorize', [
|
|
'user' => Session::get_user(),
|
|
'permission' => 'edit_posts'
|
|
]);
|
|
if ($auth !== true) return $auth;
|
|
|
|
Action Events (trigger_action)
|
|
|
|
Actions perform side effects. Return values are ignored. All handlers
|
|
execute regardless of what previous handlers did.
|
|
|
|
Usage:
|
|
Rsx::trigger_action('event.name', $data);
|
|
|
|
Handler signature:
|
|
public static function handler($data): void {
|
|
// Perform side effect
|
|
// Return value ignored
|
|
}
|
|
|
|
Flow:
|
|
Handler 1 (priority 10): executes
|
|
Handler 2 (priority 20): executes
|
|
Handler 3 (priority 30): executes
|
|
All handlers always run
|
|
|
|
Use cases:
|
|
- Logging
|
|
- Sending notifications
|
|
- Triggering webhooks
|
|
- Cache invalidation
|
|
- Analytics tracking
|
|
|
|
Example:
|
|
#[OnEvent('user.registered', priority: 10)]
|
|
public static function send_welcome_email($data) {
|
|
Mail::to($data['user']->email)->send(new WelcomeEmail($data['user']));
|
|
}
|
|
|
|
#[OnEvent('user.registered', priority: 20)]
|
|
public static function log_registration($data) {
|
|
Log::info('New user registered', ['user_id' => $data['user']->id]);
|
|
}
|
|
|
|
#[OnEvent('user.registered', priority: 30)]
|
|
public static function notify_admins($data) {
|
|
Notification::send_admin_alert('New user: ' . $data['user']->email);
|
|
}
|
|
|
|
// Usage:
|
|
Rsx::trigger_action('user.registered', ['user' => $user]);
|
|
|
|
PRIORITY SYSTEM
|
|
|
|
Handlers execute in priority order (lowest number first). Default is 100.
|
|
|
|
#[OnEvent('event.name', priority: 10)] // Runs first
|
|
#[OnEvent('event.name', priority: 50)] // Runs second
|
|
#[OnEvent('event.name', priority: 100)] // Runs third (default)
|
|
|
|
Priority Guidelines:
|
|
1-10 Critical early processing (auth, validation)
|
|
11-50 Main application logic
|
|
51-100 Post-processing, cleanup
|
|
101+ Logging, analytics (fire last)
|
|
|
|
For gate events, lower priority handlers can block higher priority ones
|
|
from ever executing.
|
|
|
|
API REFERENCE
|
|
|
|
Rsx::trigger_filter(string $event, mixed $data): mixed
|
|
|
|
Passes $data through all handlers for $event, with each handler
|
|
receiving the output of the previous handler.
|
|
|
|
Parameters:
|
|
$event - Event name (e.g., 'file.upload.params')
|
|
$data - Initial data to transform
|
|
|
|
Returns:
|
|
Final data after all handlers have processed it
|
|
|
|
Example:
|
|
$params = ['filename' => 'test.pdf'];
|
|
$params = Rsx::trigger_filter('upload.params', $params);
|
|
// $params may now have additional fields added by handlers
|
|
|
|
Rsx::trigger_gate(string $event, mixed $data): mixed
|
|
|
|
Executes authorization handlers until one returns non-true. Returns
|
|
true if all handlers allow, or the first denial response.
|
|
|
|
Parameters:
|
|
$event - Event name (e.g., 'api.authorize')
|
|
$data - Authorization context data
|
|
|
|
Returns:
|
|
true if all handlers allow access
|
|
Response object (or any non-true value) if denied
|
|
|
|
Example:
|
|
$result = Rsx::trigger_gate('delete.authorize', ['post' => $post]);
|
|
if ($result !== true) {
|
|
return $result; // Return denial response
|
|
}
|
|
// Proceed with delete
|
|
|
|
Rsx::trigger_action(string $event, mixed $data): void
|
|
|
|
Executes all handlers as side effects. Return values ignored.
|
|
|
|
Parameters:
|
|
$event - Event name (e.g., 'user.logged_in')
|
|
$data - Event context data
|
|
|
|
Returns:
|
|
void
|
|
|
|
Example:
|
|
Rsx::trigger_action('order.completed', [
|
|
'order' => $order,
|
|
'user' => $user
|
|
]);
|
|
|
|
NAMING CONVENTIONS
|
|
|
|
Event names use dot notation to indicate scope and hierarchy:
|
|
|
|
{module}.{entity}.{action}
|
|
|
|
Examples:
|
|
file.upload.authorize File upload authorization
|
|
file.upload.params File upload parameter modification
|
|
file.upload.complete File upload completion
|
|
user.login.authorize User login authorization
|
|
user.login.success User login success
|
|
post.save.before Before saving post
|
|
post.save.after After saving post
|
|
|
|
Guidelines:
|
|
- Use lowercase with dots
|
|
- Start with module/feature name
|
|
- Include entity being acted upon
|
|
- End with action or lifecycle event
|
|
- Be specific and descriptive
|
|
|
|
FRAMEWORK EVENTS
|
|
|
|
The framework fires these events automatically:
|
|
|
|
File Upload Events
|
|
|
|
file.upload.authorize (gate)
|
|
When: Before processing upload
|
|
Data: {request: Request, user: User|null, params: array}
|
|
Use: Authorization, rate limiting, quota checks
|
|
|
|
file.upload.params (filter)
|
|
When: Before saving file
|
|
Data: array of upload parameters
|
|
Use: Add metadata, modify filename, set categories
|
|
|
|
file.upload.complete (action)
|
|
When: After file saved successfully
|
|
Data: {attachment: File_Attachment_Model, request: Request, params: array}
|
|
Use: Logging, notifications, post-processing
|
|
|
|
file.upload.response (filter)
|
|
When: Before sending response to client
|
|
Data: {success: bool, attachment: array}
|
|
Use: Add custom fields to response, modify URLs
|
|
|
|
IMPLEMENTATION DETAILS
|
|
|
|
Manifest Integration
|
|
|
|
Event handlers are discovered during manifest compilation by scanning
|
|
for #[OnEvent] attributes on public static methods.
|
|
|
|
The manifest extracts:
|
|
- Event name from first attribute parameter
|
|
- Priority from second parameter or 'priority' key
|
|
- Class and method name for callable construction
|
|
|
|
Handlers are stored in manifest_data.php under 'event_handlers' key:
|
|
|
|
'event_handlers' => [
|
|
'file.upload.authorize' => [
|
|
[
|
|
'class' => 'Rsx\\Handlers\\Upload_Handlers',
|
|
'method' => 'require_auth',
|
|
'priority' => 10,
|
|
'file' => '/path/to/Upload_Handlers.php'
|
|
]
|
|
]
|
|
]
|
|
|
|
This approach avoids runtime reflection - all handler discovery
|
|
happens during manifest compilation.
|
|
|
|
Event_Registry
|
|
|
|
The Event_Registry class loads handler definitions from the manifest
|
|
and constructs callables:
|
|
|
|
Event_Registry::get_handlers('event.name')
|
|
// Returns array of callables sorted by priority
|
|
|
|
Handler callables are created as closures that invoke the static
|
|
methods:
|
|
|
|
function ($data) use ($class, $method) {
|
|
return $class::$method($data);
|
|
}
|
|
|
|
Registry loads handlers lazily on first access.
|
|
|
|
Performance
|
|
|
|
Handler discovery: Zero runtime cost (manifest compilation time only)
|
|
Event triggering: O(n) where n = number of handlers for that event
|
|
Handler lookup: O(1) from manifest array
|
|
|
|
EXAMPLES
|
|
|
|
Complete Authorization Example
|
|
|
|
File: /rsx/handlers/Upload_Security.php
|
|
|
|
namespace Rsx\Handlers;
|
|
|
|
use App\RSpade\Core\Session\Session;
|
|
|
|
class Upload_Security
|
|
{
|
|
#[OnEvent('file.upload.authorize', priority: 10)]
|
|
public static function require_login($data)
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Login required'
|
|
], 401);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#[OnEvent('file.upload.authorize', priority: 20)]
|
|
public static function check_quota($data)
|
|
{
|
|
$user = Session::get_user();
|
|
if ($user->storage_used >= $user->storage_quota) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'Storage quota exceeded'
|
|
], 403);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#[OnEvent('file.upload.authorize', priority: 30)]
|
|
public static function check_file_type($data)
|
|
{
|
|
$allowed = ['jpg', 'png', 'pdf', 'doc', 'docx'];
|
|
$ext = $data['request']->file('file')->getClientOriginalExtension();
|
|
if (!in_array(strtolower($ext), $allowed)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => 'File type not allowed'
|
|
], 400);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Complete Filter Chain Example
|
|
|
|
File: /rsx/handlers/Content_Filters.php
|
|
|
|
namespace Rsx\Handlers;
|
|
|
|
class Content_Filters
|
|
{
|
|
#[OnEvent('post.content.filter', priority: 10)]
|
|
public static function strip_dangerous_html($content)
|
|
{
|
|
return strip_tags($content, '<p><a><strong><em><ul><ol><li>');
|
|
}
|
|
|
|
#[OnEvent('post.content.filter', priority: 20)]
|
|
public static function convert_links($content)
|
|
{
|
|
return preg_replace(
|
|
'/(https?:\/\/[^\s]+)/',
|
|
'<a href="$1" target="_blank">$1</a>',
|
|
$content
|
|
);
|
|
}
|
|
|
|
#[OnEvent('post.content.filter', priority: 30)]
|
|
public static function add_nofollow($content)
|
|
{
|
|
return str_replace('<a ', '<a rel="nofollow" ', $content);
|
|
}
|
|
}
|
|
|
|
// Usage in controller:
|
|
$clean_content = Rsx::trigger_filter('post.content.filter', $raw_content);
|
|
|
|
Complete Action Example
|
|
|
|
File: /rsx/handlers/User_Events.php
|
|
|
|
namespace Rsx\Handlers;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
class User_Events
|
|
{
|
|
#[OnEvent('user.registered', priority: 10)]
|
|
public static function send_welcome_email($data)
|
|
{
|
|
Mail::to($data['user']->email)->send(
|
|
new WelcomeEmail($data['user'])
|
|
);
|
|
}
|
|
|
|
#[OnEvent('user.registered', priority: 50)]
|
|
public static function log_registration($data)
|
|
{
|
|
Log::info('User registered', [
|
|
'user_id' => $data['user']->id,
|
|
'email' => $data['user']->email,
|
|
'ip' => request()->ip()
|
|
]);
|
|
}
|
|
|
|
#[OnEvent('user.registered', priority: 100)]
|
|
public static function track_analytics($data)
|
|
{
|
|
Analytics::track('user_registration', [
|
|
'user_id' => $data['user']->id
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Usage in registration controller:
|
|
Rsx::trigger_action('user.registered', ['user' => $user]);
|
|
|
|
RSX VS LARAVEL
|
|
|
|
Event Registration:
|
|
Laravel: Manual registration in EventServiceProvider
|
|
RSX: Automatic discovery via #[OnEvent] attributes
|
|
|
|
Event Listeners:
|
|
Laravel: Separate listener classes with handle() method
|
|
RSX: Static methods with #[OnEvent] attribute
|
|
|
|
Performance:
|
|
Laravel: Runtime service container resolution
|
|
RSX: Compile-time manifest indexing
|
|
|
|
Flexibility:
|
|
Laravel: Event/Listener classes, queued listeners, subscribers
|
|
RSX: Three event types (filter/gate/action) for common patterns
|
|
|
|
Learning Curve:
|
|
Laravel: Understanding events, listeners, service providers
|
|
RSX: Mark methods with #[OnEvent], call Rsx::trigger_*()
|
|
|
|
Example Comparison:
|
|
|
|
Laravel Event/Listener:
|
|
// app/Events/UserRegistered.php
|
|
class UserRegistered {
|
|
public function __construct(public User $user) {}
|
|
}
|
|
|
|
// app/Listeners/SendWelcomeEmail.php
|
|
class SendWelcomeEmail {
|
|
public function handle(UserRegistered $event) {
|
|
Mail::to($event->user)->send(new Welcome());
|
|
}
|
|
}
|
|
|
|
// app/Providers/EventServiceProvider.php
|
|
protected $listen = [
|
|
UserRegistered::class => [
|
|
SendWelcomeEmail::class,
|
|
]
|
|
];
|
|
|
|
// Usage:
|
|
event(new UserRegistered($user));
|
|
|
|
RSX Events:
|
|
// rsx/handlers/User_Handlers.php
|
|
class User_Handlers {
|
|
#[OnEvent('user.registered', priority: 10)]
|
|
public static function send_welcome_email($data) {
|
|
Mail::to($data['user'])->send(new Welcome());
|
|
}
|
|
}
|
|
|
|
// Usage:
|
|
Rsx::trigger_action('user.registered', ['user' => $user]);
|
|
|
|
TROUBLESHOOTING
|
|
|
|
Handler Not Executing
|
|
|
|
Problem: Event triggered but handler not called
|
|
|
|
Solutions:
|
|
1. Verify #[OnEvent] attribute syntax is correct
|
|
2. Ensure method is public and static
|
|
3. Check handler class is in /rsx/ directory (manifest scanned)
|
|
4. Rebuild manifest if handler was just added
|
|
5. Verify event name matches exactly (case sensitive)
|
|
|
|
Debug:
|
|
// Check what handlers are registered
|
|
$handlers = Event_Registry::get_handlers('event.name');
|
|
var_dump($handlers);
|
|
|
|
Handler Executing in Wrong Order
|
|
|
|
Problem: Handlers run in unexpected sequence
|
|
|
|
Solutions:
|
|
1. Check priority values (lower = earlier)
|
|
2. Ensure priority parameter is spelled correctly
|
|
3. Verify priority is integer, not string
|
|
|
|
Example:
|
|
#[OnEvent('event', priority: 10)] // Runs first
|
|
#[OnEvent('event', priority: 20)] // Runs second
|
|
|
|
Gate Event Not Stopping
|
|
|
|
Problem: Gate continues after handler returns non-true
|
|
|
|
Solutions:
|
|
1. Verify handler returns exactly true to allow
|
|
2. Check that denial handler returns Response object
|
|
3. Ensure trigger_gate() result is checked in caller
|
|
|
|
Example:
|
|
// Correct:
|
|
$result = Rsx::trigger_gate('auth', $data);
|
|
if ($result !== true) {
|
|
return $result; // Important: return the denial
|
|
}
|
|
|
|
Filter Not Modifying Data
|
|
|
|
Problem: Filter chain doesn't modify data as expected
|
|
|
|
Solutions:
|
|
1. Verify each handler returns modified data
|
|
2. Check handlers receive data from previous handler
|
|
3. Ensure trigger_filter() result is used
|
|
|
|
Example:
|
|
// Wrong:
|
|
Rsx::trigger_filter('modify', $data); // Result ignored!
|
|
|
|
// Correct:
|
|
$data = Rsx::trigger_filter('modify', $data);
|
|
|
|
Manifest Not Finding Handlers
|
|
|
|
Problem: New handlers not discovered
|
|
|
|
Solutions:
|
|
1. Ensure file is in /rsx/ directory
|
|
2. Verify PHP syntax is valid (parse errors break manifest)
|
|
3. Check class namespace is declared
|
|
4. Wait for automatic manifest rebuild (dev mode)
|
|
|
|
SEE ALSO
|
|
php artisan rsx:man file_upload
|
|
php artisan rsx:man manifest_api
|
|
php artisan rsx:man coding_standards
|