Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,12 +9,10 @@
|
||||
|
||||
namespace App\RSpade\Core;
|
||||
|
||||
use App\Models\FlashAlert;
|
||||
use RuntimeException;
|
||||
use App\RSpade\Core\Debug\Rsx_Caller_Exception;
|
||||
use App\RSpade\Core\Events\Event_Registry;
|
||||
use App\RSpade\Core\Manifest\Manifest;
|
||||
use App\RSpade\Core\Session\Session;
|
||||
|
||||
/**
|
||||
* Core RSX framework utility class
|
||||
@@ -42,14 +40,21 @@ class Rsx
|
||||
*/
|
||||
protected static $current_params = null;
|
||||
|
||||
/**
|
||||
* Current route type ('spa' or 'standard')
|
||||
* @var string|null
|
||||
*/
|
||||
protected static $current_route_type = 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
|
||||
* @param string|null $route_type Route type ('spa' or 'standard')
|
||||
*/
|
||||
public static function _set_current_controller_action($controller_class, $action_method, array $params = [])
|
||||
public static function _set_current_controller_action($controller_class, $action_method, array $params = [], $route_type = null)
|
||||
{
|
||||
// Extract just the class name without namespace
|
||||
$parts = explode('\\', $controller_class);
|
||||
@@ -58,6 +63,7 @@ class Rsx
|
||||
static::$current_controller = $class_name;
|
||||
static::$current_action = $action_method;
|
||||
static::$current_params = $params;
|
||||
static::$current_route_type = $route_type;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +96,16 @@ class Rsx
|
||||
return static::$current_params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current route is a SPA route
|
||||
*
|
||||
* @return bool True if current route type is 'spa', false otherwise
|
||||
*/
|
||||
public static function is_spa()
|
||||
{
|
||||
return static::$current_route_type === 'spa';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the current controller and action tracking
|
||||
*/
|
||||
@@ -98,117 +114,17 @@ class Rsx
|
||||
static::$current_controller = null;
|
||||
static::$current_action = null;
|
||||
static::$current_params = null;
|
||||
static::$current_route_type = 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');
|
||||
}
|
||||
// Flash alert methods have been removed - use Flash class instead:
|
||||
// Flash_Alert::success($message)
|
||||
// Flash_Alert::error($message)
|
||||
// Flash_Alert::info($message)
|
||||
// Flash_Alert::warning($message)
|
||||
//
|
||||
// See: /system/app/RSpade/Core/Flash/Flash.php
|
||||
// See: /system/app/RSpade/Core/Flash/CLAUDE.md
|
||||
|
||||
/**
|
||||
* Generate URL for a controller route
|
||||
@@ -223,42 +139,54 @@ HTML;
|
||||
*
|
||||
* Usage examples:
|
||||
* ```php
|
||||
* // Simple route without parameters (defaults to 'index' action)
|
||||
* // Controller route (defaults to 'index' method)
|
||||
* $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);
|
||||
* // Controller route with explicit method
|
||||
* $url = Rsx::Route('Frontend_Client_View_Controller::view', 123);
|
||||
* // Returns: /clients/view/123
|
||||
*
|
||||
* // SPA action route
|
||||
* $url = Rsx::Route('Contacts_Index_Action');
|
||||
* // Returns: /contacts
|
||||
*
|
||||
* // Route with integer parameter (sets 'id')
|
||||
* $url = Rsx::Route('Contacts_View_Action', 123);
|
||||
* // Returns: /contacts/123
|
||||
*
|
||||
* // Route with named parameters (array)
|
||||
* $url = Rsx::Route('Frontend_Client_View_Controller', 'view', ['id' => 'C001']);
|
||||
* // Returns: /clients/view/C001
|
||||
* $url = Rsx::Route('Contacts_View_Action', ['id' => 'C001']);
|
||||
* // Returns: /contacts/C001
|
||||
*
|
||||
* // Route with required and query parameters
|
||||
* $url = Rsx::Route('Frontend_Client_View_Controller', 'view', [
|
||||
* $url = Rsx::Route('Contacts_View_Action', [
|
||||
* 'id' => 'C001',
|
||||
* 'tab' => 'history'
|
||||
* ]);
|
||||
* // Returns: /clients/view/C001?tab=history
|
||||
* // Returns: /contacts/C001?tab=history
|
||||
*
|
||||
* // Placeholder route for scaffolding (controller doesn't need to exist)
|
||||
* $url = Rsx::Route('Future_Feature_Controller', '#index');
|
||||
* // Placeholder route for scaffolding (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 string $action Controller class, SPA action, or "Class::method". Defaults to 'index' method if not specified.
|
||||
* @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
|
||||
* @throws RuntimeException If class doesn't exist, isn't a controller/action, method doesn't exist, or lacks Route attribute
|
||||
*/
|
||||
public static function Route($class_name, $action_name = 'index', $params = null)
|
||||
public static function Route($action, $params = null)
|
||||
{
|
||||
// Parse action into class_name and action_name
|
||||
// Format: "Controller_Name" or "Controller_Name::method_name" or "Spa_Action_Name"
|
||||
if (str_contains($action, '::')) {
|
||||
[$class_name, $action_name] = explode('::', $action, 2);
|
||||
} else {
|
||||
$class_name = $action;
|
||||
$action_name = 'index';
|
||||
}
|
||||
|
||||
// Normalize params to array
|
||||
$params_array = [];
|
||||
if (is_int($params)) {
|
||||
@@ -281,8 +209,8 @@ HTML;
|
||||
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");
|
||||
// Not found as PHP class - might be a SPA action, try that instead
|
||||
return static::_try_spa_action_route($class_name, $params_array);
|
||||
}
|
||||
|
||||
// Verify it extends Rsx_Controller_Abstract
|
||||
@@ -366,7 +294,8 @@ HTML;
|
||||
}
|
||||
|
||||
if (!$has_route) {
|
||||
throw new Rsx_Caller_Exception("Method {$class_name}::{$action_name} must have Route or Ajax_Endpoint attribute");
|
||||
// Not a controller method with Route/Ajax - check if it's a SPA action class
|
||||
return static::_try_spa_action_route($class_name, $params_array);
|
||||
}
|
||||
|
||||
if (!$route_pattern) {
|
||||
@@ -377,6 +306,68 @@ HTML;
|
||||
return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, $action_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to generate URL for a SPA action class
|
||||
* Called when class lookup fails for controller - checks if it's a JavaScript SPA action
|
||||
*
|
||||
* @param string $class_name The class name (might be a JS SPA action)
|
||||
* @param array $params_array Parameters for URL generation
|
||||
* @return string The generated URL
|
||||
* @throws Rsx_Caller_Exception If not a valid SPA action or route not found
|
||||
*/
|
||||
protected static function _try_spa_action_route(string $class_name, array $params_array): string
|
||||
{
|
||||
// Check if this is a JavaScript class that extends Spa_Action
|
||||
try {
|
||||
$is_spa_action = Manifest::js_is_subclass_of($class_name, 'Spa_Action');
|
||||
} catch (\RuntimeException $e) {
|
||||
// Not a JS class or not found
|
||||
throw new Rsx_Caller_Exception("Class {$class_name} must extend Rsx_Controller_Abstract or Spa_Action");
|
||||
}
|
||||
|
||||
if (!$is_spa_action) {
|
||||
throw new Rsx_Caller_Exception("JavaScript class {$class_name} must extend Spa_Action to generate routes");
|
||||
}
|
||||
|
||||
// Get the file path for this JS class
|
||||
try {
|
||||
$file_path = Manifest::js_find_class($class_name);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new Rsx_Caller_Exception("SPA action class {$class_name} not found in manifest");
|
||||
}
|
||||
|
||||
// Get file metadata which contains decorator information
|
||||
try {
|
||||
$file_data = Manifest::get_file($file_path);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new Rsx_Caller_Exception("File metadata not found for SPA action {$class_name}");
|
||||
}
|
||||
|
||||
// Extract route pattern from decorators
|
||||
// JavaScript files have 'decorators' array in their metadata
|
||||
// Format: [[0 => 'route', 1 => ['/contacts']], ...]
|
||||
$route_pattern = null;
|
||||
if (isset($file_data['decorators']) && is_array($file_data['decorators'])) {
|
||||
foreach ($file_data['decorators'] as $decorator) {
|
||||
// Decorator format: [0 => 'decorator_name', 1 => [arguments]]
|
||||
if (isset($decorator[0]) && $decorator[0] === 'route') {
|
||||
// First argument is the route pattern
|
||||
if (isset($decorator[1][0])) {
|
||||
$route_pattern = $decorator[1][0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$route_pattern) {
|
||||
throw new Rsx_Caller_Exception("SPA action {$class_name} must have @route() decorator with pattern");
|
||||
}
|
||||
|
||||
// Generate URL from pattern using same logic as regular routes
|
||||
return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, '(SPA action)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL from route pattern by replacing parameters
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user