Files
rspade_system/docs/CLAUDE.dist.md
root 9ebcc359ae 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>
2025-11-19 17:48:15 +00:00

33 KiB

RSpade Framework - AI/LLM Development Guide

PURPOSE: Essential directives for AI/LLM assistants developing RSX applications with RSpade.

What is RSpade?

Visual Basic-like development for PHP/Laravel. Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.

Philosophy: Modern anti-modernization. While JavaScript fragments into complexity and React demands quarterly paradigm shifts, RSpade asks: "What if coding was easy again?" Business apps need quick builds and easy maintenance, not bleeding-edge architecture.

Important: RSpade is built on Laravel but diverges significantly. Do not assume Laravel patterns work in RSX without verification.

Terminology: RSpade = Complete framework | RSX = Your application code in /rsx/


CRITICAL RULES

🔴 RSpade Builds Automatically - NEVER RUN BUILD COMMANDS

RSpade is an INTERPRETED framework - like Python or PHP, changes are automatically detected and compiled on-the-fly. There is NO manual build step.

ABSOLUTELY FORBIDDEN (unless explicitly instructed):

  • npm run compile / npm run build - DO NOT EXIST
  • bin/publish - Creates releases for OTHER developers (not for testing YOUR changes)
  • rsx:bundle:compile - Bundles compile automatically in dev mode
  • rsx:manifest:build - Manifest rebuilds automatically in dev mode
  • ANY command with "build", "compile", or "publish"

How it works:

  1. Edit JS/SCSS/PHP files
  2. Refresh browser
  3. Changes are live (< 1 second)

If you find yourself wanting to run build commands: STOP. You're doing something wrong. Changes are already live.

🔴 Framework Updates

php artisan rsx:framework:pull  # 5-minute timeout required

Updates take 2-5 minutes. Includes code pull, manifest rebuild, bundle recompilation. For AI: Always use 5-minute timeout.

🔴 Fail Loud - No Silent Fallbacks

ALWAYS fail visibly. No redundant fallbacks, silent failures, or alternative code paths.

// ❌ CATASTROPHIC
try { $clean = Sanitizer::sanitize($input); }
catch (Exception $e) { $clean = $input; }  // DISASTER

// ✅ CORRECT
$clean = Sanitizer::sanitize($input);  // Let it throw

SECURITY-CRITICAL: If sanitization/validation/auth fails, NEVER continue. Always throw immediately.

NO BLANKET TRY/CATCH: Use try/catch only for expected failures (file uploads, external APIs, user input parsing). NEVER wrap database operations or entire functions "just in case".

// ❌ WRONG - Defensive "on error resume"
try {
    $user->save();
    $result = process_data($user);
    return $result;
} catch (Exception $e) {
    throw new Exception("Failed: " . $e->getMessage(), 0, $e);
}

// ✅ CORRECT - Let exceptions bubble
$user->save();
$result = process_data($user);
return $result;

Exception handlers format errors for different contexts (Ajax JSON, CLI, HTML). Don't wrap exceptions with generic messages - let them bubble to the global handler.

🔴 No Defensive Coding

Core classes ALWAYS exist. Never check.

// ❌ BAD: if (typeof Rsx !== 'undefined') { Rsx.Route('Controller::method') }
// ✅ GOOD: Rsx.Route('Controller::method')

🔴 Static-First Philosophy

Classes are namespacing tools. Use static unless instances needed (models, resources). Avoid dependency injection.

🔴 Git Workflow - Framework is READ-ONLY

