Move small tasks from wishlist to todo, update npm packages Replace #[Auth] attributes with manual auth checks and code quality rule Remove on_jqhtml_ready lifecycle method from framework Complete ACL system with 100-based role indexing and /dev/acl tester WIP: ACL system implementation with debug instrumentation Convert rsx:check JS linting to RPC socket server Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature Reorganize wishlists: priority order, mark sublayouts complete, add email Update model_fetch docs: mark MVP complete, fix enum docs, reorganize Comprehensive documentation overhaul: clarity, compression, and critical rules Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null() Add JS ORM relationship lazy-loading and fetch array handling Add JS ORM relationship fetching and CRUD documentation Fix ORM hydration and add IDE resolution for Base_* model stubs Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework Enhance JS ORM infrastructure and add Json_Tree class name badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
446 lines
14 KiB
Plaintext
Executable File
446 lines
14 KiB
Plaintext
Executable File
CONTROLLER(3) RSX Framework Manual CONTROLLER(3)
|
|
|
|
NAME
|
|
Controller - RSX request handling and routing system
|
|
|
|
SYNOPSIS
|
|
use App\RSpade\Core\Controller\Rsx_Controller_Abstract;
|
|
use App\RSpade\Core\Session\Session;
|
|
|
|
class User_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response_unauthorized();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#[Route('/users', methods: ['GET'])]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view('User_List');
|
|
}
|
|
|
|
#[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');
|
|
|
|
AUTHENTICATION
|
|
|
|
RSX uses manual authentication checks rather than declarative attributes.
|
|
Auth checks are placed directly in controller code for visibility.
|
|
|
|
A code quality rule (PHP-AUTH-01) verifies all endpoints have auth checks,
|
|
either in the method body or controller's pre_dispatch().
|
|
|
|
Controller-Wide Authentication (Recommended):
|
|
|
|
class Dashboard_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response_unauthorized();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#[Route('/dashboard')]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
// Auth checked in pre_dispatch
|
|
return rsx_view('Dashboard');
|
|
}
|
|
}
|
|
|
|
Method-Level Authentication:
|
|
|
|
#[Route('/admin')]
|
|
public static function admin_panel(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
return response_unauthorized();
|
|
}
|
|
if (!Permission::has_role(User_Model::ROLE_ADMIN)) {
|
|
return response_unauthorized('Admin access required');
|
|
}
|
|
return rsx_view('Admin_Panel');
|
|
}
|
|
|
|
Response Helpers:
|
|
|
|
response_unauthorized(?string $message = null)
|
|
Context-aware: JSON for Ajax, redirect/403 for web
|
|
|
|
response_not_found(?string $message = null)
|
|
Context-aware: JSON for Ajax, 404 page for web
|
|
|
|
Public Endpoints:
|
|
|
|
Mark public endpoints with @auth-exempt in class docblock:
|
|
|
|
/**
|
|
* @auth-exempt Public landing page
|
|
*/
|
|
class Landing_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[Route('/')]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view('Landing');
|
|
}
|
|
}
|
|
|
|
Permission Helpers (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 AUTHENTICATION
|
|
Ajax endpoints use the same auth pattern as routes.
|
|
The pre_dispatch() check covers all methods in the controller:
|
|
|
|
#[Ajax_Endpoint]
|
|
public static function get_data(Request $request, array $params = [])
|
|
{
|
|
// Auth checked in pre_dispatch
|
|
return ['data' => 'value'];
|
|
}
|
|
|
|
On response_unauthorized(), Ajax returns:
|
|
{
|
|
"success": false,
|
|
"error_code": "unauthorized"
|
|
}
|
|
|
|
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 (!Session::is_logged_in()) {
|
|
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 Session:: in pre_dispatch:
|
|
|
|
public static function pre_dispatch(Request $request, array $params = [])
|
|
{
|
|
if (!Session::is_logged_in()) {
|
|
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
|
|
|
|
PHP-AUTH-01 code quality error:
|
|
- Add auth check to pre_dispatch() (recommended)
|
|
- OR add auth check at start of method
|
|
- OR add @auth-exempt comment for public endpoints
|
|
- Run: php artisan rsx:check
|
|
|
|
Permission denied / Unauthorized:
|
|
- Check Session::is_logged_in() returns true
|
|
- Verify Permission helpers in rsx/permission.php
|
|
- Check user has required role/permission
|
|
|
|
JavaScript stub missing:
|
|
- Ensure Ajax_Endpoint attribute present
|
|
- Rebuild manifest: php artisan rsx:manifest:build
|
|
- Check storage/rsx-build/js-stubs/
|
|
|
|
Authentication issues:
|
|
- Implement pre_dispatch() with Session::is_logged_in() check
|
|
- Return response_unauthorized() on failure
|
|
- Verify session configuration
|
|
|
|
SEE ALSO
|
|
manifest_api(3), bundle_api(3), jqhtml(3), rsx:routes(1)
|
|
|
|
RSX Framework 2025-09-17 CONTROLLER(3) |