Remove unused blade settings pages not linked from UI Convert remaining frontend pages to SPA actions Convert settings user_settings and general to SPA actions Convert settings profile pages to SPA actions Convert contacts and projects add/edit pages to SPA actions Convert clients add/edit page to SPA action with loading pattern Refactor component scoped IDs from $id to $sid Fix jqhtml comment syntax and implement universal error component system Update all application code to use new unified error system Remove all backwards compatibility - unified error system complete Phase 5: Remove old response classes Phase 3-4: Ajax response handler sends new format, old helpers deprecated Phase 2: Add client-side unified error foundation Phase 1: Add server-side unified error foundation Add unified Ajax error response system with constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
34 KiB
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: 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 EXISTbin/publish- Creates releases for OTHER developers (not for testing YOUR changes)rsx:bundle:compile- Bundles compile automatically in dev modersx:manifest:build- Manifest rebuilds automatically in dev mode- ANY command with "build", "compile", or "publish"
How it works:
- Edit JS/SCSS/PHP files
- Refresh browser
- 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- 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 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 $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
/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
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
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 - Remove server-side route entirely:
// Remove #[Route] method completely. Add Ajax endpoints:
#[Ajax_Endpoint]
public static function fetch_items(Request $request, array $params = []) {
return ['items' => Feature_Model::all()];
}
CRITICAL: Do NOT add #[SPA] to feature controllers. The #[SPA] attribute only exists in the bootstrap controller (e.g., Frontend_Spa_Controller::index). Feature controllers should only contain #[Ajax_Endpoint] methods for data fetching.
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/123→Rsx.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
.scssand.jsfiles instead - Exception: jqhtml templates may use
@clickdirective syntax (jqhtml-specific feature)
Why no inline handlers:
- Violates separation of concerns (HTML structure vs behavior)
- Makes code harder to maintain and test
rsx:checkwill 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"<% } %> />
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
🔴 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.datainon_load() - Need to reload with new params? Modify
this.args, callreload() - 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, 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).
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 ID
Component Access
this.$sid(name) → jQuery object (for DOM):
this.$sid('button').on('click', ...);
this.sid(name) → Component instance (for methods):
const comp = this.sid('child'); // ✅ Returns component
await comp.reload();
const comp = this.sid('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
<Define>IS the element - usetag=""attributethis.datastarts empty{}, set defaults in on_create()- ONLY modify
this.datain on_create() and on_load() (enforced) - on_load() can ONLY access this.args and this.data (enforced)
- Use
this.state = {}in on_create() for component state (not loaded from Ajax) - Use this.args for reload parameters, reload() to re-fetch
- Use
Controller.method()not$.ajax() - Blade components self-closing only
on_create/render/stopmust be sync- Use this.sid() for components, NOT this.sid().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
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 method(Request $request, array $params = []) {
return $data; // Success - framework wraps as {_success: true, _ajax_return_value: ...}
}
Call: await Controller.method({param: value})
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.
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 /page --screenshot-path=/tmp/page.png --screenshot-width=mobile # Capture screenshot
rsx:debug /path --help # Show all options
Screenshot presets: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large
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
- Fail loud - No silent failures
- Static by default - Unless instances needed
- Path-agnostic - Reference by name
- Bundles required - For JavaScript
- Use RsxAuth - Never Laravel Auth
- No mass assignment - Explicit only
- Forward migrations - No rollbacks
- Don't run rsx:clean - Cache auto-invalidates
- 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.