Always include params in window.rsxapp to reduce state variations Add request params to window.rsxapp global Enhance module creation commands with clear nomenclature guidance Add module/submodule/feature nomenclature clarification to docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
440 lines
15 KiB
PHP
Executable File
440 lines
15 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;
|
|
|
|
/**
|
|
* Current request params
|
|
* @var array|null
|
|
*/
|
|
protected static $current_params = 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
|
|
* @param array $params Optional request params to store
|
|
*/
|
|
public static function _set_current_controller_action($controller_class, $action_method, array $params = [])
|
|
{
|
|
// 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;
|
|
static::$current_params = $params;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Get the current request params
|
|
*
|
|
* @return array|null The current request params or null if not set
|
|
*/
|
|
public static function get_current_params()
|
|
{
|
|
return static::$current_params;
|
|
}
|
|
|
|
/**
|
|
* Clear the current controller and action tracking
|
|
*/
|
|
public static function _clear_current_controller_action()
|
|
{
|
|
static::$current_controller = null;
|
|
static::$current_action = null;
|
|
static::$current_params = 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;
|
|
}
|
|
}
|