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>
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 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
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/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"<% } %> />
🔴 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$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
<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.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
- 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.