NEVER modify /var/www/html/system/ - It's like node_modules or the Linux kernel.

  • App repo: /var/www/html/.git (you control)
  • Framework: /var/www/html/system/ (submodule, don't touch)
  • Your code: /var/www/html/rsx/ (all changes here)

Commit discipline: ONLY commit when explicitly asked. Commits are milestones, not individual changes.

🔴 DO NOT RUN rsx:clean

RSpade's cache auto-invalidates on file changes. Running rsx:clean causes 30-60 second rebuilds with zero benefit.

When to use: Only on catastrophic corruption, after framework updates (automatic), or when explicitly instructed.

Correct workflow: Edit → Save → Reload browser → See changes (< 1 second)

🔴 Trust Code Quality Rules

Each rsx:check rule has remediation text that tells AI assistants exactly what to do:

  • Some rules say "fix immediately"
  • Some rules say "present options and wait for decision"

AI should follow the rule's guidance precisely. Rules are deliberately written and well-reasoned.


NAMING CONVENTIONS

Enforced by rsx:check:

Context Convention Example
PHP Methods/Variables underscore_case user_name
PHP Classes Like_This User_Controller
JavaScript Classes Like_This User_Card
Files lowercase_underscore user_controller.php
Database Tables lowercase_plural users
Constants UPPERCASE MAX_SIZE

File Prefix Grouping

Files sharing a common prefix are a related set. When renaming, maintain the grouping across ALL files with that prefix.

Example - rsx/app/frontend/calendar/:

frontend_calendar_event.scss
frontend_calendar_event_controller.php
frontend_calendar_event.blade.php
frontend_calendar_event.js

Critical: Never create same-name different-case files (e.g., user.php and User.php).


DIRECTORY STRUCTURE

/var/www/html/
├── rsx/                        # YOUR CODE
│   ├── app/                    # Modules
│   ├── models/                 # Database models
│   ├── public/                 # Static files (web-accessible)
│   ├── resource/               # Framework-ignored
│   └── theme/                  # Global assets
└── system/                     # FRAMEWORK (read-only)

Special Directories (Path-Agnostic)

resource/ - ANY directory named this is framework-ignored. Store helpers, docs, third-party code. Exception: /rsx/resource/config/ IS processed.

public/ - ANY directory named this is web-accessible, framework-ignored. 5min cache, 30d with ?v=.

Path-Agnostic Loading

Classes found by name, not path. No imports needed.

$user = User_Model::find(1);  // Framework finds it
// NOT: use Rsx\Models\User_Model;  // Auto-generated

CONFIGURATION

Two-tier system:

  • Framework: /system/config/rsx.php (never modify)
  • User: /rsx/resource/config/rsx.php (your overrides)

Merged via array_merge_deep(). Common overrides: development.auto_rename_files, bundle_aliases, console_debug.


ROUTING & CONTROLLERS

class Frontend_Controller extends Rsx_Controller_Abstract
{
    #[Auth('Permission::anybody()')]
    #[Route('/', methods: ['GET'])]
    public static function index(Request $request, array $params = [])
    {
        return rsx_view('Frontend_Index', [
            'bundle' => Frontend_Bundle::render()
        ]);
    }
}

Rules: Only GET/POST allowed. Use :param syntax. All routes MUST have #[Auth].

#[Auth] Attribute

#[Auth('Permission::anybody()')]           // Public
#[Auth('Permission::authenticated()')]     // Require login
#[Auth('Permission::has_role("admin")')]   // Custom

Controller-wide: Add to pre_dispatch(). Multiple attributes = all must pass.

Type-Safe URLs

MANDATORY: All URLs must be generated using Rsx::Route() - hardcoded URLs are forbidden.

// PHP - Controller (defaults to 'index' method)
Rsx::Route('User_Controller')

// PHP - Controller with explicit method
Rsx::Route('User_Controller::show', 123);

// PHP - With query parameters
Rsx::Route('Login_Controller::logout', ['redirect' => '/dashboard']);
// Generates: /logout?redirect=%2Fdashboard

// JavaScript (identical syntax)
Rsx.Route('User_Controller')
Rsx.Route('User_Controller::show', 123);
Rsx.Route('Login_Controller::logout', {redirect: '/dashboard'});

Signature: Rsx::Route($action, $params = null) / Rsx.Route(action, params = null)

  • $action - Controller class, SPA action, or "Class::method" (defaults to 'index' if no :: present)
  • $params - Integer sets 'id', array/object provides named params

Query Parameters: Extra params become query string - automatically URL-encoded

Enforcement: rsx:check will flag hardcoded URLs like /login or /logout?redirect=... and require you to use Rsx::Route(). Do it right the first time to avoid rework.


SPA (SINGLE PAGE APPLICATION) ROUTING

Client-side routing for authenticated application areas. One PHP bootstrap controller, multiple JavaScript actions that navigate without page reloads.

SPA Components

1. PHP Bootstrap Controller (ONE per feature/bundle)

class Frontend_Spa_Controller extends Rsx_Controller_Abstract {
    #[SPA]
    #[Auth('Permission::authenticated()')]
    public static function index(Request $request, array $params = []) {
        return rsx_view(SPA);
    }
}

CRITICAL: One #[SPA] per feature/bundle (e.g., /app/frontend, /app/root, /app/login). Bundles separate features to save bandwidth, reduce processing time, and segregate confidential code (e.g., root admin from unauthorized users). The #[SPA] bootstrap performs server-side auth checks with failure/redirect before loading client-side actions. Typically one #[SPA] per feature at rsx/app/(feature)/(feature)_spa_controller::index.

2. JavaScript Actions (MANY)

@route('/contacts')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Contacts')  // Optional browser title
class Contacts_Index_Action extends Spa_Action {
    async on_load() {
        this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
    }
}

3. Layout

class Frontend_Layout extends Spa_Layout {
    on_action(url, action_name, args) {
        // Called after action created, before on_ready
        // Access this.action immediately
        this.update_navigation(url);
    }
}

Layout template must have $id="content" element where actions render.

URL Generation & Navigation

// PHP/JavaScript - same syntax
Rsx::Route('Contacts_Index_Action')  // /contacts
Rsx::Route('Contacts_View_Action', 123)  // /contacts/123
Spa.dispatch('/contacts/123');  // Programmatic navigation
Spa.layout   // Current layout instance
Spa.action   // Current action instance

URL Parameters

// URL: /contacts/123?tab=history
@route('/contacts/:id')
class Contacts_View_Action extends Spa_Action {
    on_create() {
        console.log(this.args.id);   // "123" (route param)
        console.log(this.args.tab);  // "history" (query param)
    }
}

File Organization

/rsx/app/frontend/
├── Frontend_Spa_Controller.php         # Single SPA bootstrap
├── Frontend_Layout.js
├── Frontend_Layout.jqhtml
└── contacts/
    ├── frontend_contacts_controller.php    # Ajax endpoints only
    ├── Contacts_Index_Action.js            # /contacts
    ├── Contacts_Index_Action.jqhtml
    ├── Contacts_View_Action.js             # /contacts/:id
    └── Contacts_View_Action.jqhtml

Use SPA for: Authenticated areas, dashboards, admin panels Avoid for: Public pages (SEO needed), simple static pages

Details: php artisan rsx:man spa


CONVERTING BLADE PAGES TO SPA ACTIONS

7-step procedure to convert server-side pages to client-side SPA:

1. Create Action Files

# In feature directory
touch Feature_Index_Action.js Feature_Index_Action.jqhtml

2. Create Action Class (.js)

@route('/path')
@layout('Frontend_Spa_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Page Title')
class Feature_Index_Action extends Spa_Action {
    full_width = true;  // For DataGrid pages
    async on_load() {
        this.data.items = await Feature_Controller.fetch_items();
    }
}

3. Convert Template (.jqhtml)

Blade → jqhtml syntax:

  • {{ $var }}<%= this.data.var %>
  • {!! $html !!}<%!= this.data.html %>
  • @if($cond)<% if (this.data.cond) { %>
  • @foreach($items as $item)<% for (let item of this.data.items) { %>
  • @endforeach<% } %>
  • {{-- comment --}}<%-- comment --%>
  • {{ Rsx::Route('Class') }}<%= Rsx.Route('Class') %>
<Define:Feature_Index_Action>
    <Page>
        <Page_Header><Page_Title>Title</Page_Title></Page_Header>
        <% for (let item of this.data.items) { %>
            <div><%= item.name %></div>
        <% } %>
    </Page>
</Define:Feature_Index_Action>

4. Update Controller

// Change #[Route] to #[SPA]
#[SPA]
public static function index(Request $request, array $params = []) {
    return rsx_view(SPA);
}

// Add Ajax endpoints
#[Ajax_Endpoint]
public static function fetch_items(Request $request, array $params = []): array {
    return ['items' => Feature_Model::all()];
}

5. Update Route References

Search entire codebase for old route references:

grep -r "Feature_Controller::method" rsx/app/

Find/replace in all files:

  • Rsx::Route('Feature_Controller::method')Rsx::Route('Feature_Action')
  • Rsx.Route('Feature_Controller::method')Rsx.Route('Feature_Action')
  • Hardcoded /feature/method/123Rsx.Route('Feature_Action', 123)

Check: DataGrids, dashboards, save endpoints, navigation, breadcrumbs

6. Archive Old Files

mkdir -p rsx/resource/archive/frontend/feature/
mv feature_index.blade.php rsx/resource/archive/frontend/feature/

7. Test

php artisan rsx:debug /path

Verify: No JS errors, page renders, data loads.

Common Patterns:

DataGrid (no data loading):

class Items_Index_Action extends Spa_Action {
    full_width = true;
    async on_load() {} // DataGrid loads own data
}

Detail page (load data):

@route('/items/:id')
class Items_View_Action extends Spa_Action {
    async on_load() {
        this.data.item = await Items_Controller.get({id: this.args.id});
    }
}

BLADE & VIEWS

@rsx_id('Frontend_Index')  {{-- Every view starts with this --}}

<body class="{{ rsx_body_class() }}">  {{-- Adds view class --}}

@rsx_include('Component_Name')  {{-- Include by name, not path --}}

NO inline styles, scripts, or event handlers in Blade views:

  • No <style> tags
  • No <script> tags
  • No inline event handlers: onclick="", onchange="", etc. are forbidden
  • Use companion .scss and .js files instead
  • Exception: jqhtml templates may use @click directive syntax (jqhtml-specific feature)

Why no inline handlers:

  • Violates separation of concerns (HTML structure vs behavior)
  • Makes code harder to maintain and test
  • rsx:check will flag inline event handlers and require refactoring

Correct pattern:

// Blade view - NO event handlers
<button id="submit-btn" class="btn btn-primary">Submit</button>

// Companion .js file
$('#submit-btn').click(() => {
    // Handle click
});

SCSS Pairing

/* frontend_index.scss - Same directory as view */
.Frontend_Index {  /* Matches @rsx_id */
    .content { padding: 20px; }
}

Passing Data to JavaScript

Use @rsx_page_data for page-specific data needed by JavaScript (IDs, config, etc.):

@rsx_page_data(['user_id' => $user->id, 'mode' => 'edit'])

@section('content')
  <!-- Your HTML -->
@endsection

Access in JavaScript:

const user_id = window.rsxapp.page_data.user_id;

Use when data doesn't belong in DOM attributes. Multiple calls merge together.


JAVASCRIPT

Auto-Initialization

class Frontend_Index {
    static async on_app_ready() {
        // DOM ready
    }

    static async on_jqhtml_ready() {
        // Components ready
    }
}

CRITICAL: JavaScript only executes when bundle rendered.


BUNDLE SYSTEM

One bundle per page required.

class Frontend_Bundle extends Rsx_Bundle_Abstract
{
    public static function define(): array
    {
        return [
            'include' => [
                'jquery',                    // Required
                'lodash',                    // Required
                'rsx/theme/variables.scss',  // Order matters
                'rsx/app/frontend',          // Directory
                'rsx/models',                // For JS stubs
                '/public/vendor/css/core.css', // Public directory asset (filemtime cache-busting)
            ],
        ];
    }
}

Bundles support /public/ prefix for including static assets from public directories with automatic cache-busting.

Auto-compiles on page reload in development.

<head>
    {!! Frontend_Bundle::render() !!}
</head>

JQHTML COMPONENTS

Philosophy

For mechanical thinkers who see structure, not visuals. Write <User_Card> not <div class="card">. Name what things ARE.

Template Syntax

🔴 CRITICAL: <Define> IS the element, not a wrapper

<!-- ✅ CORRECT - Define becomes button -->
<Define:Save_Button tag="button" class="btn btn-primary">
    Save
</Define:Save_Button>

<!-- Renders as: -->
<button class="Save_Button Component btn btn-primary">Save</button>

Interpolation: <%= escaped %> | <%!= unescaped %> | <% javascript %>

Conditional Attributes (v2.2.162+): Apply attributes conditionally using <% if (condition) { %>attr="value"<% } %> directly in attribute context. Works with static values, interpolations, and multiple conditions per element. Example: <input <% if (this.args.required) { %>required="required"<% } %> />

🔴 State Management Rules (ENFORCED)

this.args - Component arguments (read-only in on_load(), modifiable elsewhere) this.data - Ajax-loaded data (writable ONLY in on_create() and on_load()) this.state - Arbitrary component state (modifiable anytime)

Quick Guide:

  • Loading from API? Use this.data in on_load()
  • Need to reload with new params? Modify this.args, call reload()
  • UI state (toggles, counters, etc)? Use this.state
// WITH Ajax data
class Users_List extends Component {
    on_create() {
        this.data.users = [];  // Defaults
    }
    async on_load() {
        this.data.users = await User_Controller.fetch({filter: this.args.filter});
    }
    on_ready() {
        // Change filter → reload
        this.args.filter = 'new';
        this.reload();
    }
}

// WITHOUT Ajax data
class Toggle_Button extends Component {
    on_create() {
        this.state = {is_open: false};
    }
    on_ready() {
        this.$.on('click', () => {
            this.state.is_open = !this.state.is_open;
        });
    }
}

on_load() restrictions (enforced):

  • Read this.args, write this.data
  • NO DOM access, NO this.state, NO modifying this.args

Lifecycle

  1. on_create() → Setup default state BEFORE template (sync)
  2. render → Template executes with initialized state
  3. on_render() → Hide uninitialized UI (sync)
  4. on_load() → Fetch data into this.data (async)
  5. on_ready() → DOM manipulation safe (async)

on_create() now runs first - Initialize this.data properties here so templates can safely reference them:

on_create() {
    this.data.rows = [];      // Prevents "not iterable" errors
    this.data.loading = true; // Template can check loading state
}

Double-render: If on_load() modifies this.data, component renders twice (defaults → populated).

Loading Pattern

async on_load() {
    const result = await Product_Controller.list({page: 1});
    this.data.products = result.products;
    this.data.loaded = true;  // Simple flag at END
}
<% if (!this.data.loaded) { %>
    Loading...
<% } else { %>
    <!-- Show data -->
<% } %>

NEVER call this.render() in on_load() - automatic re-render happens.

Attributes

  • $quoted="string" → String literal
  • $unquoted=expression → JavaScript expression
  • $id="name" → Scoped element ID

Component Access

this.$id(name) → jQuery object (for DOM):

this.$id('button').on('click', ...);

this.id(name) → Component instance (for methods):

const comp = this.id('child');  // ✅ Returns component
await comp.reload();

const comp = this.id('child').component();  // ❌ WRONG

Incremental Scaffolding

Undefined components work immediately - they render as div with the component name as a class.

<Dashboard>
  <Stats_Panel />
  <Recent_Activity />
</Dashboard>

Common Pitfalls

  1. <Define> IS the element - use tag="" attribute
  2. this.data starts empty {}, set defaults in on_create()
  3. ONLY modify this.data in on_create() and on_load() (enforced)
  4. on_load() can ONLY access this.args and this.data (enforced)
  5. Use this.state = {} in on_create() for component state (not loaded from Ajax)
  6. Use this.args for reload parameters, reload() to re-fetch
  7. Use Controller.method() not $.ajax()
  8. Blade components self-closing only
  9. on_create/render/stop must be sync
  10. Use this.id() for components, NOT this.id().component()

Bundle Integration Required

{!! Frontend_Bundle::render() !!}  {{-- Required for JS --}}
<User_Card user_id="123" />       {{-- Now JS executes --}}

For advanced topics: php artisan rsx:man jqhtml


FORM COMPONENTS

Form fields (<Rsx_Form> with $data, $controller, $method):

<Rsx_Form $data="{!! json_encode($form_data) !!}" $controller="Controller" $method="save">
  <Form_Field $name="email" $label="Email" $required=true>
    <Text_Input $type="email" />
  </Form_Field>

  <Form_Field_Hidden $name="id" />
</Rsx_Form>
  • Form_Field - Standard formatted field with label, errors, help text
  • Form_Field_Hidden - Single-tag hidden input (extends Form_Field_Abstract)
  • Form_Field_Abstract - Base class for custom formatting (advanced)

Disabled fields: Use $disabled=true attribute on input components to disable fields. Unlike standard HTML, disabled fields still return values via vals() (useful for read-only data that should be submitted).

<Text_Input $type="email" $disabled=true />
<Select_Input $options="{!! json_encode($options) !!}" $disabled=true />
<Checkbox_Input $label="Subscribe" $disabled=true />

Form component classes use the vals() dual-mode pattern:

class My_Form extends Component {
    vals(values) {
        if (values) {
            // Setter - populate form
            this.$id('name').val(values.name || '');
            return null;
        } else {
            // Getter - extract values
            return {name: this.$id('name').val()};
        }
    }
}

Validation: Form_Utils.apply_form_errors(form.$, errors) - Matches by name attribute.


MODALS

Basic dialogs:

await Modal.alert("File saved");
if (await Modal.confirm("Delete?")) { /* confirmed */ }
let name = await Modal.prompt("Enter name:");

Form modals:

const result = await Modal.form({
    title: "Edit User",
    component: "User_Form",
    component_args: {data: user},
    on_submit: async (form) => {
        const values = form.vals();
        const response = await User_Controller.save(values);

        if (response.errors) {
            Form_Utils.apply_form_errors(form.$, response.errors);
            return false; // Keep open
        }

        return response.data; // Close and return
    }
});

Requirements: Form component must implement vals() and include <div $id="error_container"></div>.

Modal Classes (for complex/reusable modals):

// Define modal class
class Add_User_Modal extends Modal_Abstract {
    static async show() {
        const result = await Modal.form({...});
        return result || false;
    }
}

// Use from page JS
const user = await Add_User_Modal.show();
if (user) {
    // Orchestrate post-modal actions
    grid.reload();
    await Next_Modal.show(user.id);
}

Pattern: Extend Modal_Abstract, implement static show(), return data or false. Page JS orchestrates flow, modal classes encapsulate UI.

Details: php artisan rsx:man modals


JQUERY EXTENSIONS

RSpade extends jQuery with utility methods:

Element existence: $('.element').exists() instead of .length > 0

Component traversal: this.$.shallowFind('.Widget') - Finds child elements matching selector that don't have another element of the same class as a parent between them and the component. Prevents selecting widgets from nested child components.

// Use case: Finding form widgets without selecting nested widgets
this.$.shallowFind('.Form_Field').each(function() {
    // Only processes fields directly in this form,
    // not fields in nested sub-forms
});

Sibling component lookup: $('.element').closest_sibling('.Widget') - Searches for elements within progressively higher ancestors. Like .closest() but searches within ancestors instead of matching them. Stops at body tag. Useful for component-to-component communication.

Form validation: $('form').checkValidity() instead of $('form')[0].checkValidity()

Click override: .click() automatically calls e.preventDefault(). Use .click_allow_default() for native behavior.

For complete details: php artisan rsx:man jquery


MODELS & DATABASE

No Mass Assignment

// ✅ CORRECT
$user = new User_Model();
$user->email = $email;
$user->save();

// ❌ WRONG
User_Model::create(['email' => $email]);

Enums

🔴 Read php artisan rsx:man enum for complete documentation before implementing.

Integer-backed enums with model-level mapping to constants, labels, and custom properties.

class Project_Model extends Rsx_Model_Abstract {
    public static $enums = [
        'status_id' => [
            1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success'],
            2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false],
        ],
    ];
}

// Usage
$project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_label;  // "Active"
echo $project->status_badge;  // "bg-success" (custom property)

Migration: Use BIGINT for enum columns, TINYINT(1) for booleans. Run rsx:migrate:document_models after adding enums.

Migrations

Forward-only, no rollbacks.

php artisan make:migration:safe create_users_table
php artisan migrate:begin
php artisan migrate
php artisan migrate:commit

FILE ATTACHMENTS

Files upload UNATTACHED → validate → assign via API. Session-based validation prevents cross-user file assignment.

// Controller: Assign uploaded file
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
    $attachment->attach_to($user, 'profile_photo');  // Single (replaces)
    $attachment->add_to($project, 'documents');      // Multiple (adds)
}

