Add semantic token highlighting for 'that' variable and comment file references in VS Code extension Add Phone_Text_Input and Currency_Input components with formatting utilities Implement client widgets, form standardization, and soft delete functionality Add modal scroll lock and update documentation Implement comprehensive modal system with form integration and validation Fix modal component instantiation using jQuery plugin API Implement modal system with responsive sizing, queuing, and validation support Implement form submission with validation, error handling, and loading states Implement country/state selectors with dynamic data loading and Bootstrap styling Revert Rsx::Route() highlighting in Blade/PHP files Target specific PHP scopes for Rsx::Route() highlighting in Blade Expand injection selector for Rsx::Route() highlighting Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls Update jqhtml packages to v2.2.165 Add bundle path validation for common mistakes (development mode only) Create Ajax_Select_Input widget and Rsx_Reference_Data controller Create Country_Select_Input widget with default country support Initialize Tom Select on Select_Input widgets Add Tom Select bundle for enhanced select dropdowns Implement ISO 3166 geographic data system for country/region selection Implement widget-based form system with disabled state support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
421 lines
14 KiB
PHP
Executable File
421 lines
14 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\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');
|
|
}
|
|
|
|
/**
|
|
* Generate URL for a controller route
|
|
*
|
|
* This method generates URLs for controller actions by looking up route patterns
|
|
* and replacing parameters. It handles both regular routes and Ajax endpoints.
|
|
*
|
|
* 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 "#" 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');
|
|
* // Returns: /dashboard
|
|
*
|
|
* // Route with explicit action
|
|
* $url = Rsx::Route('Frontend_Index_Controller', 'index');
|
|
* // Returns: /dashboard
|
|
*
|
|
* // Route with integer parameter (sets 'id')
|
|
* $url = Rsx::Route('Frontend_Client_View_Controller', 'view', 123);
|
|
* // Returns: /clients/view/123
|
|
*
|
|
* // Route with named parameters (array)
|
|
* $url = Rsx::Route('Frontend_Client_View_Controller', 'view', ['id' => 'C001']);
|
|
* // Returns: /clients/view/C001
|
|
*
|
|
* // Route with required and query parameters
|
|
* $url = Rsx::Route('Frontend_Client_View_Controller', 'view', [
|
|
* 'id' => 'C001',
|
|
* 'tab' => 'history'
|
|
* ]);
|
|
* // Returns: /clients/view/C001?tab=history
|
|
*
|
|
* // Placeholder route for scaffolding (controller doesn't need to exist)
|
|
* $url = Rsx::Route('Future_Feature_Controller', '#index');
|
|
* // 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.
|
|
* @param int|array|\stdClass|null $params Route parameters. Integer sets 'id', array/object provides named params.
|
|
* @return string The generated URL
|
|
* @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', $params = null)
|
|
{
|
|
// Normalize params to array
|
|
$params_array = [];
|
|
if (is_int($params)) {
|
|
$params_array = ['id' => $params];
|
|
} elseif (is_array($params)) {
|
|
$params_array = $params;
|
|
} elseif ($params instanceof \stdClass) {
|
|
$params_array = (array) $params;
|
|
} elseif ($params !== null) {
|
|
throw new RuntimeException("Params must be integer, array, stdClass, or null");
|
|
}
|
|
|
|
// Placeholder route: action starts with # means unimplemented/scaffolding
|
|
// Skip all validation and return placeholder
|
|
if (str_starts_with($action_name, '#')) {
|
|
return '#';
|
|
}
|
|
|
|
// 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("Could not generate route URL: controller class {$class_name} not found");
|
|
}
|
|
|
|
// 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 (no param substitution)
|
|
if ($has_ajax_endpoint) {
|
|
$ajax_url = '/_ajax/' . urlencode($class_name) . '/' . urlencode($action_name);
|
|
// Add query params if provided
|
|
if (!empty($params_array)) {
|
|
$ajax_url .= '?' . http_build_query($params_array);
|
|
}
|
|
return $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");
|
|
}
|
|
|
|
// Generate URL from pattern
|
|
return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, $action_name);
|
|
}
|
|
|
|
/**
|
|
* Generate URL from route pattern by replacing parameters
|
|
*
|
|
* @param string $pattern The route pattern (e.g., '/users/:id/view')
|
|
* @param array $params Parameters to fill into the route
|
|
* @param string $class_name Controller class name (for error messages)
|
|
* @param string $action_name Action name (for error messages)
|
|
* @return string The generated URL
|
|
* @throws RuntimeException If required parameters are missing
|
|
*/
|
|
protected static function _generate_url_from_pattern($pattern, $params, $class_name, $action_name)
|
|
{
|
|
// Extract required parameters from the pattern
|
|
$required_params = [];
|
|
if (preg_match_all('/:([a-zA-Z_][a-zA-Z0-9_]*)/', $pattern, $matches)) {
|
|
$required_params = $matches[1];
|
|
}
|
|
|
|
// Check for required parameters
|
|
$missing = [];
|
|
foreach ($required_params as $required) {
|
|
if (!array_key_exists($required, $params)) {
|
|
$missing[] = $required;
|
|
}
|
|
}
|
|
|
|
if (!empty($missing)) {
|
|
throw new RuntimeException(
|
|
"Required parameters [" . implode(', ', $missing) . "] are missing for route " .
|
|
"{$pattern} on {$class_name}::{$action_name}"
|
|
);
|
|
}
|
|
|
|
// Build the URL by replacing parameters
|
|
$url = $pattern;
|
|
$used_params = [];
|
|
|
|
foreach ($required_params as $param_name) {
|
|
$value = $params[$param_name];
|
|
// URL encode the value
|
|
$encoded_value = urlencode($value);
|
|
$url = str_replace(':' . $param_name, $encoded_value, $url);
|
|
$used_params[$param_name] = true;
|
|
}
|
|
|
|
// Collect any extra parameters for query string
|
|
$query_params = [];
|
|
foreach ($params as $key => $value) {
|
|
if (!isset($used_params[$key])) {
|
|
$query_params[$key] = $value;
|
|
}
|
|
}
|
|
|
|
// Append query string if there are extra parameters
|
|
if (!empty($query_params)) {
|
|
$url .= '?' . http_build_query($query_params);
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
}
|