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 .= << {$message} 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; } }