// Model: Retrieve attachments
$photo = $user->get_attachment('profile_photo');
$documents = $project->get_attachments('documents');

// Display
$photo->get_thumbnail_url('cover', 128, 128);
$photo->get_url();
$photo->get_download_url();

Endpoints: POST /_upload, GET /_download/:key, GET /_thumbnail/:key/:type/:width/:height

Details: php artisan rsx:man file_upload


AJAX ENDPOINTS

#[Ajax_Endpoint]
public static function get_data(Request $request, array $params = [])
{
    return ['success' => true, 'data' => ...];
}
const result = await Demo_Controller.get_data({user_id: 123});

Flash Alerts in Ajax Endpoints

Server-side: Use success alerts ONLY when providing a redirect.

When the Ajax response includes a redirect, there's no on-screen element from the previous page to display the success message. Flash alerts persist across navigation to show the result.

#[Ajax_Endpoint]
public static function save(Request $request, array $params = []): array {
    // Validation errors - return for client to handle
    if (empty($params['name'])) {
        return ['success' => false, 'errors' => ['name' => 'Required']];
    }

    $client->save();

    // Success with redirect - use flash alert
    Flash_Alert::success('Client saved successfully');

    return [
        'client_id' => $client->id,
        'redirect' => Rsx::Route('Clients_View_Action', $client->id),
    ];
}

