Files
rspade_system/app/RSpade/Core/Rsx.php
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

349 lines
12 KiB
PHP
Executable File

<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*
* @ROUTE-EXISTS-01-EXCEPTION - This file contains documentation examples with fictional route names
*/
namespace App\RSpade\Core;
use App\Models\FlashAlert;
use RuntimeException;
use App\RSpade\Core\Debug\Rsx_Caller_Exception;
use App\RSpade\Core\Manifest\Manifest;
use App\RSpade\Core\Rsx_Route_Proxy;
use App\RSpade\Core\Session\Session;
/**
* Core RSX framework utility class
*
* Provides static utility methods for the RSX framework including
* flash messages and other core functionality.
*/
class Rsx
{
/**
* Current controller being executed
* @var string|null
*/
protected static $current_controller = null;
/**
* Current action being executed
* @var string|null
*/
protected static $current_action = null;
/**
* Set the current controller and action being executed
*
* @param string $controller_class The controller class name
* @param string $action_method The action method name
*/
public static function _set_current_controller_action($controller_class, $action_method)
{
// Extract just the class name without namespace
$parts = explode('\\', $controller_class);
$class_name = end($parts);
static::$current_controller = $class_name;
static::$current_action = $action_method;
}
/**
* Get the current controller class name
*
* @return string|null The current controller class or null if not set
*/
public static function get_current_controller()
{
return static::$current_controller;
}
/**
* Get the current action method name
*
* @return string|null The current action method or null if not set
*/
public static function get_current_action()
{
return static::$current_action;
}
/**
* Clear the current controller and action tracking
*/
public static function _clear_current_controller_action()
{
static::$current_controller = null;
static::$current_action = null;
}
/**
* Add a flash alert message for the current session
*
* @param string $message The message to display
* @param string $class_attribute Optional CSS class attribute (defaults to 'alert alert-danger alert-flash')
* @return void
*/
public static function flash_alert($message, $class_attribute = 'alert alert-danger alert-flash')
{
$session_id = Session::get_session_id();
if ($session_id === null) {
return;
}
$flash_alert = new FlashAlert();
$flash_alert->session_id = $session_id;
$flash_alert->message = $message;
$flash_alert->class_attribute = $class_attribute;
$flash_alert->created_at = now();
$flash_alert->save();
}
/**
* Render all flash alerts for the current session
*
* Returns HTML for all flash messages and deletes them from the database.
* Messages are rendered as Bootstrap 5 alerts with dismissible buttons.
*
* @return string HTML string containing all flash alerts or empty string
*/
public static function render_flash_alerts()
{
$session_id = Session::get_session_id();
if ($session_id === null) {
return '';
}
// Get all flash alerts for this session
$alerts = FlashAlert::where('session_id', $session_id)
->orderBy('created_at', 'asc')
->get();
if ($alerts->isEmpty()) {
return '';
}
// Delete the alerts now that we're rendering them
FlashAlert::where('session_id', $session_id)
->delete();
// Build HTML for all alerts
$html = '';
foreach ($alerts as $alert) {
$message = htmlspecialchars($alert->message);
$class = htmlspecialchars($alert->class_attribute);
$html .= <<<HTML
<div class="{$class} show" role="alert">
{$message}
</div>
HTML;
}
return $html;
}
/**
* Helper method to add a success flash alert
*
* @param string $message
* @return void
*/
public static function flash_success($message)
{
self::flash_alert($message, 'alert alert-success alert-flash');
}
/**
* Helper method to add an error flash alert
*
* @param string $message
* @return void
*/
public static function flash_error($message)
{
self::flash_alert($message, 'alert alert-danger alert-flash');
}
/**
* Helper method to add a warning flash alert
*
* @param string $message
* @return void
*/
public static function flash_warning($message)
{
self::flash_alert($message, 'alert alert-warning alert-flash');
}
/**
* Helper method to add an info flash alert
*
* @param string $message
* @return void
*/
public static function flash_info($message)
{
self::flash_alert($message, 'alert alert-info alert-flash');
}
/**
* Create a route proxy for type-safe URL generation
*
* This method creates a route proxy that can generate URLs for a specific controller action.
* The proxy ensures all required route parameters are provided and handles extra parameters
* as query string values.
*
* Placeholder Routes:
* When the action starts with '#' (e.g., '#index', '#show'), it indicates a placeholder/unimplemented
* route for scaffolding purposes. These skip validation and return href="#" to allow incremental
* development without requiring all controllers to exist.
*
* Usage examples:
* ```php
* // Simple route without parameters (defaults to 'index' action)
* $url = Rsx::Route('Frontend_Index_Controller')->url();
* // Returns: /dashboard
*
* // Route with explicit action
* $url = Rsx::Route('Frontend_Index_Controller')->url();
* // Returns: /dashboard
*
* // Route with required parameter
* $url = Rsx::Route('Frontend_Client_View_Controller')->url(['id' => 'C001']);
* // Returns: /clients/view/C001
*
* // Route with required and query parameters
* $url = Rsx::Route('Frontend_Client_View_Controller')->url([
* 'id' => 'C001',
* 'tab' => 'history'
* ]);
* // Returns: /clients/view/C001?tab=history
*
* // Generate absolute URL
* $absolute = Rsx::Route('Frontend_Index_Controller')->absolute_url();
* // Returns: https://example.com/dashboard
*
* // Check if route is current
* if (Rsx::Route('Frontend_Index_Controller')->is_current()) {
* // This is the currently executing route
* }
*
* // Placeholder route for scaffolding (controller doesn't need to exist)
* $url = Rsx::Route('Future_Feature_Controller', '#index')->url();
* // Returns: #
* ```
*
* @param string $class_name The controller class name (e.g., 'User_Controller')
* @param string $action_name The action/method name (defaults to 'index'). Use '#action' for placeholders.
* @return Rsx_Route_Proxy Route proxy instance for URL generation
* @throws RuntimeException If class doesn't exist, isn't a controller, method doesn't exist, or lacks Route attribute
*/
public static function Route($class_name, $action_name = 'index')
{
// Placeholder route: action starts with # means unimplemented/scaffolding
// Skip all validation and return a placeholder proxy
if (str_starts_with($action_name, '#')) {
return new Rsx_Route_Proxy($class_name, $action_name, '#');
}
// Try to find the class in the manifest
try {
$metadata = Manifest::php_get_metadata_by_class($class_name);
} catch (RuntimeException $e) {
// Report error at caller's location (the blade template or PHP code calling Rsx::Route)
throw new Rsx_Caller_Exception("Class {$class_name} not found in manifest");
}
// Verify it extends Rsx_Controller_Abstract
$extends = $metadata['extends'] ?? '';
$is_controller = false;
if ($extends === 'Rsx_Controller_Abstract') {
$is_controller = true;
} else {
// Check if it extends a class that extends Rsx_Controller_Abstract
$current_class = $extends;
$max_depth = 10;
while ($current_class && $max_depth-- > 0) {
try {
$parent_metadata = Manifest::php_get_metadata_by_class($current_class);
if (($parent_metadata['extends'] ?? '') === 'Rsx_Controller_Abstract') {
$is_controller = true;
break;
}
$current_class = $parent_metadata['extends'] ?? '';
} catch (RuntimeException $e) {
// Check if parent is the abstract controller with FQCN
if ($current_class === 'Rsx_Controller_Abstract' ||
$current_class === 'App\\RSpade\\Core\\Controller\\Rsx_Controller_Abstract') {
$is_controller = true;
}
break;
}
}
}
if (!$is_controller) {
throw new Rsx_Caller_Exception("Class {$class_name} must extend Rsx_Controller_Abstract");
}
// Check if method exists and has Route attribute
if (!isset($metadata['public_static_methods'][$action_name])) {
throw new Rsx_Caller_Exception("Method {$action_name} not found in class {$class_name}");
}
$method_info = $metadata['public_static_methods'][$action_name];
// All methods in public_static_methods are guaranteed to be static
// No need to check - but we assert for safety
if (!isset($method_info['static']) || !$method_info['static']) {
shouldnt_happen("Method {$class_name}::{$action_name} in public_static_methods is not static - extraction bug");
}
// Check for Route or Ajax_Endpoint attribute
$has_route = false;
$has_ajax_endpoint = false;
$route_pattern = null;
if (isset($method_info['attributes'])) {
foreach ($method_info['attributes'] as $attr_name => $attr_instances) {
if ($attr_name === 'Route' || str_ends_with($attr_name, '\\Route')) {
$has_route = true;
// Get the route pattern from the first instance
if (!empty($attr_instances)) {
$route_args = $attr_instances[0];
$route_pattern = $route_args[0] ?? ($route_args['pattern'] ?? null);
}
break;
}
if ($attr_name === 'Ajax_Endpoint' || str_ends_with($attr_name, '\\Ajax_Endpoint')) {
$has_ajax_endpoint = true;
break;
}
}
}
// If has Ajax_Endpoint, return AJAX route URL
if ($has_ajax_endpoint) {
$ajax_url = '/_ajax/' . urlencode($class_name) . '/' . urlencode($action_name);
return new Rsx_Route_Proxy($class_name, $action_name, $ajax_url);
}
if (!$has_route) {
throw new Rsx_Caller_Exception("Method {$class_name}::{$action_name} must have Route or Ajax_Endpoint attribute");
}
if (!$route_pattern) {
throw new Rsx_Caller_Exception("Route attribute on {$class_name}::{$action_name} must have a pattern");
}
return new Rsx_Route_Proxy($class_name, $action_name, $route_pattern);
}
}