Files
rspade_system/app/RSpade/man/event_hooks.txt
root 77b4d10af8 Refactor filename naming system and apply convention-based renames
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>
2025-11-13 19:10:02 +00:00

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