CONTROLLER(3) RSX Framework Manual CONTROLLER(3) NAME Controller - RSX request handling and routing system SYNOPSIS use App\RSpade\Core\Controller\Rsx_Controller_Abstract; class User_Controller extends Rsx_Controller_Abstract { #[Auth('Permission::authenticated()')] #[Route('/users', methods: ['GET'])] public static function index(Request $request, array $params = []) { return rsx_view('User_List'); } #[Auth('Permission::authenticated()')] #[Ajax_Endpoint] public static function get_profile(Request $request, array $params = []) { return ['name' => 'John', 'email' => 'john@example.com']; } } DESCRIPTION RSX Controllers provide a simplified approach to request handling through static methods and automatic discovery. Unlike Laravel's dependency injection heavy controllers that require constructor injection and service container resolution, RSX uses static methods that can be called from anywhere without instantiation. The framework automatically discovers controllers through the manifest system - no manual registration in route files required. Routes are defined directly on methods using attributes, keeping routing logic with the code it affects. Key differences from Laravel: - Laravel: Instance methods with dependency injection - RSX: Static methods with explicit parameters - Laravel: Routes defined in separate routes/*.php files - RSX: Routes defined via #[Route] attributes on methods - Laravel: Manual API resource controllers - RSX: Automatic JavaScript stub generation for Ajax methods - Laravel: Middleware defined in route files or constructors - RSX: pre_dispatch hooks for authentication/authorization Benefits: - No dependency injection complexity - Routes live with their handlers - Automatic Ajax/JavaScript integration - Simple static method calls - No service container overhead CREATING A CONTROLLER 1. Extend Rsx_Controller_Abstract 2. Add static methods with Request and params parameters 3. Use attributes for routing class Dashboard_Controller extends Rsx_Controller_Abstract { #[Route('/dashboard')] public static function index(Request $request, array $params = []) { return rsx_view('Dashboard'); } } ROUTE ATTRIBUTES #[Route(path, methods)] Define HTTP route. path: URL pattern with optional parameters methods: ['GET'] or ['POST'] (default both) Examples: #[Route('/users')] #[Route('/users/{id}', methods: ['GET'])] #[Route('/api/users', methods: ['POST'])] ROUTE PARAMETERS URL segments in braces become $params entries: #[Route('/users/{id}/posts/{post_id}')] public static function show(Request $request, array $params = []) { $user_id = $params['id']; $post_id = $params['post_id']; } Query parameters (GET) also added to $params: /users?sort=name $params['sort'] === 'name' POST data accessed via $request, NOT $params: $name = $request->input('name'); $email = $request->post('email'); REQUIRE ATTRIBUTE #[Auth(callable, message, redirect, redirect_to)] REQUIRED on all routes. Defines access control check. callable: 'Class::method()' string to execute message: Optional error message redirect: Optional URL to redirect on failure (HTTP only) redirect_to: Optional ['Controller', 'action'] (HTTP only) All routes MUST have at least one #[Auth] attribute, either on: - The route method itself - The controller's pre_dispatch() method (applies to all routes) - Both (pre_dispatch Require runs first, then route Require) Multiple #[Auth] attributes are supported - all must pass. Permission Method Contract: public static function method_name(Request $request, array $params, ...$args): mixed Returns: - true or null: Allow access - false: Deny access - Response: Custom response (overrides default handling) Examples: // Public access #[Auth('Permission::anybody()')] #[Route('/')] public static function index(Request $request, array $params = []) { return rsx_view('Landing'); } // Authenticated users only #[Auth('Permission::authenticated()', message: 'Please log in', redirect: '/login')] #[Route('/dashboard')] public static function dashboard(Request $request, array $params = []) { return rsx_view('Dashboard'); } // Redirect using controller/action #[Auth('Permission::authenticated()', message: 'Login required', redirect_to: ['Login_Index_Controller', 'show_login'])] #[Route('/profile')] public static function profile(Request $request, array $params = []) { return rsx_view('Profile'); } // Permission with arguments #[Auth('Permission::has_role("admin")')] #[Route('/admin')] public static function admin_panel(Request $request, array $params = []) { return rsx_view('Admin_Panel'); } // Multiple requirements #[Auth('Permission::authenticated()')] #[Auth('Permission::has_permission("edit_users")')] #[Route('/users/edit')] public static function edit_users(Request $request, array $params = []) { return rsx_view('User_Edit'); } // Controller-wide requirement class Admin_Controller extends Rsx_Controller_Abstract { #[Auth('Permission::has_role("admin")', message: 'Admin access required', redirect: '/')] public static function pre_dispatch(Request $request, array $params = []) { return null; } // All routes in this controller require admin role #[Route('/admin/users')] public static function users(Request $request, array $params = []) { return rsx_view('Admin_Users'); } } Creating Permission Methods (rsx/permission.php): class Permission extends Permission_Abstract { public static function anybody(Request $request, array $params): mixed { return true; // Always allow } public static function authenticated(Request $request, array $params): mixed { return Session::is_logged_in(); } public static function has_role(Request $request, array $params, string $role): mixed { if (!Session::is_logged_in()) { return false; } return Session::get_user()->has_role($role); } } AJAX ENDPOINTS AND REQUIRE Ajax endpoints also require #[Auth] attributes. For Ajax endpoints, redirect parameters are ignored and JSON errors returned: #[Auth('Permission::authenticated()', message: 'Login required')] #[Ajax_Endpoint] public static function get_data(Request $request, array $params = []) { return ['data' => 'value']; } On failure, returns: { "success": false, "error": "Login required", "error_type": "permission_denied" } HTTP Status: 403 Forbidden API_INTERNAL ATTRIBUTE #[Ajax_Endpoint] Makes method callable via Ajax. Generates JavaScript stub automatically. Returns JSON response. PHP: #[Ajax_Endpoint] public static function search(Request $request, array $params = []) { $query = $params['query'] ?? ''; return User::where('name', 'like', "%$query%")->get(); } JavaScript (auto-generated): const results = await User_Controller.search({query: 'john'}); JAVASCRIPT STUB GENERATION Controllers with Ajax_Endpoint methods get stubs in storage/rsx-build/js-stubs/ControllerName.js: class User_Controller { static async search(...args) { return Ajax.call(Rsx.Route('User_Controller', 'search'), args); } } Stubs included automatically in bundles. Note: Rsx.Route() generates type-safe URLs like /_ajax/User_Controller/search CALLING API METHODS From JavaScript: // Single argument const user = await User_Controller.get_user(123); // Named parameters const results = await User_Controller.search({ query: 'john', limit: 10 }); // Multiple arguments const data = await User_Controller.process(id, options); From PHP: User_Controller::get_user($request, ['id' => 123]); ROUTE RESOLUTION PHP: $url = Rsx::Route('User_Controller', 'show', ['id' => 5]); // Returns: "/users/5" if (Rsx::Route('User_Controller')->is_current()) { // Current page is users index } JavaScript: const url = Rsx.Route('User_Controller', 'show', {id: 5}); // Returns: "/users/5" PRE_DISPATCH HOOK Run before any action in controller: public static function pre_dispatch(Request $request, array $params = []) { // Check authentication if (!RsxAuth::check()) { return redirect('/login'); } // Return null to continue return null; } Return non-null to override response. RESPONSE TYPES Views: return rsx_view('View_Name', ['data' => $value]); JSON (Ajax_Endpoint): return ['key' => 'value']; // Auto-converted to JSON Redirects: return redirect('/path'); return redirect()->route('route.name'); Raw responses: return response('content', 200); return response()->json(['key' => 'value']); AUTHENTICATION Use RsxAuth in pre_dispatch: public static function pre_dispatch(Request $request, array $params = []) { if (!RsxAuth::check()) { if ($request->ajax()) { return response()->json(['error' => 'Unauthorized'], 401); } return redirect('/login'); } return null; } ERROR HANDLING Throw exceptions for errors: public static function show(Request $request, array $params = []) { $user = User::find($params['id']); if (!$user) { throw new NotFoundHttpException('User not found'); } return rsx_view('User_Profile', ['user' => $user]); } AJAX VS PAGE REQUESTS Detect Ajax requests: if ($request->ajax()) { return ['data' => $data]; // JSON } else { return rsx_view('Page', ['data' => $data]); // HTML } EXAMPLES // CRUD controller class Product_Controller extends Rsx_Controller_Abstract { #[Route('/products')] public static function index(Request $request, array $params = []) { $products = Product::paginate(20); return rsx_view('Product_List', compact('products')); } #[Route('/products/{id}')] public static function show(Request $request, array $params = []) { $product = Product::findOrFail($params['id']); return rsx_view('Product_Detail', compact('product')); } #[Ajax_Endpoint] public static function create(Request $request, array $params = []) { $product = Product::create($params); return $product; } #[Ajax_Endpoint] public static function update(Request $request, array $params = []) { $product = Product::findOrFail($params['id']); $product->update($params); return $product; } #[Ajax_Endpoint] public static function delete(Request $request, array $params = []) { Product::destroy($params['id']); return ['success' => true]; } } // JavaScript usage async function create_product(data) { const product = await Product_Controller.create(data); console.log('Created:', product); } async function update_product(id, data) { const product = await Product_Controller.update({id, ...data}); console.log('Updated:', product); } ROUTE CACHING Routes extracted from manifest and cached. Clear cache after adding routes: php artisan rsx:clean NAMING CONVENTIONS Controllers: Noun_Controller (User_Controller, Product_Controller) Actions: verb or verb_noun (index, show, create, update_profile) Routes: RESTful patterns (/users, /users/{id}) FILE ORGANIZATION rsx/app/module/ ├── module_controller.php # Main controller ├── module_api_controller.php # API endpoints └── feature/ └── module_feature_controller.php TESTING ROUTES Use rsx:debug command: php artisan rsx:debug /users php artisan rsx:debug /api/search --post='{"query":"test"}' TROUBLESHOOTING Route not found: - Check Route attribute syntax - Run php artisan rsx:routes to list all - Clear cache: php artisan rsx:clean Missing #[Auth] attribute error: - Add #[Auth('Permission::anybody()')] to route method - OR add #[Auth] to pre_dispatch() for controller-wide access - Rebuild manifest: php artisan rsx:manifest:build Permission denied (403): - Check permission method logic returns true - Verify Session::is_logged_in() for authenticated routes - Add message parameter for clearer errors - Check permission method exists in rsx/permission.php JavaScript stub missing: - Ensure Ajax_Endpoint attribute present - Ensure #[Auth] attribute present on Ajax method - Rebuild manifest: php artisan rsx:manifest:build - Check storage/rsx-build/js-stubs/ Authentication issues: - Implement pre_dispatch hook - Use Permission::authenticated() in Require - Verify session configuration SEE ALSO manifest_api(3), bundle_api(3), jqhtml(3), rsx:routes(1) RSX Framework 2025-09-17 CONTROLLER(3)