Server pattern: Success alerts with redirect only. Client handles errors by updating on-screen UI.

Client-side: Flash alerts are a UI choice. Use Flash_Alert.error(), Flash_Alert.warning(), etc. on case-by-case basis (e.g., "Failed to add item to cart - item not available").


BROWSER STORAGE

Rsx_Storage - Scoped sessionStorage/localStorage with automatic fallback and quota management. All keys automatically scoped by session, user, site, and build. Gracefully handles unavailable storage and quota exceeded errors. Storage is volatile - use only for non-critical data (caching, UI state, transient messages).

Rsx_Storage.session_set(key, value) / Rsx_Storage.session_get(key) / Rsx_Storage.local_set(key, value) / Rsx_Storage.local_get(key)

Details: php artisan rsx:man storage


Model Fetch

#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
    if (!RsxAuth::check()) return false;
    return static::find($id);
}
const user = await User_Model.fetch(1);

AUTHENTICATION

Always use RsxAuth, never Laravel Auth or $_SESSION.

RsxAuth::check();     // Is authenticated
RsxAuth::user();      // User model
RsxAuth::id();        // User ID

Sessions persist 365 days. Never implement "Remember Me".


JAVASCRIPT DECORATORS

/** @decorator */
function logCalls(target, key, descriptor) { /* ... */ }

