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:
root
2025-11-19 17:48:15 +00:00
parent 77b4d10af8
commit 9ebcc359ae
4360 changed files with 37751 additions and 18578 deletions

View File

@@ -122,7 +122,7 @@ class Dispatcher
console_debug('DISPATCH', 'Matched default route pattern:', $controller_name, '::', $action_name);
// Try to find the controller using manifest
// First try to find as PHP controller
try {
$metadata = Manifest::php_get_metadata_by_class($controller_name);
$controller_fqcn = $metadata['fqcn'];
@@ -173,7 +173,56 @@ class Dispatcher
return redirect($proper_url, 302);
}
} catch (\RuntimeException $e) {
console_debug('DISPATCH', 'Controller not found in manifest:', $controller_name);
console_debug('DISPATCH', 'Not a PHP controller, checking if SPA action:', $controller_name);
// Not found as PHP controller - check if it's a SPA action
try {
$is_spa_action = Manifest::js_is_subclass_of($controller_name, 'Spa_Action');
if ($is_spa_action) {
console_debug('DISPATCH', 'Found SPA action class:', $controller_name);
// Get the file path for this JS class
$file_path = Manifest::js_find_class($controller_name);
// Get file metadata which contains decorator information
$file_data = Manifest::get_file($file_path);
if (!$file_data) {
console_debug('DISPATCH', 'SPA action metadata not found:', $controller_name);
return null;
}
// Extract route pattern from @route() decorator
// Format: [[0 => 'route', 1 => ['/contacts']], ...]
$route_pattern = null;
if (isset($file_data['decorators']) && is_array($file_data['decorators'])) {
foreach ($file_data['decorators'] as $decorator) {
if (isset($decorator[0]) && $decorator[0] === 'route') {
if (isset($decorator[1][0])) {
$route_pattern = $decorator[1][0];
break;
}
}
}
}
if ($route_pattern) {
// Generate proper URL for the SPA action
$params = array_merge($extra_params, $request->query->all());
$proper_url = Rsx::Route($controller_name, $action_name, $params);
console_debug('DISPATCH', 'Redirecting to SPA action route:', $proper_url);
return redirect($proper_url, 302);
} else {
console_debug('DISPATCH', 'SPA action missing @route() decorator:', $controller_name);
}
}
} catch (\RuntimeException $spa_e) {
console_debug('DISPATCH', 'Not a SPA action either:', $controller_name);
}
return null;
}
}
@@ -246,7 +295,8 @@ class Dispatcher
Debugger::console_debug('DISPATCH', 'Matched route to ' . $handler_class . '::' . $handler_method . ' params: ' . json_encode($params));
// Set current controller and action in Rsx for tracking
\App\RSpade\Core\Rsx::_set_current_controller_action($handler_class, $handler_method, $params);
$route_type = $route_match['type'] ?? 'standard';
\App\RSpade\Core\Rsx::_set_current_controller_action($handler_class, $handler_method, $params, $route_type);
// Load and validate handler class
static::__load_handler_class($handler_class);
@@ -339,93 +389,78 @@ class Dispatcher
if (empty($routes)) {
\Log::debug('Manifest::get_routes() returned empty array');
console_debug('DISPATCH', 'Warning: got 0 routes from Manifest::get_routes()');
} else {
\Log::debug('Manifest has ' . count($routes) . ' route types');
// Log details for debugging but don't output to console
foreach ($routes as $type => $type_routes) {
\Log::debug("Route type '$type' has " . count($type_routes) . ' patterns');
// Show first few patterns for debugging in logs only
$patterns = array_slice(array_keys($type_routes), 0, 5);
\Log::debug(' First patterns: ' . implode(', ', $patterns));
}
return null;
}
// Sort handler types by priority
$sorted_types = array_keys(static::$handler_priorities);
usort($sorted_types, function ($a, $b) {
return static::$handler_priorities[$a] - static::$handler_priorities[$b];
});
\Log::debug('Manifest has ' . count($routes) . ' routes');
// Collect all matching routes
$matches = [];
// Get all patterns and sort by priority
$patterns = array_keys($routes);
$patterns = RouteResolver::sort_by_priority($patterns);
// Try each handler type in priority order
foreach ($sorted_types as $type) {
if (!isset($routes[$type])) {
// Try to match each pattern
foreach ($patterns as $pattern) {
$route = $routes[$pattern];
// Check if HTTP method is supported
if (!in_array($method, $route['methods'])) {
continue;
}
$type_routes = $routes[$type];
// Try to match the URL
$params = RouteResolver::match_with_query($url, $pattern);
// Get all patterns for this type
$patterns = array_keys($type_routes);
if ($params !== false) {
// Found a match - verify the method has the required attribute
$class_fqcn = $route['class'];
$method_name = $route['method'];
// Sort patterns by priority
$patterns = RouteResolver::sort_by_priority($patterns);
// Get method metadata from manifest
$class_metadata = Manifest::php_get_metadata_by_fqcn($class_fqcn);
$method_metadata = $class_metadata['public_static_methods'][$method_name] ?? null;
// Try to match each pattern
foreach ($patterns as $pattern) {
$route_info = $type_routes[$pattern];
if (!$method_metadata) {
throw new \RuntimeException(
"Route method not found in manifest: {$class_fqcn}::{$method_name}\n" .
"Pattern: {$pattern}"
);
}
// Check if method is supported
if (isset($route_info[$method])) {
// Try to match the URL
$params = RouteResolver::match_with_query($url, $pattern);
// Check for Route or SPA attribute
$attributes = $method_metadata['attributes'] ?? [];
$has_route = false;
if ($params !== false) {
// Handle new structure where each method can have multiple handlers
$handlers = $route_info[$method];
// If it's not an array of handlers, convert it (backwards compatibility)
if (!isset($handlers[0])) {
$handlers = [$handlers];
}
// Add all matching handlers
foreach ($handlers as $handler) {
$matches[] = [
'type' => $type,
'pattern' => $pattern,
'class' => $handler['class'],
'method' => $handler['method'],
'params' => $params,
'file' => $handler['file'] ?? null,
'require' => $handler['require'] ?? [],
];
}
foreach ($attributes as $attr_name => $attr_instances) {
if (str_ends_with($attr_name, '\\Route') || $attr_name === 'Route' ||
str_ends_with($attr_name, '\\SPA') || $attr_name === 'SPA') {
$has_route = true;
break;
}
}
}
}
// Check for duplicate routes
if (count($matches) > 1) {
$error_msg = "Multiple routes match the request '{$method} {$url}':\n\n";
foreach ($matches as $match) {
$error_msg .= " - Pattern: {$match['pattern']}\n";
$error_msg .= " Class: {$match['class']}::{$match['method']}\n";
if (!empty($match['file'])) {
$error_msg .= " File: {$match['file']}\n";
if (!$has_route) {
throw new \RuntimeException(
"Route method {$class_fqcn}::{$method_name} is missing required #[Route] or #[SPA] attribute.\n" .
"Pattern: {$pattern}\n" .
"File: {$route['file']}"
);
}
$error_msg .= " Type: {$match['type']}\n\n";
}
$error_msg .= 'Routes must be unique. Please remove duplicate route definitions.';
throw new RuntimeException($error_msg);
// Return route with params
return [
'type' => $route['type'],
'pattern' => $pattern,
'class' => $route['class'],
'method' => $route['method'],
'params' => $params,
'file' => $route['file'] ?? null,
'require' => $route['require'] ?? [],
];
}
}
// Return the single match or null
return $matches[0] ?? null;
// No match found
return null;
}
/**
@@ -572,8 +607,9 @@ class Dispatcher
throw new Exception("Method not public: {$class_name}::{$method_name}");
}
// Set current controller and action for tracking
Rsx::_set_current_controller_action($class_name, $method_name, $params);
// NOTE: Do NOT call _set_current_controller_action here - it's already been set
// earlier in the dispatch flow with the correct route type. Calling it again
// would overwrite the route type with null.
// Check if this is a controller (all methods are static)
if (static::__is_controller($class_name)) {
@@ -1096,7 +1132,11 @@ class Dispatcher
"Auth redirect_to must be array ['Controller', 'action'] on {$handler_class}::{$handler_method}"
);
}
$url = Rsx::Route($redirect_to[0], $redirect_to[1] ?? 'index');
$action = $redirect_to[0];
if (isset($redirect_to[1]) && $redirect_to[1] !== 'index') {
$action .= '::' . $redirect_to[1];
}
$url = Rsx::Route($action);
if ($message) {
Rsx::flash_error($message);
}