Add PHP-ALIAS-01 rule: prohibit field aliasing in serialization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
45 KiB
=============================================================================== FRAMEWORK DOCUMENTATION - READ ONLY
This file: /var/www/html/system/docs/CLAUDE.dist.md (symlinked to ~/.claude/CLAUDE.md) Your file: /var/www/html/CLAUDE.md
This file is replaced during php artisan rsx:framework:pull. Do not edit it.
For application-specific notes, edit /var/www/html/CLAUDE.md instead.
DOCUMENTATION SIZE CONSTRAINTS
Claude Code recommends combined CLAUDE.md files under 40kb - already tight for framework + application documentation. Every line must justify its existence.
When editing /var/www/html/CLAUDE.md:
- Terse, not verbose - no filler words or redundant explanations
- Complete, not partial - include all critical information
- Patterns over prose - code examples beat paragraphs
- Reference this file's tone and density as the standard
===============================================================================
RSpade Framework - AI/LLM Development Guide
PURPOSE: Essential directives for AI/LLM assistants developing RSX applications with RSpade.
CRITICAL: Questions vs Commands
- Questions get answers, NOT actions - "Is that fire?" gets "Yes" not "Let me run through it". User has a plan, don't take destructive action when asked a question.
- Commands get implementation - Clear directives result in code changes
What is RSpade?
Visual Basic-like development for PHP/Laravel. Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.
Philosophy: RSpade is rebellion against JavaScript fatigue. Like VB6 made Windows programming accessible, RSpade makes web development simple again. No build steps, no config hell, just code and reload.
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
CRITICAL: RSpade Builds Automatically - NEVER RUN BUILD COMMANDS
RSpade is INTERPRETED - changes compile on-the-fly. NO manual build steps.
FORBIDDEN (unless explicitly instructed):
npm run compile/build- Don't existbin/publish- For releases, not testingrsx:bundle:compile/rsx:manifest:build- Automatic- ANY "build", "compile", or "publish" command
Edit → Save → Refresh browser → Changes live (< 1 second)
Framework Updates
php artisan rsx:framework:pull # User-initiated only
Updates take 2-5 minutes. Includes code pull, manifest rebuild, bundle recompilation. Only run when requested by user.
Fail Loud - No Silent Fallbacks
ALWAYS fail visibly. No redundant fallbacks, silent failures, or alternative code paths.
// ❌ WRONG
try { $clean = Sanitizer::sanitize($input); }
catch (Exception $e) { $clean = $input; } // DISASTER
// ✅ CORRECT
$clean = Sanitizer::sanitize($input); // Let it throw
SECURITY-CRITICAL: Never continue after security failures. Only catch expected failures (file uploads, APIs, user input). Let exceptions bubble to 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.
No Field Aliasing
Field names must be identical across all layers. Database → PHP → JSON → JavaScript: same names, always.
// ❌ WRONG - renaming during serialization
return ['type_label' => $contact->type_id_label];
// ✅ CORRECT - names match source
return ['type_id_label' => $contact->type_id_label];
One string everywhere. Grep finds all usages. No mental mapping between layers.
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.
Class Overrides
To customize framework classes without modifying /system/, copy them to rsx/ with the same class name. The manifest automatically uses your version and renames the framework file to .upstream.
Common override targets (copy from system/app/RSpade/Core/Models/):
User_Model- Add custom fields, relationships, methodsUser_Profile_Model- Extend profile dataSite_Model- Add site-specific settings
cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php
# Edit namespace to Rsx\Models, customize as needed
Details: php artisan rsx:man class_override
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.jqhtml
frontend_calendar_event.js
Critical: Never create same-name different-case files (e.g., user.php and User.php).
Component Naming Pattern
Input components follow: {Supertype}_{Variant}_{Supertype} → e.g., Select_Country_Input, Select_State_Input, Select_Ajax_Input
DIRECTORY STRUCTURE
/var/www/html/
├── rsx/ # YOUR CODE
│ ├── app/ # Modules
│ ├── models/ # Database models
│ ├── services/ # Background tasks, external integrations
│ ├── public/ # Static files (web-accessible)
│ ├── resource/ # Framework-ignored
│ └── theme/ # Global assets
└── system/ # FRAMEWORK (read-only)
Services: Extend Rsx_Service_Abstract for non-HTTP functionality like scheduled tasks or external system integrations.
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
{
public static function pre_dispatch(Request $request, array $params = [])
{
if (!Session::is_logged_in()) return response_unauthorized();
return null;
}
#[Route('/', methods: ['GET'])]
public static function index(Request $request, array $params = [])
{
return rsx_view('Frontend_Index', [
'bundle' => Frontend_Bundle::render()
]);
}
}
Rules: Only GET/POST. Use :param syntax. Manual auth checks in pre_dispatch or method body.
Authentication Pattern
// Controller-wide auth (recommended)
public static function pre_dispatch(Request $request, array $params = []) {
if (!Session::is_logged_in()) return response_unauthorized();
return null;
}
// Public endpoints: add @auth-exempt to class docblock
/** @auth-exempt Public route */
Code quality: PHP-AUTH-01 rule verifies auth checks exist. Use @auth-exempt for public routes.
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- Unimplemented routes: Prefix method with
#→Rsx::Route('Feature::#index')generateshref="#"and bypasses validation
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 module with auth in pre_dispatch
public static function pre_dispatch(Request $request, array $params = []) {
if (!Session::is_logged_in()) return response_unauthorized();
return null;
}
#[SPA]
public static function index(Request $request, array $params = []) {
return rsx_view(SPA);
}
One #[SPA] per module at rsx/app/(module)/(module)_spa_controller::index. Segregates code by permission level.
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 $sid="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
Pattern: /rsx/app/(module)/(feature)/
- Module: Major functionality (login, frontend, root)
- Feature: Screen within module (contacts, reports, invoices)
- Submodule: Feature grouping (settings), often with sublayouts
/rsx/app/frontend/ # Module
├── Frontend_Spa_Controller.php # Single SPA bootstrap
├── Frontend_Layout.js
├── Frontend_Layout.jqhtml
└── contacts/ # Feature
├── 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
Sublayouts
Sublayouts are Spa_Layout classes for nested persistent UI (e.g., settings sidebar). Use multiple @layout decorators - first is outermost: @layout('Frontend_Spa_Layout') then @layout('Settings_Layout'). Each must have $sid="content". Layouts persist when unchanged; only differing parts recreated. All receive on_action(url, action_name, args) with final action info.
Details: php artisan rsx:man spa
View Action Pattern (Loading Data)
For SPA actions that load data (view/edit CRUD pages), use the three-state pattern:
on_create() {
this.data.record = { name: '' }; // Stub prevents undefined errors
this.data.error_data = null;
this.data.loading = true;
}
async on_load() {
try {
this.data.record = await Controller.get({id: this.args.id});
} catch (e) {
this.data.error_data = e;
}
this.data.loading = false;
}
Template uses three states: <Loading_Spinner> → <Universal_Error_Page_Component> → content.
Details: php artisan rsx:man view_action_patterns
CONVERTING BLADE PAGES TO SPA ACTIONS
For converting server-side Blade pages to client-side SPA actions, see php artisan rsx:man blade_to_spa.
The process involves creating Action classes with @route decorators and converting templates from Blade to jqhtml syntax.
BLADE & VIEWS
Note: SPA pages are the preferred standard. Use Blade only for SEO-critical public pages or authentication flows.
@rsx_id('Frontend_Index') {{-- Every view starts with this --}}
<body class="{{ rsx_body_class() }}"> {{-- Adds view class --}}
NO inline styles, scripts, or event handlers - Use companion .scss and .js files.
jqhtml components work fully in Blade (no slots).
SCSS Component-First Architecture
Philosophy: Every styled element is a component. If it needs custom styles, give it a name, a jqhtml definition, and scoped SCSS. This eliminates CSS spaghetti - generic classes overriding each other unpredictably across files.
Recognition: When building a page, ask: "Is this structure unique, or a pattern?" A datagrid page with toolbar, tabs, filters, and search is a pattern - create Datagrid_Card once with slots, use it everywhere. A one-off project dashboard is unique - create Project_Dashboard for that page. If you're about to copy-paste structural markup, stop and extract a component.
Composition: Use slots to separate structure from content. The component owns layout and styling; pages provide the variable parts via slots. This keeps pages declarative and components reusable.
Enforcement: SCSS in rsx/app/ and rsx/theme/components/ must wrap in a single component class matching the jqhtml/blade file. This works because all jqhtml components, SPA actions/layouts, and Blade views with @rsx_id automatically render with class="Component_Name" on their root element. rsx/lib/ is for non-visual plumbing (validators, utilities). rsx/theme/ (outside components/) holds primitives, variables, Bootstrap overrides.
BEM Child Classes: When using BEM notation, child element classes must use the component's exact class name as prefix. SCSS .Component_Name { &__element } compiles to .Component_Name__element, so HTML must match: <div class="Component_Name__element"> not <div class="component-name__element">. No kebab-case conversion.
Variables: Define shared values (colors, spacing, border-radius) in rsx/theme/variables.scss or similar. These must be explicitly included before directory includes in bundle definitions. Component-local variables can be defined within the scoped rule.
Supplemental files: Multiple SCSS files can target the same component (e.g., breakpoint-specific styles) if a primary file with matching filename exists.
Details: php artisan rsx:man scss
Responsive Breakpoints
RSX replaces Bootstrap's default breakpoints (xs/sm/md/lg/xl/xxl) with semantic device names.
Tier 1 - Semantic:
mobile: 0 - 1023px (phone + tablet)desktop: 1024px+
Tier 2 - Granular:
phone: 0 - 799px |tablet: 800 - 1023px |desktop-sm: 1024 - 1699px |desktop-md: 1700 - 2199px |desktop-lg: 2200px+
SCSS Mixins: @include mobile { }, @include desktop { }, @include phone { }, @include tablet { }, @include desktop-sm { }, etc.
Bootstrap Classes: .col-mobile-6, .col-desktop-4, .d-mobile-none, .d-tablet-block, .col-phone-12 .col-tablet-6 .col-desktop-sm-4
Utility Classes: .mobile-only, .desktop-only, .phone-only, .hide-mobile, .hide-tablet
Note: Bootstrap's default classes like .col-md-6 or .d-lg-none do NOT work - use the RSX breakpoint names instead.
JS Detection: Responsive.is_mobile(), Responsive.is_desktop() (Tier 1 - broad); Responsive.is_phone(), Responsive.is_tablet(), Responsive.is_desktop_sm(), Responsive.is_desktop_md(), Responsive.is_desktop_lg() (Tier 2 - specific ranges)
Details: php artisan rsx:man responsive
JavaScript for Blade Pages
Unlike SPA actions (which use component lifecycle), Blade pages use static on_app_ready() with a page guard:
class My_Page { // Matches @rsx_id('My_Page')
static on_app_ready() {
if (!$('.My_Page').exists()) return; // Guard required - fires for ALL pages in bundle
// Page code here
}
}
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
CRITICAL: JavaScript only executes when bundle rendered. See "JavaScript for Blade Pages" in BLADE & VIEWS section for the on_app_ready() pattern.
BUNDLE SYSTEM
One bundle per page required. Compiles JS/CSS automatically on request - no manual build steps.
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
],
];
}
}
<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"<% } %> />
Inline Logic: <% this.handler = () => action(); %> then @click=this.handler - No JS file needed for simple components
Event Handlers: @click=this.method (unquoted) - Methods defined inline or in companion .js
Validation: <% if (!this.args.required) throw new Error('Missing arg'); %> - Fail loud in template
Simple Components (No JS File Needed)
For simple components without external data or complex state, write JS directly in the template:
<Define:CSV_Renderer>
<%
// Validate input
if (!this.args.csv_data) throw new Error('csv_data required');
// Parse CSV
const rows = this.args.csv_data.split('\n').map(r => r.split(','));
// Define click handler inline
this.toggle = () => { this.args.expanded = !this.args.expanded; this.render(); };
%>
<table>
<% for (let row of rows) { %>
<tr>
<% for (let cell of row) { %>
<td><%= cell %></td>
<% } %>
</tr>
<% } %>
</table>
<button @click=this.toggle>Toggle View</button>
</Define:CSV_Renderer>
When to use inline JS: Simple data transformations, conditionals, loops, basic event handlers When to create .js file: External data loading, complex state management, multiple methods, or when JS overwhelms the template (should look mostly like HTML with some logic, not JS with some HTML)
State Management Rules (ENFORCED)
Quick Guide:
- Loading from API? → Use
this.datainon_load() - Need reload with different params? → Modify
this.args, callreload() - UI state (toggles, selections)? → Use
this.state
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)
// 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, writethis.data - ❌ NO DOM access, NO
this.state, NO modifyingthis.args
Lifecycle
- on_create() → Setup default state BEFORE template (sync)
- render → Template executes with initialized state
- on_render() → Hide uninitialized UI (sync)
- on_load() → Fetch data into
this.data(async) - 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).
Component API - CRITICAL FOR LLM AGENTS
This section clarifies common misunderstandings. Read carefully.
DOM Access Methods:
| Method | Returns | Purpose |
|---|---|---|
this.$ |
jQuery | Root element of component (stable, survives redraws) |
this.$sid('name') |
jQuery | Child element with $sid="name" (always returns jQuery, even if empty) |
this.sid('name') |
Component or null | Child component instance (null if not found or not a component) |
this.$.find('.class') |
jQuery | Standard jQuery find (use when $sid isn't appropriate) |
WRONG: this.$el - This does not exist. Use this.$
The reload() Paradigm - MANDATORY:
reload() = on_load() → render() → on_ready()
render() = template redraw only (NO on_ready)
LLM agents must ALWAYS use reload(), NEVER call render() directly.
When you need to refresh a component after a mutation (add, edit, delete), call this.reload(). Yes, this makes another server call via on_load(). This is intentional. The extra round-trip is acceptable - our server is fast and the paradigm simplicity is worth it.
WRONG approach (do NOT do this):
// ❌ BAD - Trying to be "efficient" by skipping server round-trip
async add_item() {
const new_item = await Controller.add({name: 'Test'});
this.data.items.push(new_item); // ERROR: Cannot modify this.data outside on_load
this.render(); // WRONG: Event handlers will break, on_ready won't run
}
CORRECT approach:
// ✅ GOOD - Clean, consistent, reliable
async add_item() {
await Controller.add({name: 'Test'});
this.reload(); // Calls on_load() to refresh this.data, then on_ready() for handlers
}
Event Handlers - Set Up in on_ready():
Event handlers must be registered in on_ready(). Since on_ready() runs after every reload(), handlers automatically reattach when the DOM is redrawn.
on_ready() {
this.$sid('save_btn').click(() => this.save());
this.$sid('delete_btn').click(() => this.delete());
}
WRONG: Event delegation to avoid reload(). If you find yourself writing this.$.on('click', '[data-sid="btn"]', handler) to "survive" render calls, you're doing it wrong. Use reload() and let on_ready() reattach handlers.
this.data Modification Rules (ENFORCED):
on_create(): Set defaults only (e.g.,this.data.items = [])on_load(): Fetch and assign from server (e.g.,this.data.items = await Controller.list())- Everywhere else: Read-only. Attempting to modify
this.dataoutside these methods throws an error.
on_render() - LLM Should Not Use:
on_render() exists for human developers doing performance optimization. LLM agents should pretend it doesn't exist. Use on_ready() for all post-render DOM work.
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$sid="name"→ Scoped element IDattr="<%= expr %>"→ HTML attribute with interpolation
Key restrictions:
<Define>attributes are static - No<%= %>on the<Define>tag. For dynamic attributes on the root element, use inline JS:<% this.$.attr('data-id', this.args.id); %>$prefix= component args, NOT HTML attributes -<My_Component $data-id=123 />createsthis.args['data-id'], not adata-idDOM attribute- Conditional attributes use if-statements -
<% if (cond) { %>checked<% } %>not ternaries
Component Access
$sid attribute = "scoped ID" - unique within component instance
From within component methods:
- this.$ → jQuery selector for the component element itself
- this.$sid(name) → jQuery selector for child element with
$sid="name" - this.sid(name) → Component instance of child (or null if not a component)
- $(selector).component() → Get component instance from jQuery element
await $(selector).component().ready()→ Await component initialization. Rarely needed -on_ready()auto-waits for children created during render. Use for dynamically created components or Blade page JS interaction.
Custom Component Events
Fire: this.trigger('event_name', data) | Listen: this.sid('child').on('event_name', (component, data) => {})
Key difference from jQuery: Events fired BEFORE handler registration still trigger the callback when registered. This solves component lifecycle timing issues where child events fire before parent registers handlers. Never use this.$.trigger() for custom events (enforced by JQHTML-EVENT-01).
Dynamic Component Creation
To dynamically create/replace a component in JavaScript:
// Destroys existing component (if any) and creates new one in its place
$(selector).component('Component_Name', { arg1: value1, arg2: value2 });
// Example: render a component into a container
this.$sid('result_container').component('My_Component', {
data: myData,
some_option: true
});
Incremental Scaffolding
Undefined components work immediately - they render as div with the component name as a class.
<Dashboard>
<Stats_Panel />
<Recent_Activity />
</Dashboard>
Key Pitfalls (ABSOLUTE RULES)
<Define>IS the element - usetag=""attributethis.datastarts empty{}- MUST set defaults inon_create()- ONLY modify
this.datainon_create()andon_load()(enforced by framework) on_load()can ONLY accessthis.argsandthis.data(no DOM, nothis.state)- Use
this.state = {}inon_create()for UI state (not from Ajax) - Use
this.argsfor reload parameters, callreload()to re-fetch - Use
Controller.method()not$.ajax()- PHP methods with #[Ajax_Endpoint] auto-callable from JS on_create/render/stopmust be syncthis.sid()returns component instance,$(selector).component()converts jQuery to 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.$sid('name').val(values.name || '');
return null;
} else {
// Getter - extract values
return {name: this.$sid('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 $sid="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
| Method | Purpose |
|---|---|
.exists() |
Check element exists (instead of .length > 0) |
.shallowFind(selector) |
Find children without nested component interference |
.closest_sibling(selector) |
Search within ancestor hierarchy |
.checkValidity() |
Form validation helper |
.click() |
Auto-prevents default |
.click_allow_default() |
Native click behavior |
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
CRITICAL: 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.
Model Fetch
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!Session::is_logged_in()) return false;
return static::find($id);
}
const project = await Project_Model.fetch(1); // Throws if not found
const maybe = await Project_Model.fetch_or_null(999); // Returns null if not found
console.log(project.status_label); // Enum properties populated
console.log(Project_Model.STATUS_ACTIVE); // Static enum constants
// Lazy relationships (requires #[Ajax_Endpoint_Model_Fetch] on relationship method)
const client = await project.client(); // belongsTo → Model or null
const tasks = await project.tasks(); // hasMany → Model[]
Security: Both fetch() and relationships require #[Ajax_Endpoint_Model_Fetch] attribute. Related models must also implement fetch() with this attribute.
Details: php artisan rsx:man model_fetch
Migrations
Forward-only, no rollbacks. Deterministic transformations against known state.
php artisan make:migration:safe create_users_table
php artisan migrate:begin
php artisan migrate
php artisan migrate:commit
NO defensive coding in migrations:
// ❌ WRONG - conditional logic
$fk_exists = DB::select("SELECT ... FROM information_schema...");
if (!empty($fk_exists)) { DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar"); }
// ✅ CORRECT - direct statements
DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar");
DB::statement("ALTER TABLE foo DROP COLUMN baz");
No IF EXISTS, no information_schema queries, no fallbacks. Know current state, write exact transformation. Failures fail loud - snapshot rollback exists for recovery.
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 method(Request $request, array $params = []) {
return $data; // Success - framework wraps as {_success: true, _ajax_return_value: ...}
}
PHP→JS Auto-mapping:
// PHP: My_Controller class
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
return ['id' => 123];
}
// JS: Automatically callable
const result = await My_Controller.save({name: 'Test'});
console.log(result.id); // 123
Error Responses
Use response_error(Ajax::ERROR_CODE, $metadata):
// Not found
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
// Validation
return response_error(Ajax::ERROR_VALIDATION, [
'email' => 'Invalid',
'name' => 'Required'
]);
// Auto-message
return response_error(Ajax::ERROR_UNAUTHORIZED);
Codes: ERROR_VALIDATION, ERROR_NOT_FOUND, ERROR_UNAUTHORIZED, ERROR_AUTH_REQUIRED, ERROR_FATAL, ERROR_GENERIC
Client:
try {
const data = await Controller.get(id);
} catch (e) {
if (e.code === Ajax.ERROR_NOT_FOUND) {
// Handle
} else {
alert(e.message); // Generic
}
}
Unhandled errors auto-show flash alert.
DATA FETCHING (CRITICAL)
DEFAULT: Use Model.fetch(id) for all single-record retrieval from JavaScript.
const user = await User_Model.fetch(1); // Throws if not found
const user = await User_Model.fetch_or_null(1); // Returns null if not found
Requires #[Ajax_Endpoint_Model_Fetch] on the model's fetch() method.
Auto-populates enum properties and enables lazy relationship loading.
If model not available in JS bundle: STOP and ask the developer. Bundles should include all models they need (rsx/models in include paths). Do not create workaround endpoints without approval.
Custom Ajax endpoints require developer approval and are only for:
- Aggregations, batch operations, or complex result sets
- System/root-only models intentionally excluded from bundle
- Queries beyond simple ID lookup
Details: php artisan rsx:man model_fetch
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
AUTHENTICATION
Always use Session - Static methods only. Never Laravel Auth or $_SESSION.
Session::is_logged_in(); // Returns true if user logged in
Session::get_user(); // Returns user model or null
Session::get_user_id(); // Returns user ID or null
Session::get_site(); // Returns site model
Session::get_site_id(); // Returns current site ID
Session::get_session_id(); // Returns session 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 /page --screenshot-path=/tmp/page.png --screenshot-width=mobile # Capture screenshot
rsx:debug /page --dump-dimensions=".card" # Add position/size data attributes to elements
rsx:debug /path --help # Show all options
# Simulate user interactions with --eval (executes before DOM capture)
rsx:debug /contacts --user=1 --eval="$('.page-link[data-page=\"2\"]').click(); await sleep(2000)"
rsx:debug /form --eval="$('#name').val('test'); $('form').submit(); await sleep(500)"
Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large
The --eval option runs JavaScript after page load but before DOM capture. Use await sleep(ms) to wait for async operations. This is powerful for testing pagination, form submissions, and other interactive behavior.
Use this instead of manually browsing, especially for SPA pages and Ajax-heavy features.
CRITICAL: SPA routes ARE server routes. The server knows all SPA routes. rsx:debug uses Playwright to fully render pages including all JavaScript and SPA navigation. If you get a 404, the route genuinely doesn't exist - check your URL pattern and route definitions. Never dismiss 404s as "SPA routes can't be tested server-side" - this analysis is incorrect.
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.
z-index: Bootstrap defaults + 1100 (modal children), 1200 (flash alerts), 9000+ (system). Details: rsx:man zindex
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
- Fail loud - No silent failures
- Static by default - Unless instances needed
- Path-agnostic - Reference by name
- Bundles required - For JavaScript
- Use Session - Never Laravel Auth
- No mass assignment - Explicit only
- Forward migrations - No rollbacks
- Don't run rsx:clean - Cache auto-invalidates
- All routes need auth checks - In pre_dispatch() or method body (@auth-exempt for public)
GETTING HELP
php artisan rsx:man <topic> # Framework documentation
php artisan list rsx # All commands
Topics: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences
PROJECT DOCUMENTATION
Project-specific technical documentation lives in /rsx/resource/man/. These are man-page-style text files documenting features specific to your application that build on or extend the framework.
When to create a project man page:
- Feature has non-obvious implementation details
- Multiple components interact in ways that need explanation
- Configuration options or patterns need documentation
- AI agents or future developers need reference material
Format: Plain text files (.txt) following Unix man page conventions. See /rsx/resource/man/CLAUDE.md for writing guidelines.
Remember: RSpade prioritizes simplicity and rapid development. When in doubt, choose the straightforward approach.