class Service {
    @logCalls
    @mutex
    async save() { /* ... */ }
}

COMMANDS

Module Creation

rsx:app:module:create <name>              # /name
rsx:app:module:feature:create <m> <f>     # /m/f
rsx:app:component:create --name=x         # Component

Development

rsx:check          # Code quality
rsx:debug /page    # Test routes (see below)
rsx:man <topic>    # Documentation
db:query "SQL" --json

Testing Routes

rsx:debug /path - Preferred method for testing routes

Uses Playwright to render the page and show rendered output, JavaScript errors, and console messages.

rsx:debug /clients              # Test route
rsx:debug /dashboard --user=1   # Simulate authenticated user
rsx:debug /contacts --console   # Show console.log output
rsx:debug /path --help          # Show all options

Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.

Debugging

  • rsx_dump_die() - Debug output
  • console_debug("CHANNEL", ...) - Channel logging
  • CONSOLE_DEBUG_FILTER=CHANNEL - Filter output

ERROR HANDLING

if (!$expected) {
    shouldnt_happen("Class {$expected} missing");
}

Use for "impossible" conditions that indicate broken assumptions.


CODE QUALITY

Professional UI: Hover effects ONLY on buttons, links, form fields. Static elements remain static.

Run rsx:check before commits. Enforces naming, prohibits animations on non-actionable elements.


MAIN_ABSTRACT MIDDLEWARE

Optional /rsx/main.php:

class Main extends Main_Abstract
{
    public function init() { }                   // Bootstrap once
    public function pre_dispatch($request, $params) { return null; } // Before routes
    public function unhandled_route($request, $params) { }          // 404s
}

KEY REMINDERS

  1. Fail loud - No silent failures
  2. Static by default - Unless instances needed
  3. Path-agnostic - Reference by name
  4. Bundles required - For JavaScript
  5. Use RsxAuth - Never Laravel Auth
  6. No mass assignment - Explicit only
  7. Forward migrations - No rollbacks
  8. Don't run rsx:clean - Cache auto-invalidates
  9. All routes need #[Auth] - No exceptions

GETTING HELP

php artisan rsx:man <topic>  # Detailed docs
php artisan list rsx          # All commands

Topics: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences

Remember: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.