Fix contact add link to use Contacts_Edit_Action SPA route
Add multi-route support for controllers and SPA actions Add screenshot feature to rsx:debug and convert contacts edit to SPA 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -260,22 +260,11 @@ class Rsx
|
||||
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;
|
||||
// Check for Ajax_Endpoint attribute
|
||||
$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;
|
||||
@@ -293,17 +282,29 @@ class Rsx
|
||||
return $ajax_url;
|
||||
}
|
||||
|
||||
if (!$has_route) {
|
||||
// Not a controller method with Route/Ajax - check if it's a SPA action class
|
||||
// Look up routes in manifest using routes_by_target
|
||||
$target = $class_name . '::' . $action_name;
|
||||
$manifest = Manifest::get_full_manifest();
|
||||
|
||||
if (!isset($manifest['data']['routes_by_target'][$target])) {
|
||||
// Not a controller method with Route - check if it's a SPA action class
|
||||
return static::_try_spa_action_route($class_name, $params_array);
|
||||
}
|
||||
|
||||
if (!$route_pattern) {
|
||||
throw new Rsx_Caller_Exception("Route attribute on {$class_name}::{$action_name} must have a pattern");
|
||||
$routes = $manifest['data']['routes_by_target'][$target];
|
||||
|
||||
// Select best matching route based on provided parameters
|
||||
$selected_route = static::_select_best_route($routes, $params_array);
|
||||
|
||||
if (!$selected_route) {
|
||||
throw new Rsx_Caller_Exception(
|
||||
"No suitable route found for {$class_name}::{$action_name} with provided parameters. " .
|
||||
"Available routes: " . implode(', ', array_column($routes, 'pattern'))
|
||||
);
|
||||
}
|
||||
|
||||
// Generate URL from pattern
|
||||
return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, $action_name);
|
||||
// Generate URL from selected pattern
|
||||
return static::_generate_url_from_pattern($selected_route['pattern'], $params_array, $class_name, $action_name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,43 +330,82 @@ class Rsx
|
||||
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");
|
||||
// Look up routes in manifest using routes_by_target
|
||||
$manifest = Manifest::get_full_manifest();
|
||||
|
||||
if (!isset($manifest['data']['routes_by_target'][$class_name])) {
|
||||
throw new Rsx_Caller_Exception("SPA action {$class_name} has no registered routes 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}");
|
||||
$routes = $manifest['data']['routes_by_target'][$class_name];
|
||||
|
||||
// Select best matching route based on provided parameters
|
||||
$selected_route = static::_select_best_route($routes, $params_array);
|
||||
|
||||
if (!$selected_route) {
|
||||
throw new Rsx_Caller_Exception(
|
||||
"No suitable route found for SPA action {$class_name} with provided parameters. " .
|
||||
"Available routes: " . implode(', ', array_column($routes, 'pattern'))
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Generate URL from selected pattern
|
||||
return static::_generate_url_from_pattern($selected_route['pattern'], $params_array, $class_name, '(SPA action)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the best matching route from available routes based on provided parameters
|
||||
*
|
||||
* Selection algorithm:
|
||||
* 1. Filter routes where all required parameters can be satisfied by provided params
|
||||
* 2. Among satisfiable routes, prioritize those with MORE parameters (more specific)
|
||||
* 3. If tie, any route works (deterministic by using first match)
|
||||
*
|
||||
* @param array $routes Array of route data from manifest
|
||||
* @param array $params_array Provided parameters
|
||||
* @return array|null Selected route data or null if none match
|
||||
*/
|
||||
protected static function _select_best_route(array $routes, array $params_array): ?array
|
||||
{
|
||||
$satisfiable = [];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$pattern = $route['pattern'];
|
||||
|
||||
// Extract required parameters from pattern
|
||||
$required_params = [];
|
||||
if (preg_match_all('/:([a-zA-Z_][a-zA-Z0-9_]*)/', $pattern, $matches)) {
|
||||
$required_params = $matches[1];
|
||||
}
|
||||
|
||||
// Check if all required parameters are provided
|
||||
$can_satisfy = true;
|
||||
foreach ($required_params as $required) {
|
||||
if (!array_key_exists($required, $params_array)) {
|
||||
$can_satisfy = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($can_satisfy) {
|
||||
$satisfiable[] = [
|
||||
'route' => $route,
|
||||
'param_count' => count($required_params),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$route_pattern) {
|
||||
throw new Rsx_Caller_Exception("SPA action {$class_name} must have @route() decorator with pattern");
|
||||
if (empty($satisfiable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate URL from pattern using same logic as regular routes
|
||||
return static::_generate_url_from_pattern($route_pattern, $params_array, $class_name, '(SPA action)');
|
||||
// Sort by parameter count descending (most parameters first)
|
||||
usort($satisfiable, function ($a, $b) {
|
||||
return $b['param_count'] <=> $a['param_count'];
|
||||
});
|
||||
|
||||
// Return the route with the most parameters
|
||||
return $satisfiable[0]['route'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user