Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

112
app/RSpade/man/CLAUDE.md Executable file
View File

@@ -0,0 +1,112 @@
# RSX Framework Manual Pages
## Documentation Philosophy
These manual pages follow traditional Unix man page conventions from the late 1990s:
- **Plain text format** - No fancy formatting, boxes, or special characters
- **Simple indentation** - Code examples use 4-space indentation
- **Readable and copyable** - Easy to copy/paste examples
- **Traditional sections** - NAME, SYNOPSIS, DESCRIPTION, etc.
## Writing Style Requirements
### Format
- Use plain ASCII text only
- Code examples indented with 4 spaces
- No box drawing characters (└─┘│┌┐)
- No special Unicode symbols
- Section headers in ALL CAPS
- Subsection headers in Title Case
### Content Requirements
Each man page must include:
1. **General introduction** - Explain what the RSX feature is and its purpose
2. **RSX philosophy** - How it simplifies development compared to standard approaches
3. **Laravel differences** - Explicitly note how RSX differs from Laravel equivalents
4. **Practical examples** - Real-world usage patterns from actual RSX applications
5. **Common pitfalls** - What mistakes developers might make
### RSX vs Laravel Context
Always explain how RSX features differ from their Laravel counterparts:
- Path-agnostic class loading vs PSR-4 autoloading
- Manifest-based discovery vs service provider registration
- Static controller methods vs instance methods with DI
- Unified bundle system vs mix/vite configuration
- Automatic route discovery vs manual route files
## Manual Page Sections
Standard sections in order:
```
NAME
Component name and one-line description
SYNOPSIS
Quick usage examples showing the most common use case
DESCRIPTION
Overview of the component, its philosophy, and how it differs from Laravel
BASIC USAGE
Step-by-step introduction to using the feature
API REFERENCE
Detailed method/function documentation
EXAMPLES
Practical, real-world code examples
RSX VS LARAVEL
Explicit comparison with Laravel equivalents
TROUBLESHOOTING
Common errors and their solutions
SEE ALSO
Related manual pages
```
## File Naming
- Use lowercase with underscores: `manifest_api.txt`
- Extensions: Always `.txt` for man pages
- No special characters or spaces
## Example Introduction Template
```
DESCRIPTION
The RSX [feature] provides [what it does] through [how it works].
Unlike Laravel's [equivalent], which requires [Laravel approach],
RSX automatically [RSX approach] without any configuration.
This path-agnostic approach means developers never need to worry
about [common Laravel pain point]. The framework handles all
[technical detail] transparently.
Key differences from Laravel:
- Laravel: [how Laravel does it]
- RSX: [how RSX does it]
Benefits:
- No [configuration/boilerplate] required
- Automatic [feature]
- Works with [use case]
```
## Testing Documentation
After writing or updating a man page:
1. Test with `php artisan rsx:man [topic]`
2. Verify plain text output (no special characters)
3. Ensure code examples are properly indented
4. Confirm examples can be copy/pasted directly
## Future Documentation Topics
Planned manual pages to create:
- `model.txt` - ORM with enum constants and Ajax fetch
- `auth.txt` - Session-based authentication (no JWT)
- `testing.txt` - RSX test framework
- `ajax.txt` - Internal API system with auto-generated stubs
- `main.txt` - Application-wide middleware hooks
- `migration.txt` - Forward-only database migrations

65
app/RSpade/man/README.md Executable file
View File

@@ -0,0 +1,65 @@
# RSX Framework Manual Pages
## Overview
This directory contains technical documentation for the RSX Framework, formatted as traditional Unix manual pages from the late 1990s. Plain text format ensures easy copying and pasting of examples.
## Design Philosophy
- **Plain text format** - No fancy formatting or special characters
- **Laravel comparisons** - Explicit differences from Laravel equivalents
- **Framework philosophy** - Explains the "why" behind RSX design choices
- **Practical examples** - Real-world usage patterns
- **LLM-optimized** - Structured for easy parsing
## Available Documentation
### Core Systems
- `manifest_api.txt` - Manifest class API for file discovery and metadata
- `manifest_build.txt` - Manifest compilation process and extension system
- `bundle_api.txt` - Bundle system for asset compilation and management
- `controller.txt` - Controllers, routing, and Ajax endpoints
- `jqhtml.txt` - JQHTML component system and jQuery integration
### Naming Convention
Files use `alphanumeric_underscore.txt` format for consistency with RSX conventions.
## Usage
Read directly or access via `rsx:man` command:
```bash
php artisan rsx:man manifest_api
php artisan rsx:man controller
```
## Format
Each manual page follows standard sections:
- NAME - Component and brief description
- SYNOPSIS - Quick usage examples
- DESCRIPTION - Overview of functionality
- Subsections for specific features
- EXAMPLES - Practical code samples
- TROUBLESHOOTING - Common issues
- SEE ALSO - Related documentation
## Contributing
When adding new documentation:
1. Use `.txt` extension with underscores in filename
2. Follow existing format structure
3. Focus on API reference, not education
4. Include real examples from codebase
5. Keep descriptions terse but complete
## Future Documentation
Planned additions:
- `model.txt` - ORM and database models
- `auth.txt` - Authentication system
- `middleware.txt` - Request middleware
- `testing.txt` - Testing framework
- `commands.txt` - Artisan commands
- `debugging.txt` - Debug tools and techniques

624
app/RSpade/man/auth.txt Executable file
View File

@@ -0,0 +1,624 @@
NAME
auth - RSX authentication, multi-tenant, and experience-based session system
SYNOPSIS
Multi-realm authentication with request-scoped overrides for complex B2B SaaS
applications
DESCRIPTION
The RSX authentication system provides flexible session management for B2B
SaaS applications with support for:
1. Multi-tenant organizations (site_id)
2. Multiple authentication realms (experience_id)
3. Customer portals vs staff panels with separate logins
4. Subdomain-based tenant enforcement
5. Organization picker workflows (Trello-style)
Unlike traditional single-context authentication, RSX allows users to be
logged into multiple "experiences" simultaneously using the same cookie.
For example, a user can be logged into a customer portal as one user AND
a staff admin panel as a different user, with the system automatically
determining which session to use based on the request context.
Key differences from traditional authentication:
- Traditional: One user per session cookie
- RSX: Multiple sessions per cookie (different experiences)
- Traditional: Site/tenant determined by session record
- RSX: Site can be enforced per-request (subdomain) without session writes
- Traditional: Can't be logged in as different users simultaneously
- RSX: Customer portal login + staff panel login using same cookie
Benefits:
- Support customer portals and staff panels simultaneously
- Subdomain-based tenant enforcement without session overhead
- Flexible organization switching (Trello-style) when needed
- Clean API for request-scoped overrides
CORE CONCEPTS
Site (site_id):
Represents data segregation for multi-tenant applications.
Each site is typically an organization/company/tenant.
Can be set in session (persisted) or overridden per-request (ephemeral).
Experience (experience_id):
Represents authentication realm or context.
Different experiences use different session records but same cookie.
Typical values:
- 0: Default (standard user login)
- 1: Staff portal (admin/employee accounts)
- 2: Customer portal (client-facing accounts)
Request-Scoped Override:
Temporary value that takes precedence for current request only.
Does NOT modify session record in database.
Use case: Subdomain enforcement, temporary context switching.
EXPERIENCE-BASED AUTHENTICATION
Concept:
An experience is an authentication realm. The same user can be logged
into different experiences simultaneously, each with its own session
record and user_id, all sharing one cookie token.
How It Works:
1. Request arrives with cookie token
2. Framework sets experience_id based on URL path (e.g., /staff/* = 1)
3. Session::init() loads session WHERE token=X AND experience_id=Y
4. Different experience = different session = different user
Database Structure:
sessions table has experience_id column (default 0)
Same session_token can have multiple rows with different experience_id
Each experience has its own user_id, site_id, csrf_token
Use Cases:
Customer Portal + Staff Panel:
// Customer visits /portal/dashboard
Session::set_experience_id(2);
// Loads customer portal session (experience_id=2, user_id=123)
// Same user visits /staff/admin
Session::set_experience_id(1);
// Loads staff session (experience_id=1, user_id=456)
// User is logged in as BOTH simultaneously
Public Site + Admin:
// Anonymous visitor on public site
Session::set_experience_id(0);
// No login required
// Same visitor accesses /admin
Session::set_experience_id(1);
// Requires staff login (different experience)
SETTING EXPERIENCE CONTEXT
In Main.php pre_dispatch:
public static function pre_dispatch(Request $request, array $params = [])
{
$path = $request->path();
// Determine experience based on URL
if (str_starts_with($path, 'staff/')) {
Session::set_experience_id(1); // Staff realm
} elseif (str_starts_with($path, 'portal/')) {
Session::set_experience_id(2); // Customer realm
} else {
Session::set_experience_id(0); // Default realm
}
return null;
}
Based on Subdomain:
$host = $request->getHost();
if (str_ends_with($host, '-staff.yourapp.com')) {
Session::set_experience_id(1); // Staff subdomain
} elseif (str_ends_with($host, '-portal.yourapp.com')) {
Session::set_experience_id(2); // Customer subdomain
}
API Methods:
Session::set_experience_id(int $id): void
Set experience context for current request. Clears cached
session/user/site data and forces re-initialization with new
experience filter.
Session::get_experience_id(): int
Get current experience ID (default 0).
REQUEST-SCOPED SITE OVERRIDE
Concept:
Override site_id for current request without modifying session record.
Use case: Subdomain enforcement where subdomain determines tenant.
Behavior:
- Session::get_site_id() returns override value if set
- Session record site_id remains unchanged
- Override cleared at end of request (not persisted)
- Useful when site is determined by subdomain, not user choice
Use Case - Subdomain Enforcement:
// User visits acme.yourapp.com
$subdomain = 'acme';
$site = Site_Model::where('subdomain', $subdomain)->first();
if ($site) {
// Enforce this site for current request
Session::set_request_site_id_override($site->id);
// All code that calls Session::get_site_id() will get this value
// Session record NOT modified
}
// Later in controller:
$site_id = Session::get_site_id(); // Returns enforced site_id
Use Case - Temporary Context:
// Temporarily switch site context without changing session
Session::set_request_site_id_override($other_site_id);
// Process data for other site
$data = process_for_site();
// Clear override to return to normal
Session::clear_request_site_id_override();
API Methods:
Session::set_request_site_id_override(int $site_id): void
Override site_id for current request only. Does NOT persist to
database. Takes precedence over session site_id.
Session::clear_request_site_id_override(): void
Remove override, return to normal session-based site_id.
Session::has_request_site_id_override(): bool
Check if override is currently active.
MULTI-TENANT PATTERNS
Pattern 1: Subdomain Enforcement
Each tenant has a subdomain. Site is determined by subdomain, not
user choice. User cannot switch sites.
Implementation:
In Main.php pre_dispatch:
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
$site = Site_Model::where('subdomain', $subdomain)->first();
if ($site) {
Session::set_request_site_id_override($site->id);
}
Result:
- acme.yourapp.com → site_id enforced to Acme's site
- widget.yourapp.com → site_id enforced to Widget's site
- Session record site_id remains 0 (not set)
- No database writes for site switching
Pattern 2: Organization Picker (Trello-Style)
User logs in, then picks which organization to work with.
Can switch organizations at any time.
Implementation:
Login:
Session::set_user_id($user_id); // Just login
Organization Selection:
// Show list of user's organizations
$sites = Site_User_Model::where('user_id', $user_id)->get();
// User picks one
Session::set_site_id($selected_site_id); // Persists to session
Switching Organizations:
Session::set_site_id($different_site_id); // Updates session
Result:
- User explicitly chooses organization
- Choice persisted in session record
- Can switch organizations without re-login
Pattern 3: Combined (Subdomain + Organization Picker)
Subdomain determines site, but user can belong to multiple sites
and explicitly choose which one to access.
Implementation:
// Subdomain enforcement
$site = Site_Model::where('subdomain', $subdomain)->first();
if ($site) {
Session::set_request_site_id_override($site->id);
}
// Verify user has access
$site_user = Site_User_Model::where('user_id', Session::get_user_id())
->where('site_id', Session::get_site_id())
->first();
if (!$site_user) {
return redirect('/access-denied');
}
Result:
- Subdomain determines which site
- User must have access to that site
- Cannot switch sites (determined by subdomain)
CUSTOMER PORTAL IMPLEMENTATION
Concept:
Customer portal is a separate authentication realm (experience_id=2)
where end-user customers log in with their own accounts, distinct
from staff/admin accounts.
Database Structure:
users table:
- id, email, password, name, etc.
- is_customer TINYINT(1) (true for customer portal users)
- is_staff TINYINT(1) (true for staff panel users)
OR separate tables:
customer_users table (customer portal accounts)
staff_users table (staff panel accounts)
Main.php Experience Detection:
public static function pre_dispatch(Request $request, array $params = [])
{
if (str_starts_with($request->path(), 'portal/')) {
Session::set_experience_id(2); // Customer realm
} elseif (str_starts_with($request->path(), 'staff/')) {
Session::set_experience_id(1); // Staff realm
} else {
Session::set_experience_id(0); // Default
}
return null;
}
Customer Login:
#[Route('/portal/login')]
public static function customer_login(Request $request, array $params = [])
{
// Experience already set to 2 by pre_dispatch
if ($request->method() === 'POST') {
$email = $request->input('email');
$password = $request->input('password');
$user = User_Model::where('email', $email)
->where('is_customer', true)
->first();
if ($user && password_verify($password, $user->password)) {
Session::set_user($user); // Creates session with experience_id=2
return redirect('/portal/dashboard');
}
Rsx::flash_error('Invalid credentials');
}
return view('portal/login');
}
Staff Login:
#[Route('/staff/login')]
public static function staff_login(Request $request, array $params = [])
{
// Experience already set to 1 by pre_dispatch
if ($request->method() === 'POST') {
$email = $request->input('email');
$password = $request->input('password');
$user = User_Model::where('email', $email)
->where('is_staff', true)
->first();
if ($user && password_verify($password, $user->password)) {
Session::set_user($user); // Creates session with experience_id=1
return redirect('/staff/dashboard');
}
Rsx::flash_error('Invalid credentials');
}
return view('staff/login');
}
Result:
- Same user can be logged into /portal/ and /staff/ simultaneously
- Different user accounts (different user_id per experience)
- Single cookie token with multiple session records
- Automatic context switching based on URL
ORGANIZATION PICKER WORKFLOW (INCOMPLETE FEATURE)
Current State:
The framework currently supports setting site_id via Session::set_site()
and retrieving it via Session::get_site_id(). However, there is no
built-in UI workflow for organization selection.
Needed Implementation:
1. Organization Listing Route:
Route that shows all organizations user has access to.
Query Site_User_Model to find user's sites.
2. Organization Selection Handler:
Accepts site_id, verifies user has access, calls
Session::set_site_id() to persist choice.
3. Organization Switcher Component:
UI component in layout showing current organization with dropdown
to switch to different organization.
4. Middleware/Hook:
Check if user has site_id set. If not, redirect to organization
picker (for multi-tenant apps requiring explicit selection).
Example Implementation:
Route - Show Organizations:
#[Route('/select-organization')]
#[Auth('Permission::authenticated()')]
public static function select_organization(Request $request, array $params = [])
{
$user_id = Session::get_user_id();
$sites = Site_User_Model::where('user_id', $user_id)
->with('site')
->get();
return view('auth/select-organization', [
'sites' => $sites,
]);
}
Route - Set Organization:
#[Route('/set-organization/:site_id')]
#[Auth('Permission::authenticated()')]
public static function set_organization(Request $request, array $params = [])
{
$site_id = $params['site_id'];
$user_id = Session::get_user_id();
// Verify access
$site_user = Site_User_Model::where('user_id', $user_id)
->where('site_id', $site_id)
->first();
if (!$site_user) {
Rsx::flash_error('Access denied to this organization');
return redirect('/select-organization');
}
// Set site in session (persists)
Session::set_site_id($site_id);
Rsx::flash_success('Switched to ' . $site_user->site->name);
return redirect('/dashboard');
}
Main.php - Require Organization Selection:
public static function pre_dispatch(Request $request, array $params = [])
{
// For logged-in users on app routes
if (Session::is_logged_in() && str_starts_with($request->path(), 'app/')) {
// If no site selected, redirect to picker
if (Session::get_site_id() === 0) {
// Allow access to selection routes
if (!str_starts_with($request->path(), 'select-organization')) {
return redirect('/select-organization');
}
}
}
return null;
}
Future Enhancements:
- Remember last selected organization per user
- Auto-select if user only has one organization
- Organization switcher in navigation bar
- Organization-specific branding/theming
COMBINING SUBDOMAIN + EXPERIENCE
Use Case:
Customer portal and staff panel, each with their own subdomain,
both multi-tenant with subdomain-based site enforcement.
URL Structure:
Customer portals:
- acme-portal.yourapp.com
- widget-portal.yourapp.com
Staff panels:
- acme-staff.yourapp.com
- widget-staff.yourapp.com
Implementation:
public static function pre_dispatch(Request $request, array $params = [])
{
$host = $request->getHost();
$parts = explode('.', $host);
$subdomain = $parts[0] ?? '';
// Parse subdomain for tenant and experience
if (str_ends_with($subdomain, '-portal')) {
// Customer portal
$tenant = str_replace('-portal', '', $subdomain);
Session::set_experience_id(2);
} elseif (str_ends_with($subdomain, '-staff')) {
// Staff panel
$tenant = str_replace('-staff', '', $subdomain);
Session::set_experience_id(1);
} else {
// Default experience, no tenant enforcement
Session::set_experience_id(0);
return null;
}
// Enforce site based on tenant subdomain
$site = Site_Model::where('subdomain', $tenant)->first();
if ($site) {
Session::set_request_site_id_override($site->id);
} else {
return response('Tenant not found', 404);
}
return null;
}
Result:
- acme-portal.yourapp.com → experience_id=2, site_id=acme
- acme-staff.yourapp.com → experience_id=1, site_id=acme
- Same cookie works across both subdomains
- Different sessions, different users, same tenant
API REFERENCE
Experience Methods:
Session::set_experience_id(int $experience_id): void
Set authentication realm for current request. Clears cached
data and re-initializes session with new experience filter.
Does NOT persist to database - request-scoped only.
In CLI mode: sets static property.
Session::get_experience_id(): int
Get current experience ID. Default 0.
In CLI mode: returns static property.
Request-Scoped Site Override:
Session::set_request_site_id_override(int $site_id): void
Override site_id for current request only. Takes precedence
over session record site_id. Does NOT persist to database.
Clears cached site.
Session::clear_request_site_id_override(): void
Remove site override, return to session-based site_id.
Clears cached site.
Session::has_request_site_id_override(): bool
Check if request currently has site override active.
Modified Methods:
Session::get_site_id(): int
Returns request override if set, otherwise session site_id.
Respects this precedence:
1. Request override (set_request_site_id_override)
2. CLI static property
3. Session record site_id
4. Default 0
Session::init(): void
Now filters by experience_id when loading session:
WHERE session_token=? AND active=1 AND experience_id=?
MIGRATION PATH
Phase 1: Add Experience Support (Backward Compatible)
1. Run migration to add experience_id column (default 0)
2. All existing sessions have experience_id=0
3. Existing code continues working unchanged
Phase 2: Enable Experience-Based Features
1. Add experience detection in Main.php pre_dispatch
2. Create customer portal login routes
3. Create staff panel login routes
4. Test simultaneous logins to both experiences
Phase 3: Add Subdomain Enforcement
1. Add subdomain detection in Main.php pre_dispatch
2. Call Session::set_request_site_id_override()
3. Verify site filtering works correctly
Phase 4: Organization Picker (Optional)
1. Create organization selection routes
2. Add organization switcher UI component
3. Add middleware to require organization selection
EXAMPLES
Example 1 - Customer Portal + Staff Panel:
Main.php:
public static function pre_dispatch(Request $request, array $params = [])
{
if (str_starts_with($request->path(), 'portal/')) {
Session::set_experience_id(2);
} elseif (str_starts_with($request->path(), 'staff/')) {
Session::set_experience_id(1);
}
return null;
}
Usage:
User visits /portal/dashboard → logged in as customer (user_id=123)
User visits /staff/admin → logged in as staff (user_id=456)
Both work simultaneously with same cookie
Example 2 - Subdomain-Based Multi-Tenant:
Main.php:
public static function pre_dispatch(Request $request, array $params = [])
{
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
$site = Site_Model::where('subdomain', $subdomain)->first();
if ($site) {
Session::set_request_site_id_override($site->id);
}
return null;
}
Usage:
User visits acme.yourapp.com → site_id=1 (Acme)
User visits widget.yourapp.com → site_id=2 (Widget)
Site enforced by subdomain, not user choice
Example 3 - Organization Picker:
After Login:
$user_id = Session::get_user_id();
$sites = Site_User_Model::where('user_id', $user_id)->get();
// Show picker
if (count($sites) > 1) {
return view('select-organization', ['sites' => $sites]);
} else {
Session::set_site_id($sites[0]->site_id);
return redirect('/dashboard');
}
TROUBLESHOOTING
Wrong User Loaded:
Problem: Getting unexpected user when calling Session::get_user()
Solution:
- Check current experience_id: Session::get_experience_id()
- Verify experience is set correctly in Main.php pre_dispatch
- Confirm session record has correct experience_id in database
Subdomain Enforcement Not Working:
Problem: Session::get_site_id() returns session value, not override
Solution:
- Verify set_request_site_id_override() is called before get_site_id()
- Check Main.php pre_dispatch is executing
- Confirm subdomain parsing logic is correct
Can't Login to Multiple Experiences:
Problem: Logging into staff panel logs out customer portal
Solution:
- Verify experience_id column exists in sessions table
- Check experience_id is set BEFORE calling Session::set_user()
- Confirm different experiences create different session records
Organization Picker Not Required:
Problem: Users can access app without selecting organization
Solution:
- Add check in Main.php pre_dispatch
- Redirect to /select-organization if site_id === 0
- Allow access to selection routes to prevent redirect loop
SEE ALSO
session - RSX session management API reference
model - Model system with relationships
routing - Type-safe URL generation and route patterns

319
app/RSpade/man/bundle_api.txt Executable file
View File

@@ -0,0 +1,319 @@
BUNDLE_API(3) RSX Framework Manual BUNDLE_API(3)
NAME
Bundle - RSX asset compilation and management system
SYNOPSIS
use App\RSpade\Core\Bundle\Rsx_Bundle_Abstract;
class My_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
'jquery', // Module alias
'Bootstrap5_Bundle', // Bundle class
'rsx/app/myapp', // Directory
'rsx/lib/utils.js', // Specific file
],
];
}
}
// Render in Blade
{!! My_Bundle::render() !!}
DESCRIPTION
RSX Bundles provide a radically simplified asset compilation system
compared to Laravel Mix or Vite. Instead of webpack configurations,
JSON manifests, and build scripts, you define a simple PHP class
with an array of what to include. The framework handles everything
else automatically.
Unlike Laravel's approach where you configure webpack, define entry
points, set up hot module replacement, and manage complex build
pipelines, RSX Bundles use a single 'include' array that accepts
any mix of directories, files, NPM packages, or other bundles.
The system automatically determines file types, resolves dependencies,
and compiles everything.
The Bundle system integrates directly with the Manifest, automatically
including JavaScript stubs for controllers and models. SCSS files are
compiled transparently. Vendor and application code are automatically
split for optimal caching.
Key differences from Laravel Mix/Vite:
- Laravel: Complex webpack.mix.js or vite.config.js files
- RSX: Simple PHP class with an include array
- Laravel: Manual configuration of entry points and outputs
- RSX: Automatic detection and compilation
- Laravel: Separate processes for JS bundling and CSS compilation
- RSX: Unified system handles all asset types
- Laravel: Manual versioning and cache busting setup
- RSX: Automatic hash-based cache busting
Benefits:
- No JavaScript build configuration needed
- Works immediately without npm run dev/build
- Automatic vendor/app code splitting
- Integrated with PHP class discovery
- Zero configuration SCSS compilation
CREATING A BUNDLE
1. Extend Rsx_Bundle_Abstract
2. Implement define() method
3. Return configuration array with 'include' key
Example:
class Dashboard_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
'jquery',
'lodash',
'bootstrap5',
'rsx/app/dashboard',
],
'config' => [
'api_version' => '2.0',
],
];
}
}
INCLUDE TYPES
Module Aliases
Predefined in config/rsx.php:
'jquery', 'lodash', 'bootstrap5', 'vue', 'react'
Bundle Classes
Reference other bundles:
'Core_Bundle', 'Bootstrap5_Src_Bundle'
Bundle Aliases
Defined in config/rsx.php:
'bootstrap5_src' => Bootstrap5_Src_Bundle::class
Directories
Include all files recursively:
'rsx/app/dashboard'
Specific Files
Include individual files:
'rsx/lib/utils.js'
'rsx/theme/variables.scss'
NPM Modules
Include from node_modules:
'npm:axios'
'npm:moment'
CDN Assets
External resources:
'cdn:https://unpkg.com/library.js'
BUNDLE RENDERING
In Blade layouts/views:
{!! Dashboard_Bundle::render() !!}
Generates:
<link href="/bundles/Dashboard__vendor.abc123.css" rel="stylesheet">
<link href="/bundles/Dashboard__app.def456.css" rel="stylesheet">
<script src="/bundles/Dashboard__vendor.abc123.js"></script>
<script src="/bundles/Dashboard__app.def456.js"></script>
Never call from controllers - only from Blade files.
VENDOR/APP SPLIT
Files automatically split:
- vendor/: Files containing "vendor/" in path, NPM modules
- app/: Everything else
Benefits:
- Vendor files cached longer (rarely change)
- App files rebuilt on changes
- Smaller incremental builds
BUNDLE PROCESSORS
Transform files during compilation.
Configured globally in config/rsx.php.
Built-in processors:
- ScssProcessor: .scss → .css
- JqhtmlProcessor: .jqhtml → JavaScript
All processors receive ALL collected files,
decide what to process based on extension.
CREATING A PROCESSOR
class MyProcessor extends AbstractBundleProcessor
{
public static function get_name(): string
{
return 'myprocessor';
}
public static function get_extensions(): array
{
return ['myext']; // Extensions to process
}
public static function process(string $file, array $options = []): ?array
{
$content = file_get_contents($file);
// Transform content
$processed = transform($content);
return [
'content' => $processed,
'extension' => 'js', // Output extension
];
}
}
Register in config/rsx.php:
'bundle_processors' => [
App\RSpade\Processors\MyProcessor::class,
],
COMPILATION PROCESS
1. Resolve all includes to file list
2. Split into vendor/app buckets
3. Check cache (skip if unchanged)
4. Run processors on files
5. Add JavaScript stubs from manifest
6. Filter to JS/CSS only
7. Compile vendor and app separately
8. In production: concatenate into single files
JAVASCRIPT STUBS
Controllers with Ajax_Endpoint methods get stubs:
// Automatically included in bundles
class User_Controller {
static async get_profile(...args) {
return Ajax.call('User_Controller', 'get_profile', args);
}
}
Models with fetch() methods get stubs:
class User_Model {
static async fetch(id) {
return Ajax.model_fetch('User_Model', id);
}
}
CONFIGURATION
Bundle config added to window.rsxapp:
'config' => [
'feature_flags' => ['new_ui'],
'api_version' => '2.0',
]
Access in JavaScript:
if (window.rsxapp.config.feature_flags.includes('new_ui')) {
// New UI code
}
CACHING
Development:
- Vendor files cached until dependencies change
- App files rebuilt on any change
- Cache keys based on file hashes
Production:
- All files concatenated and minified
- Cache forever with hash in filename
- Rebuild only via rsx:bundle:compile
REQUIRED BUNDLES
Automatically included if used:
- jquery (if $ or jQuery detected)
- lodash (if _ detected)
- jqhtml (if .jqhtml files present)
FILE ORGANIZATION
storage/rsx-build/bundles/
├── Dashboard__vendor.abc123.js
├── Dashboard__vendor.abc123.css
├── Dashboard__app.def456.js
└── Dashboard__app.def456.css
Hash changes when content changes.
EXAMPLES
// Kitchen sink bundle
class App_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
// Required modules
'jquery',
'lodash',
'bootstrap5',
// Other bundles
'Core_Bundle',
// Application code
'rsx/app',
'rsx/lib',
// Specific overrides
'rsx/theme/variables.scss',
// NPM packages
'npm:axios',
'npm:chart.js',
],
'config' => [
'app_name' => 'MyApp',
'version' => '1.0.0',
],
];
}
}
// Module-specific bundle
class Admin_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
__DIR__, // Include bundle's directory
'rsx/lib/admin',
],
];
}
}
CIRCULAR DEPENDENCIES
Framework detects and prevents circular includes:
- A includes B, B includes A = error
- Shows clear error with dependency chain
TROUBLESHOOTING
Bundle not updating:
php artisan rsx:bundle:compile My_Bundle --force
Missing files:
Check paths are relative to project root.
Verify files exist in manifest.
Processor not running:
Check processor registered in config.
Verify file extension matches.
SEE ALSO
manifest_api(3), jqhtml(3), controller(3)
RSX Framework 2025-09-17 BUNDLE_API(3)

347
app/RSpade/man/caching.txt Executable file
View File

@@ -0,0 +1,347 @@
CACHING IN RSPADE
==================
RSpade provides two caching strategies for different use cases:
1. **Request-scoped caching** - RsxCache::once()
2. **Build-scoped caching** - RsxCache::remember()
Both use closures for cache-or-generate patterns, providing clean syntax
for wrapping expensive operations.
REQUEST-SCOPED CACHING (RsxCache::once)
========================================
Simplest caching strategy - stores values in static property for request duration.
WHEN TO USE:
- Expensive calculations called multiple times per request
- Data that doesn't need persistence across requests
- No Redis overhead needed
- No locking needed (single process, single request)
CHARACTERISTICS:
- Lives in PHP memory (static array)
- No serialization overhead
- Zero lock contention
- Cleared automatically at end of request
- No expiration (request lifetime only)
SYNTAX:
$data = RsxCache::once($key, function() {
return expensive_calculation();
});
EXAMPLES:
// Avoid re-calculating same value multiple times in request
function get_user_permissions($user_id) {
return RsxCache::once("user_permissions:{$user_id}", function() use ($user_id) {
// This expensive query only runs once per request
return User_Model::find($user_id)
->permissions()
->with('roles')
->get();
});
}
// Called multiple times, but calculation only happens once
$perms1 = get_user_permissions(123); // Runs query
$perms2 = get_user_permissions(123); // Returns cached
$perms3 = get_user_permissions(123); // Returns cached
// Safe to call from different parts of codebase
class Dashboard_Controller {
public static function index(Request $request, array $params = []) {
$perms = get_user_permissions($user_id); // May use cache
// ...
}
}
class Sidebar_Component extends Jqhtml_Component {
public function on_load() {
$perms = get_user_permissions($this->args->user_id); // May use cache
}
}
BUILD-SCOPED CACHING (RsxCache::remember)
==========================================
Redis-based caching with advisory locking and automatic build key prefixing.
WHEN TO USE:
- Expensive operations that don't change often
- Data shared across multiple requests/processes
- Need to prevent cache stampede
- Want automatic invalidation on code deployment
CHARACTERISTICS:
- Stored in Redis with build key prefix
- Survives across requests until manifest rebuild
- Advisory write lock prevents stampede
- Automatic double-check after lock acquisition
- Optional expiration time
- Serialization/deserialization automatic
SYNTAX:
// Never expires (default)
$data = RsxCache::remember($key, function() {
return expensive_operation();
});
// Expires after specified seconds
$data = RsxCache::remember($key, function() {
return expensive_operation();
}, $seconds);
LOCKING BEHAVIOR:
When cache miss occurs:
1. Fast check (no lock) - returns if cached
2. Acquire write lock on cache key
3. Check again (another process may have built it)
4. Execute callback if still missing
5. Store result in Redis
6. Release lock
This prevents stampede: if 100 requests hit at once, only one builds cache
while others wait for lock, then use the built cache.
EXAMPLES:
// Cache expensive API response forever (until deployment)
function get_product_catalog() {
return RsxCache::remember('product_catalog', function() {
return External_Api::fetch_all_products();
});
}
// Cache with 1 hour expiration
function get_trending_products() {
return RsxCache::remember('trending_products', function() {
return Product_Model::where('views', '>', 1000)
->orderBy('views', 'desc')
->limit(10)
->get();
}, RsxCache::HOUR);
}
// Complex calculation cached per user
function get_user_dashboard_data($user_id) {
return RsxCache::remember("dashboard_data:{$user_id}", function() use ($user_id) {
return [
'stats' => User_Stats::calculate($user_id),
'recent_activity' => Activity::for_user($user_id)->limit(20)->get(),
'recommendations' => Recommendation_Engine::generate($user_id),
];
}, RsxCache::DAY);
}
// Expensive join query
function get_all_users_with_permissions() {
return RsxCache::remember('users_with_permissions', function() {
// This might take 500ms, but only runs once per deployment
return DB::select("
SELECT u.*, GROUP_CONCAT(p.name) as permissions
FROM users u
LEFT JOIN user_permissions up ON u.id = up.user_id
LEFT JOIN permissions p ON up.permission_id = p.id
GROUP BY u.id
");
});
}
EXPIRATION CONSTANTS
====================
RsxCache provides constants for common expiration times:
RsxCache::NO_EXPIRATION // 0 - never expire (default)
RsxCache::HOUR // 3600 seconds
RsxCache::DAY // 86400 seconds
RsxCache::WEEK // 604800 seconds
EXAMPLES:
RsxCache::remember($key, $callback); // Never expires
RsxCache::remember($key, $callback, null); // Never expires
RsxCache::remember($key, $callback, RsxCache::HOUR); // 1 hour
RsxCache::remember($key, $callback, RsxCache::DAY); // 1 day
RsxCache::remember($key, $callback, 300); // 5 minutes
RsxCache::remember($key, $callback, 86400 * 30); // 30 days
CACHE KEY NAMING
================
Use descriptive keys with context:
GOOD:
user:123:permissions
product_catalog:category_5
dashboard_stats:2024-10
api_response:github_user:hansonw
BAD:
permissions
cache1
temp
data
CACHE INVALIDATION
==================
BUILD-SCOPED CACHE:
- Automatically cleared on manifest rebuild (code deployment)
- Build key prefix ensures old cache invisible after deployment
- Manual clear: RsxCache::delete($key)
- Clear all: RsxCache::clear()
REQUEST-SCOPED CACHE:
- Automatically cleared at end of request
- No manual clearing needed/possible
WHEN TO USE WHICH
=================
USE RsxCache::once() FOR:
✓ Expensive calculations called multiple times in one request
✓ Avoiding duplicate work within single request
✓ Simple in-memory caching
✓ Getter functions that might be called from multiple places
✓ Component data loading that might trigger multiple times
USE RsxCache::remember() FOR:
✓ Database queries that don't change often
✓ External API calls
✓ Expensive computations shared across requests
✓ Product catalogs, configuration data
✓ Analytics/reporting data
✓ Anything where cache stampede is a concern
ANTI-PATTERNS
=============
DON'T cache user-specific sensitive data without user ID in key:
❌ RsxCache::remember('user_data', fn() => get_current_user_data());
✅ RsxCache::remember("user_data:{$user_id}", fn() => get_user_data($user_id));
DON'T use request-scoped cache for data that should persist:
❌ RsxCache::once('product_catalog', fn() => fetch_all_products());
✅ RsxCache::remember('product_catalog', fn() => fetch_all_products());
DON'T use short expiration when you want build-scoped:
❌ RsxCache::remember($key, $callback, 60); // Why expire if it's build data?
✅ RsxCache::remember($key, $callback); // Let manifest rebuild clear it
DON'T wrap fast operations:
❌ RsxCache::once('user_id', fn() => RsxAuth::id());
✅ $user_id = RsxAuth::id(); // Already fast, no caching needed
DON'T use external side effects in callback:
❌ RsxCache::remember($key, function() {
Log::info('Building cache'); // Don't do this
send_email_notification(); // Definitely don't do this
return calculate_data();
});
ADVANCED PATTERNS
=================
LAYERED CACHING:
Combine once() and remember() for two-level caching:
function get_user_permissions($user_id) {
// Request-scoped cache (fastest)
return RsxCache::once("user_permissions:{$user_id}", function() use ($user_id) {
// Build-scoped cache (fast)
return RsxCache::remember("user_permissions:{$user_id}", function() use ($user_id) {
// Database query (slow)
return User_Model::find($user_id)->permissions()->get();
}, RsxCache::HOUR);
});
}
First call: Database query → Store in Redis → Store in memory → Return
Second call (same request): Memory → Return
Third call (different request): Redis → Store in memory → Return
CONDITIONAL CACHING:
function get_data($use_cache = true) {
if (!$use_cache) {
return fetch_fresh_data();
}
return RsxCache::remember('data', function() {
return fetch_fresh_data();
});
}
CACHE WARMING:
// In deployment script or scheduled job
function warm_critical_caches() {
RsxCache::remember('product_catalog', fn() => Product::all());
RsxCache::remember('site_config', fn() => Config::load_all());
RsxCache::remember('menu_structure', fn() => Menu::build_tree());
}
DEBUGGING
=========
Check if data is cached:
if (RsxCache::exists($key)) {
// Cached
}
View cache statistics:
$stats = RsxCache::stats();
// Returns: total_keys, used_memory, maxmemory, etc.
Manually clear cache:
RsxCache::delete($key); // Clear specific key
RsxCache::clear(); // Clear all build-scoped cache
Note: Request-scoped cache (once) is not inspectable - it's just a static array
that lives for the request duration.
PERFORMANCE CHARACTERISTICS
============================
RsxCache::once():
- Speed: Microseconds (array lookup)
- Overhead: None (simple array)
- Scalability: Single process only
RsxCache::remember():
- Speed: 1-2ms (Redis, Unix socket)
- Overhead: Serialization + lock acquisition on miss
- Scalability: Shared across all processes
Cache build with lock:
- First miss: Lock wait + callback execution + storage
- Concurrent misses: All wait for lock, first builds, rest use result
- Hit after build: 1-2ms (no lock needed)
RELATED SYSTEMS
===============
See also:
- php artisan rsx:man locking - RsxLocks advisory locking system
- php artisan rsx:man manifest - Manifest build key system
- /system/app/RSpade/Core/REDIS_USAGE.md - Redis database allocation

View File

@@ -0,0 +1,364 @@
NAME
coding_standards - RSX framework coding conventions and standards
SYNOPSIS
Naming conventions, code organization, and design philosophy for RSX development
DESCRIPTION
The RSX framework enforces specific coding standards to maintain consistency,
readability, and framework compatibility. These standards ensure code is
maintainable and follows the framework's architectural principles.
NAMING CONVENTIONS
Methods and Variables:
- Use underscore_case for all methods and variables
- Examples: user_name, calculate_total(), get_user_data()
Constants:
- Use UPPERCASE_WITH_UNDERSCORES
- Examples: MAX_UPLOAD_SIZE, DEFAULT_TIMEOUT
Classes in /rsx/ Directory:
- Use Like_This_With_Underscores format
- Examples: Index_Controller, User_Model, Frontend_Bundle
- Matches RSX naming patterns for auto-discovery
Classes in /system/app/RSpade/ (Framework):
- Use Like_This_With_Underscores where appropriate
- Exception: When extending Laravel classes, use Laravel's PascalCase
- Default for framework internals: PascalCase (existing Laravel convention)
Critical Rule - Case-Sensitive Files:
NEVER create files with same name but different case in same directory.
Examples to avoid: Helpers.php and helpers.php
This breaks Windows/macOS compatibility and triggers critical errors.
FILENAME CONVENTION EXCEPTIONS
@FILENAME-CONVENTION-EXCEPTION Marker:
Add this marker anywhere in a file to exempt it from filename matching rules.
The framework will skip all filename convention checks for that file.
Use cases:
- Legacy files being migrated that cannot be renamed yet
- Special cases that require non-standard naming
- Files with intentional naming that violates conventions
Example:
<?php
// @FILENAME-CONVENTION-EXCEPTION
// This file has a non-standard name for legacy reasons
class User_Model { ... }
Note: File must contain this exact text (case-sensitive) anywhere in contents.
AUTOMATIC FILE RENAMING (DEVELOPMENT)
Configuration (config/rsx.php):
'development' => [
// Auto-rename files to match RSX naming conventions
'auto_rename_files' => false, // Set to true to enable
// Globally disable all filename convention checks
'ignore_filename_convention' => false, // Set to true to disable
]
Auto-Rename Behavior (auto_rename_files = true):
- During manifest build, files in ./rsx are automatically renamed
- Only renames if target filename doesn't already exist
- Manifest rebuilds after rename to reflect new file paths
- Files with @FILENAME-CONVENTION-EXCEPTION are never renamed
Examples of auto-renaming:
- TestComponent1.jqhtml → test_component_1.jqhtml
- UserController.php → user_controller.php
- MyView.blade.php → my_view.blade.php
If target exists: Normal violation thrown (conflict detected)
Global Disable (ignore_filename_convention = true):
- Disables ALL filename convention checks
- No violations thrown for any files
- Useful for projects with existing non-standard naming
- More aggressive than @FILENAME-CONVENTION-EXCEPTION marker
RSPADE FRAMEWORK INTERNAL METHOD NAMING
For files in /system/app/RSpade/ only:
Private and Protected Methods:
- MUST start with underscore: _internal_method()
- Purpose: Immediately identifies framework-internal methods
- Application code prohibition: Never call methods starting with _
Public API Methods:
- Must NOT start with underscore
- These are stable public API for applications
- Safe for RSX applications to use
Example:
// In /system/app/RSpade/Core/Something.php
public static function public_api_method() { } // OK for apps
private static function _internal_helper() { } // Framework only
protected static function _process_data() { } // Framework only
Rationale:
- Methods starting with _ may change without notice in updates
- Clear separation between stable API and internal implementation
- Prevents applications from depending on internal methods
CLASS DESIGN PHILOSOPHY
Static by Default:
- All classes should be static unless specific reason to instantiate
- Classes are namespacing tools, not OOP for the sake of OOP
- Direct access from anywhere without instantiation
Exceptions for Instances:
- ORM/Model records (represent database rows)
- Resource handles (file handles, connections)
- Service connectors needing mocking (AWS, payment gateways)
- Artisan commands
Avoid Dependency Injection:
- No DI patterns or factory classes
- Exception: Service connectors that need mocking for tests
- Utility classes, helpers, managers, processors should be static
STRING MANIPULATION PHILOSOPHY
For Source Code Analysis: NO REGEX
- Never use regex to parse PHP, JavaScript, or any programming language
- Why regex fails for source code:
* Cannot distinguish code from comments
* Cannot understand syntactic context (strings, blocks, nesting)
* Breaks when code formatting changes
* Creates unmaintainable parsing logic
- Use AST parsers instead: Extend PHP/JS AST functionality in manifest
system to generate metadata for runtime analysis
For General String Operations: Prefer Verbose Clarity
- Default approach: Use explicit string functions over regex
- Prefer readable multi-line operations:
// ✅ GOOD - Clear and maintainable
$parts = explode('/', $path);
$filename = end($parts);
$extension = substr($filename, strrpos($filename, '.') + 1);
// ❌ AVOID - Unreadable regex
preg_match('/\/([^\/]+)\.([^.]+)$/', $path, $matches);
When Simple Regex IS Appropriate:
- Well-established validation patterns using proven libraries
- Very simple matching: single character classes, basic patterns
- Performance-critical operations where regex proven faster AND simple
Implementation Process:
1. Always propose solution to user first before implementing regex
2. Justify why string functions aren't sufficient
3. Document regex purpose in plain English
4. Prefer library solutions over custom regex patterns
Source Code Analysis Guidelines:
- Extend manifest AST parsers to capture needed metadata
- Add metadata to manifest cache for runtime analysis
- Use reflection and AST data instead of pattern matching
- Consult user before any source code parsing implementation
FUNCTION ORGANIZATION PHILOSOPHY
Prefer Inline Code with Comments:
- Comment above a few lines better than separate methods
- Readability from clear comments and logical flow
- Not arbitrary function boundaries
Long Functions Are Acceptable:
- When steps are sequential and single-purpose
- Clear comments explain the flow
- Related logic stays together
Create Sub-Functions Only When:
- Code called from multiple places or multiple times
- Operation is significantly complex logical subset
- Code needs independent unit testing
- Abstraction genuinely improves understanding
Avoid Functions for the Sake of Functions:
- Don't create method to wrap file_get_contents($file)
- Don't split sequential operations arbitrarily
- Goal: Keep related logic together, not create maze of methods
Sequential Operations Belong Together:
- If step B always follows step A, same function with comments
- Not arbitrarily split across multiple methods
FILE STRUCTURE RESTRICTIONS
PHP Files Without Classes (in ./rsx directory):
PHP utility files without classes may only contain at global scope:
- Function definitions
- define() calls for constants
- namespace declarations
- use statements
- declare() directives
- Comments
NOT allowed at global scope:
- Control structures (if, for, while, switch)
- Function calls (except define())
- Variable assignments
- Object instantiation (new)
- Include/require statements
- Echo/print statements
- Exit/die statements
Rationale: Classless files are loaded on every request and should
only define functions/constants, not execute code. Code that runs
on every request should be in Main::init(), pre_dispatch() methods,
or implemented in Laravel outside RSX.
Example Valid PHP Utility File:
<?php
namespace App\Utils;
use App\Models\User;
declare(strict_types=1);
define('MAX_LENGTH', 255);
function format_name(string $name): string {
return ucfirst(trim($name)); // Any code allowed inside functions
}
PHP Files With Classes (in ./rsx directory):
PHP files with classes may only contain at global scope:
- namespace declarations
- use statements
- Comments
- include/require statements with path restrictions:
* Allowed: paths not starting with rsx/
* Allowed: paths containing /resource/
* NOT allowed: paths starting with rsx/ without /resource/
NOT allowed at global scope:
- Function definitions
- define() calls for constants
Example Valid PHP Class File:
<?php
namespace Rsx\App;
use App\Models\User;
require_once 'vendor/autoload.php'; // OK
include 'rsx/app/resource/config.php'; // OK - has /resource/
class MyClass {
// Class implementation
}
JavaScript Files Without Classes:
JavaScript utility files without ES6 classes may only contain:
- Function declarations (standard or arrow)
- Const variables with static values (no function calls)
- Functions marked with @decorator
Static values include:
- Literals (strings, numbers, booleans, null)
- Binary/unary expressions (e.g., 3 * 365)
- Static arrays and objects with literal values
Example Valid JavaScript Utility File:
// Valid constants - static values only
const DAYS_IN_YEAR = 365;
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
// Valid function
function formatName(name) {
return name.trim().toUpperCase();
}
// Invalid - function call at global scope
const timestamp = Date.now(); // NOT ALLOWED
Enforcement:
- Validated at manifest build time
- Clear error messages with line numbers
- Build fails on violations
- Ensures clean, predictable code structure
TEMPORARY FILES CONVENTION
Always Suffix with -temp:
- Pattern: filename-temp.extension
- Examples: test-temp.php, debug-script-temp.js, migration-fix-temp.sql
Applies To:
- One-off scripts
- Test files not part of test suite
- Temporary implementations or workarounds
- Debug utilities
- Any file that should be removed later
Rules:
- Purpose: Easy identification of files needing cleanup
- Never commit -temp files unless explicitly requested
- Helps prevent temporary code from becoming permanent
ROUTING RULES
Restricted HTTP Methods:
- Only GET and POST methods allowed
- PUT, PATCH, DELETE, OPTIONS throw exceptions
- Keeps routing simple and explicit
No Resource Routes:
- Route::resource() and Route::apiResource() throw exceptions
- Explicit route definitions only
- Each route defined individually for clarity
GIT WORKFLOW
Staging:
- Always use: git add -A
- Never selective staging
- Ensures all changes are included
Submodules:
- Located in /internal-libs/
- Examples: jqhtml, rsx-scss-lint
EXAMPLES
Correct Naming:
class User_Profile_Controller {
public static function show_profile(Request $request) {
$user_data = static::_get_user_info($request->user_id);
return view('profile', compact('user_data'));
}
private static function _get_user_info($user_id) {
// Framework internal method
}
}
String Processing:
// Preferred approach
$parts = explode('/', $path);
$filename = end($parts);
$extension = substr($filename, strrpos($filename, '.') + 1);
// Avoid unless necessary
preg_match('/\/([^\/]+)\.([^.]+)$/', $path, $matches);
Function Organization:
// Good: Related steps together with comments
public static function process_upload($file) {
// Validate file type and size
if (!in_array($file->extension(), ['jpg', 'png'])) {
throw new Exception('Invalid file type');
}
// Generate unique filename
$filename = uniqid() . '.' . $file->extension();
// Store file and create database record
$path = $file->storeAs('uploads', $filename);
Upload_Model::create(['filename' => $filename, 'path' => $path]);
return $filename;
}
SEE ALSO
error_handling - Error handling patterns and shouldnt_happen()
config_rsx - Framework configuration standards

171
app/RSpade/man/config_rsx.txt Executable file
View File

@@ -0,0 +1,171 @@
NAME
config/rsx.php - RSX framework configuration file
SYNOPSIS
Configuration for all RSX framework features in a single location
DESCRIPTION
The config/rsx.php file centralizes all RSX framework configuration.
Unlike Laravel which spreads configuration across multiple files,
RSX consolidates framework settings into one place for easier management.
This configuration controls:
- Manifest building and caching
- Bundle compilation settings
- Console debug output
- Browser error logging
- Development mode features
- Path configurations
STRUCTURE
The configuration file is organized into logical sections:
manifest Manifest system settings
bundles Bundle compilation configuration
console_debug Debug output configuration
log_browser_errors Client-side error logging
development Development mode features
paths Directory path overrides
MANIFEST CONFIGURATION
'manifest' => [
'cache_enabled' => env('RSX_MANIFEST_CACHE', true),
'auto_rebuild' => env('RSX_MANIFEST_AUTO_REBUILD', true),
'scan_directories' => ['/rsx', '/system/app/RSpade'],
'cache_path' => storage_path('rsx-build/manifest_data.php'),
]
cache_enabled Enable manifest caching in production
auto_rebuild Auto-rebuild on file changes (dev only)
scan_directories Directories to scan for RSX files
cache_path Where to store compiled manifest
BUNDLE CONFIGURATION
'bundles' => [
'minify' => env('RSX_BUNDLE_MINIFY', false),
'source_maps' => env('RSX_BUNDLE_SOURCEMAPS', true),
'cache_path' => storage_path('rsx-build/bundles'),
'aliases' => [
'jquery' => 'node_modules/jquery/dist/jquery.js',
'lodash' => 'node_modules/lodash/lodash.js',
],
]
minify Minify JavaScript/CSS in bundles
source_maps Generate source maps for debugging
cache_path Directory for compiled bundles
aliases Module aliases for bundle includes
CONSOLE DEBUG CONFIGURATION
'console_debug' => [
'enabled' => env('CONSOLE_DEBUG_ENABLED', true),
'outputs' => [
'cli' => false, # stderr in CLI mode
'web' => true, # Browser console
'ajax' => true, # AJAX responses
'laravel_log' => false, # Laravel log file
],
'filter_mode' => 'all', # all|whitelist|blacklist|specific
'specific_channel' => null,
'whitelist' => [],
'blacklist' => [],
'include_benchmark' => false,
'include_location' => false,
'include_backtrace' => false,
]
See: php artisan rsx:man console_debug for detailed documentation
BROWSER ERROR LOGGING
'log_browser_errors' => env('LOG_BROWSER_ERRORS', false),
When enabled, JavaScript errors are automatically sent to Laravel log.
Includes stack traces, source locations, and user agent information.
DEVELOPMENT FEATURES
'development' => [
'show_queries' => env('RSX_SHOW_QUERIES', false),
'log_manifest_builds' => env('RSX_LOG_MANIFEST', false),
'verbose_errors' => env('RSX_VERBOSE_ERRORS', true),
]
show_queries Log all database queries
log_manifest_builds Log manifest rebuild events
verbose_errors Show detailed error information
PATH CONFIGURATION
'paths' => [
'rsx_root' => base_path('rsx'),
'build_dir' => storage_path('rsx-build'),
'temp_dir' => storage_path('rsx-tmp'),
'locks_dir' => storage_path('rsx-locks'),
]
rsx_root RSX application root directory
build_dir Compiled assets directory
temp_dir Temporary files directory
locks_dir Lock files directory
ENVIRONMENT VARIABLES
Most settings can be overridden with environment variables:
RSX_MANIFEST_CACHE=false Disable manifest caching
RSX_BUNDLE_MINIFY=true Enable minification
CONSOLE_DEBUG_ENABLED=true Enable console_debug
LOG_BROWSER_ERRORS=true Log browser errors
RSX_SHOW_QUERIES=true Show database queries
EXAMPLES
Enable all debug features for development:
CONSOLE_DEBUG_ENABLED=true
CONSOLE_DEBUG_CLI=true
LOG_BROWSER_ERRORS=true
RSX_VERBOSE_ERRORS=true
RSX_SHOW_QUERIES=true
Production settings:
RSX_MANIFEST_CACHE=true
RSX_MANIFEST_AUTO_REBUILD=false
RSX_BUNDLE_MINIFY=true
CONSOLE_DEBUG_ENABLED=false
LOG_BROWSER_ERRORS=false
Filter console_debug to specific channel:
CONSOLE_DEBUG_FILTER=AUTH
CONSOLE_DEBUG_BENCHMARK=true
RSX VS LARAVEL
Laravel configuration:
- Spread across config/*.php files
- Service providers define features
- Package configuration published separately
- Environment files control everything
RSX configuration:
- Centralized in config/rsx.php
- Manifest controls discovery
- Framework features built-in
- Environment variables for overrides
TROUBLESHOOTING
Manifest not rebuilding:
- Check RSX_MANIFEST_AUTO_REBUILD=true
- Clear cache: php artisan rsx:clean
- Manual rebuild: php artisan rsx:manifest:build
Console_debug not showing:
- Check CONSOLE_DEBUG_ENABLED=true
- Verify filter settings
- Use php artisan rsx:debug --console-log
Bundles not updating:
- Clear bundle cache in storage/rsx-build/bundles
- Check file permissions on build directories
SEE ALSO
console_debug - Debug output system documentation
manifest_api - Manifest system API reference
bundle_api - Bundle compilation documentation

312
app/RSpade/man/console_debug.txt Executable file
View File

@@ -0,0 +1,312 @@
# CONSOLE_DEBUG
A powerful debugging system for RSpade applications that provides channel-based debug output across PHP and JavaScript with flexible filtering and configuration.
## OVERVIEW
The console_debug() function provides a unified debugging interface that works identically in both PHP and JavaScript. It outputs categorized debug messages that can be filtered by channel, making it easy to focus on specific areas of your application during development.
## BASIC USAGE
### PHP
```php
console_debug("AUTH", "User login attempt", $username);
console_debug("DISPATCH", "Processing route:", $route, $params);
console_debug("DB", "Query executed:", $sql);
```
### JavaScript
```javascript
Debugger.console_debug("UI", "Button clicked", buttonId);
Debugger.console_debug("AJAX", "API call", endpoint, params);
Debugger.console_debug("DOM", "Element rendered", element);
```
## CHANNELS
Debug messages are organized into channels. Common channels include:
• AUTH - Authentication and authorization events
• DISPATCH - Route dispatching and controller execution
• DB - Database queries and operations
• CACHE - Cache operations (get, set, clear)
• BUNDLE - JavaScript/CSS bundle compilation
• UI - User interface events and interactions
• AJAX - AJAX calls and API requests
• DOM - DOM manipulation and rendering
• TEST - Test-specific debug output
• BENCHMARK - Performance timing information
You can create custom channels by simply using them in your console_debug() calls.
## CONFIGURATION
Configuration is managed in `config/rsx.php` under the `console_debug` section:
```php
'console_debug' => [
'enabled' => env('CONSOLE_DEBUG_ENABLED', false),
'outputs' => [
'cli' => env('CONSOLE_DEBUG_CLI', false),
'web' => env('CONSOLE_DEBUG_WEB', true),
'ajax' => env('CONSOLE_DEBUG_AJAX', true),
'laravel_log' => env('CONSOLE_DEBUG_LOG', true),
],
'filter_mode' => env('CONSOLE_DEBUG_FILTER_MODE', 'whitelist'),
'whitelist' => ['AUTH', 'DISPATCH', 'UI', 'AJAX'],
'blacklist' => [],
'include_benchmark' => env('CONSOLE_DEBUG_BENCHMARK', false),
],
```
## FILTER MODES
The system supports four filter modes:
### all
Shows all console_debug messages regardless of channel.
### whitelist (default)
Only shows messages from channels listed in the whitelist array.
### blacklist
Shows all messages except those from channels in the blacklist array.
### specific
Only shows messages from a single specific channel set via CONSOLE_DEBUG_SPECIFIC.
## ENVIRONMENT VARIABLES
You can override configuration using environment variables:
• CONSOLE_DEBUG_ENABLED - Master switch (true/false)
• CONSOLE_DEBUG_FILTER - Override filter with channel name or ALL
• CONSOLE_DEBUG_CLI - Enable CLI output (true/false)
• CONSOLE_DEBUG_WEB - Enable browser console output (true/false)
• CONSOLE_DEBUG_AJAX - Include in AJAX responses (true/false)
• CONSOLE_DEBUG_LOG - Write to Laravel log (true/false)
• CONSOLE_DEBUG_BENCHMARK - Include timing prefixes (true/false)
• CONSOLE_DEBUG_LOCATION - Include file location (true/false)
• CONSOLE_DEBUG_BACKTRACE - Include stack trace (true/false)
## QUICK DEBUGGING
For quick debugging sessions, use the CONSOLE_DEBUG_FILTER environment variable:
```bash
# Show only AUTH channel
CONSOLE_DEBUG_FILTER=AUTH php artisan serve
# Show all channels
CONSOLE_DEBUG_FILTER=ALL php artisan serve
# With benchmarking
CONSOLE_DEBUG_BENCHMARK=true php artisan serve
```
## OUTPUT DESTINATIONS
### CLI Output
When CONSOLE_DEBUG_CLI is enabled, messages appear in stderr with cyan coloring:
```
[DISPATCH] Processing route: /dashboard
```
### Browser Console
When CONSOLE_DEBUG_WEB is enabled, messages appear in the browser's developer console:
```javascript
DISPATCH Processing route: /dashboard
```
### Laravel Log
When CONSOLE_DEBUG_LOG is enabled, messages are written to storage/logs/laravel.log:
```
[2024-01-15 10:23:45] local.DEBUG: [DISPATCH] Processing route: /dashboard
```
### AJAX Responses
When CONSOLE_DEBUG_AJAX is enabled, debug messages are included in AJAX response headers and automatically displayed in the browser console.
## BENCHMARKING
Enable benchmark timing to see how long operations take:
```bash
CONSOLE_DEBUG_BENCHMARK=true php artisan serve
```
Output includes timing in seconds since request start:
```
[12.3456] [AUTH] User authenticated: user@example.com
[12.4567] [DB] Query executed in 0.111s
```
## PLAYWRIGHT TESTING
The console_debug system integrates with Playwright testing via special headers:
• X-Console-Debug-Enabled - Force enable/disable
• X-Console-Debug-Filter - Override filter for test
• X-Console-Debug-Benchmark - Enable timing for test
• X-Console-Debug-All - Show all channels for test
These headers only work from localhost in non-production environments.
## JAVASCRIPT INTEGRATION
### Auto-batching
JavaScript console_debug messages are automatically batched and sent to the server every 2 seconds if laravel_log output is enabled.
### Error Logging
JavaScript errors are automatically captured and logged to the Laravel log when LOG_BROWSER_ERRORS is enabled in the environment.
### Synchronous Display
Messages appear immediately in the browser console, while server logging happens asynchronously in batches.
## BEST PRACTICES
### 1. Use Descriptive Channels
Choose channel names that clearly indicate the area of code:
```php
console_debug("PAYMENT_GATEWAY", "Processing transaction", $txn_id);
console_debug("EMAIL_QUEUE", "Message queued", $recipient);
```
### 2. Include Context
Pass relevant variables as additional arguments:
```php
console_debug("AUTH", "Login failed", $username, $ip_address, $attempt_count);
```
### 3. Avoid Sensitive Data
Never log passwords, tokens, or other sensitive information:
```php
// BAD
console_debug("AUTH", "Login attempt", $username, $password);
// GOOD
console_debug("AUTH", "Login attempt", $username, "***");
```
### 4. Use for Development Only
Console_debug is automatically disabled in production. Don't rely on it for production logging.
### 5. Clean Up After Debugging
Remove or comment out console_debug calls once you've fixed the issue:
```php
// console_debug("DEBUG", "Temporary debug point", $data);
```
## TROUBLESHOOTING
### Messages Not Appearing
1. Check if console_debug is enabled:
```bash
php artisan tinker
>>> config('rsx.console_debug.enabled')
```
2. Check your filter mode and channel:
```bash
>>> config('rsx.console_debug.filter_mode')
>>> config('rsx.console_debug.whitelist')
```
3. Use CONSOLE_DEBUG_FILTER=ALL to bypass filtering:
```bash
CONSOLE_DEBUG_FILTER=ALL php artisan rsx:debug /
```
### Too Many Messages
1. Use whitelist mode with specific channels:
```php
'filter_mode' => 'whitelist',
'whitelist' => ['AUTH', 'ERROR'],
```
2. Use environment variable for temporary filtering:
```bash
CONSOLE_DEBUG_FILTER=AUTH php artisan serve
```
### JavaScript Messages Not Logged
1. Ensure laravel_log output is enabled:
```php
'outputs' => [
'laravel_log' => true,
],
```
2. Check browser console for errors in batch submission.
## EXAMPLES
### Debugging Authentication Flow
```php
public function login(Request $request)
{
console_debug("AUTH", "Login attempt", $request->ip());
$credentials = $request->only(['email', 'password']);
console_debug("AUTH", "Validating credentials for", $credentials['email']);
if (Auth::attempt($credentials)) {
console_debug("AUTH", "Login successful", Auth::id());
return redirect('/dashboard');
}
console_debug("AUTH", "Login failed", $credentials['email']);
return back()->withErrors(['email' => 'Invalid credentials']);
}
```
### Debugging AJAX Calls
```javascript
class UserController {
static async fetch_profile(userId) {
Debugger.console_debug("AJAX", "Fetching user profile", userId);
try {
const user = await User_Model.fetch(userId);
Debugger.console_debug("AJAX", "Profile fetched", user);
return user;
} catch (error) {
Debugger.console_debug("AJAX", "Failed to fetch profile", error);
throw error;
}
}
}
```
### Debugging Database Queries
```php
public static function get_active_users()
{
console_debug("DB", "Fetching active users");
$query = static::where('is_active', true)
->where('last_login', '>', now()->subDays(30));
console_debug("DB", "Query SQL:", $query->toSql());
console_debug("DB", "Query bindings:", $query->getBindings());
$users = $query->get();
console_debug("DB", "Found users:", $users->count());
return $users;
}
```
## SEE ALSO
• php artisan rsx:debug - Test routes with console_debug enabled
• system/config/rsx.php - Main configuration file
• system/app/RSpade/Core/Debug/Debugger.php - PHP implementation
• system/app/RSpade/Core/Js/Debugger.js - JavaScript implementation
---
For more information, visit the RSX documentation or run:
php artisan rsx:man console_debug

458
app/RSpade/man/controller.txt Executable file
View File

@@ -0,0 +1,458 @@
CONTROLLER(3) RSX Framework Manual CONTROLLER(3)
NAME
Controller - RSX request handling and routing system
SYNOPSIS
use App\RSpade\Core\Controller\Rsx_Controller_Abstract;
class User_Controller extends Rsx_Controller_Abstract
{
#[Auth('Permission::authenticated()')]
#[Route('/users', methods: ['GET'])]
public static function index(Request $request, array $params = [])
{
return rsx_view('User_List');
}
#[Auth('Permission::authenticated()')]
#[Ajax_Endpoint]
public static function get_profile(Request $request, array $params = [])
{
return ['name' => 'John', 'email' => 'john@example.com'];
}
}
DESCRIPTION
RSX Controllers provide a simplified approach to request handling through
static methods and automatic discovery. Unlike Laravel's dependency
injection heavy controllers that require constructor injection and
service container resolution, RSX uses static methods that can be
called from anywhere without instantiation.
The framework automatically discovers controllers through the manifest
system - no manual registration in route files required. Routes are
defined directly on methods using attributes, keeping routing logic
with the code it affects.
Key differences from Laravel:
- Laravel: Instance methods with dependency injection
- RSX: Static methods with explicit parameters
- Laravel: Routes defined in separate routes/*.php files
- RSX: Routes defined via #[Route] attributes on methods
- Laravel: Manual API resource controllers
- RSX: Automatic JavaScript stub generation for Ajax methods
- Laravel: Middleware defined in route files or constructors
- RSX: pre_dispatch hooks for authentication/authorization
Benefits:
- No dependency injection complexity
- Routes live with their handlers
- Automatic Ajax/JavaScript integration
- Simple static method calls
- No service container overhead
CREATING A CONTROLLER
1. Extend Rsx_Controller_Abstract
2. Add static methods with Request and params parameters
3. Use attributes for routing
class Dashboard_Controller extends Rsx_Controller_Abstract
{
#[Route('/dashboard')]
public static function index(Request $request, array $params = [])
{
return rsx_view('Dashboard');
}
}
ROUTE ATTRIBUTES
#[Route(path, methods)]
Define HTTP route.
path: URL pattern with optional parameters
methods: ['GET'] or ['POST'] (default both)
Examples:
#[Route('/users')]
#[Route('/users/{id}', methods: ['GET'])]
#[Route('/api/users', methods: ['POST'])]
ROUTE PARAMETERS
URL segments in braces become $params entries:
#[Route('/users/{id}/posts/{post_id}')]
public static function show(Request $request, array $params = [])
{
$user_id = $params['id'];
$post_id = $params['post_id'];
}
Query parameters also added to $params:
/users?sort=name
$params['sort'] === 'name'
REQUIRE ATTRIBUTE
#[Auth(callable, message, redirect, redirect_to)]
REQUIRED on all routes. Defines access control check.
callable: 'Class::method()' string to execute
message: Optional error message
redirect: Optional URL to redirect on failure (HTTP only)
redirect_to: Optional ['Controller', 'action'] (HTTP only)
All routes MUST have at least one #[Auth] attribute, either on:
- The route method itself
- The controller's pre_dispatch() method (applies to all routes)
- Both (pre_dispatch Require runs first, then route Require)
Multiple #[Auth] attributes are supported - all must pass.
Permission Method Contract:
public static function method_name(Request $request, array $params, ...$args): mixed
Returns:
- true or null: Allow access
- false: Deny access
- Response: Custom response (overrides default handling)
Examples:
// Public access
#[Auth('Permission::anybody()')]
#[Route('/')]
public static function index(Request $request, array $params = [])
{
return rsx_view('Landing');
}
// Authenticated users only
#[Auth('Permission::authenticated()',
message: 'Please log in',
redirect: '/login')]
#[Route('/dashboard')]
public static function dashboard(Request $request, array $params = [])
{
return rsx_view('Dashboard');
}
// Redirect using controller/action
#[Auth('Permission::authenticated()',
message: 'Login required',
redirect_to: ['Login_Index_Controller', 'show_login'])]
#[Route('/profile')]
public static function profile(Request $request, array $params = [])
{
return rsx_view('Profile');
}
// Permission with arguments
#[Auth('Permission::has_role("admin")')]
#[Route('/admin')]
public static function admin_panel(Request $request, array $params = [])
{
return rsx_view('Admin_Panel');
}
// Multiple requirements
#[Auth('Permission::authenticated()')]
#[Auth('Permission::has_permission("edit_users")')]
#[Route('/users/edit')]
public static function edit_users(Request $request, array $params = [])
{
return rsx_view('User_Edit');
}
// Controller-wide requirement
class Admin_Controller extends Rsx_Controller_Abstract
{
#[Auth('Permission::has_role("admin")',
message: 'Admin access required',
redirect: '/')]
public static function pre_dispatch(Request $request, array $params = [])
{
return null;
}
// All routes in this controller require admin role
#[Route('/admin/users')]
public static function users(Request $request, array $params = [])
{
return rsx_view('Admin_Users');
}
}
Creating Permission Methods (rsx/permission.php):
class Permission extends Permission_Abstract
{
public static function anybody(Request $request, array $params): mixed
{
return true; // Always allow
}
public static function authenticated(Request $request, array $params): mixed
{
return Session::is_logged_in();
}
public static function has_role(Request $request, array $params, string $role): mixed
{
if (!Session::is_logged_in()) {
return false;
}
return Session::get_user()->has_role($role);
}
}
AJAX ENDPOINTS AND REQUIRE
Ajax endpoints also require #[Auth] attributes.
For Ajax endpoints, redirect parameters are ignored and JSON errors returned:
#[Auth('Permission::authenticated()',
message: 'Login required')]
#[Ajax_Endpoint]
public static function get_data(Request $request, array $params = [])
{
return ['data' => 'value'];
}
On failure, returns:
{
"success": false,
"error": "Login required",
"error_type": "permission_denied"
}
HTTP Status: 403 Forbidden
API_INTERNAL ATTRIBUTE
#[Ajax_Endpoint]
Makes method callable via Ajax.
Generates JavaScript stub automatically.
Returns JSON response.
PHP:
#[Ajax_Endpoint]
public static function search(Request $request, array $params = [])
{
$query = $params['query'] ?? '';
return User::where('name', 'like', "%$query%")->get();
}
JavaScript (auto-generated):
const results = await User_Controller.search({query: 'john'});
JAVASCRIPT STUB GENERATION
Controllers with Ajax_Endpoint methods get stubs in
storage/rsx-build/js-stubs/ControllerName.js:
class User_Controller {
static async search(...args) {
return Ajax.call('User_Controller', 'search', args);
}
}
Stubs included automatically in bundles.
CALLING API METHODS
From JavaScript:
// Single argument
const user = await User_Controller.get_user(123);
// Named parameters
const results = await User_Controller.search({
query: 'john',
limit: 10
});
// Multiple arguments
const data = await User_Controller.process(id, options);
From PHP:
User_Controller::get_user($request, ['id' => 123]);
ROUTE RESOLUTION
PHP:
$url = Rsx::Route('User_Controller', 'show')->url(['id' => 5]);
// Returns: "/users/5"
if (Rsx::Route('User_Controller')->is_current()) {
// Current page is users index
}
JavaScript:
const url = Rsx.Route('User_Controller', 'show', {id: 5});
// Returns: "/users/5"
PRE_DISPATCH HOOK
Run before any action in controller:
public static function pre_dispatch(Request $request, array $params = [])
{
// Check authentication
if (!RsxAuth::check()) {
return redirect('/login');
}
// Return null to continue
return null;
}
Return non-null to override response.
RESPONSE TYPES
Views:
return rsx_view('View_Name', ['data' => $value]);
JSON (Ajax_Endpoint):
return ['key' => 'value']; // Auto-converted to JSON
Redirects:
return redirect('/path');
return redirect()->route('route.name');
Raw responses:
return response('content', 200);
return response()->json(['key' => 'value']);
AUTHENTICATION
Use RsxAuth in pre_dispatch:
public static function pre_dispatch(Request $request, array $params = [])
{
if (!RsxAuth::check()) {
if ($request->ajax()) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return redirect('/login');
}
return null;
}
ERROR HANDLING
Throw exceptions for errors:
public static function show(Request $request, array $params = [])
{
$user = User::find($params['id']);
if (!$user) {
throw new NotFoundHttpException('User not found');
}
return rsx_view('User_Profile', ['user' => $user]);
}
AJAX VS PAGE REQUESTS
Detect Ajax requests:
if ($request->ajax()) {
return ['data' => $data]; // JSON
} else {
return rsx_view('Page', ['data' => $data]); // HTML
}
EXAMPLES
// CRUD controller
class Product_Controller extends Rsx_Controller_Abstract
{
#[Route('/products')]
public static function index(Request $request, array $params = [])
{
$products = Product::paginate(20);
return rsx_view('Product_List', compact('products'));
}
#[Route('/products/{id}')]
public static function show(Request $request, array $params = [])
{
$product = Product::findOrFail($params['id']);
return rsx_view('Product_Detail', compact('product'));
}
#[Ajax_Endpoint]
public static function create(Request $request, array $params = [])
{
$product = Product::create($params);
return $product;
}
#[Ajax_Endpoint]
public static function update(Request $request, array $params = [])
{
$product = Product::findOrFail($params['id']);
$product->update($params);
return $product;
}
#[Ajax_Endpoint]
public static function delete(Request $request, array $params = [])
{
Product::destroy($params['id']);
return ['success' => true];
}
}
// JavaScript usage
async function create_product(data) {
const product = await Product_Controller.create(data);
console.log('Created:', product);
}
async function update_product(id, data) {
const product = await Product_Controller.update({id, ...data});
console.log('Updated:', product);
}
ROUTE CACHING
Routes extracted from manifest and cached.
Clear cache after adding routes:
php artisan rsx:clean
NAMING CONVENTIONS
Controllers: Noun_Controller (User_Controller, Product_Controller)
Actions: verb or verb_noun (index, show, create, update_profile)
Routes: RESTful patterns (/users, /users/{id})
FILE ORGANIZATION
rsx/app/module/
├── module_controller.php # Main controller
├── module_api_controller.php # API endpoints
└── feature/
└── module_feature_controller.php
TESTING ROUTES
Use rsx:debug command:
php artisan rsx:debug /users
php artisan rsx:debug /api/search --post='{"query":"test"}'
TROUBLESHOOTING
Route not found:
- Check Route attribute syntax
- Run php artisan rsx:routes to list all
- Clear cache: php artisan rsx:clean
Missing #[Auth] attribute error:
- Add #[Auth('Permission::anybody()')] to route method
- OR add #[Auth] to pre_dispatch() for controller-wide access
- Rebuild manifest: php artisan rsx:manifest:build
Permission denied (403):
- Check permission method logic returns true
- Verify Session::is_logged_in() for authenticated routes
- Add message parameter for clearer errors
- Check permission method exists in rsx/permission.php
JavaScript stub missing:
- Ensure Ajax_Endpoint attribute present
- Ensure #[Auth] attribute present on Ajax method
- Rebuild manifest: php artisan rsx:manifest:build
- Check storage/rsx-build/js-stubs/
Authentication issues:
- Implement pre_dispatch hook
- Use Permission::authenticated() in Require
- Verify session configuration
SEE ALSO
manifest_api(3), bundle_api(3), jqhtml(3), rsx:routes(1)
RSX Framework 2025-09-17 CONTROLLER(3)

304
app/RSpade/man/enums.txt Executable file
View File

@@ -0,0 +1,304 @@
ENUMS(7) RSpade Developer Manual ENUMS(7)
NAME
Enums - Database field enumeration system for RSX models
SYNOPSIS
public static $enums = [
'field_name' => [
value => ['constant' => 'NAME', 'label' => 'Display Name', ...]
]
];
DESCRIPTION
The enum system provides a powerful way to define predefined values for database
fields with associated metadata. It automatically generates constants, magic
properties, helper methods, and JavaScript equivalents for both PHP and JavaScript
code.
DEFINING ENUMS
Basic Definition
Define enums as a static property on your model class:
public static $enums = [
'status_id' => [
1 => [
'constant' => 'STATUS_ACTIVE',
'label' => 'Active',
],
2 => [
'constant' => 'STATUS_INACTIVE',
'label' => 'Inactive',
],
],
];
Standard Properties
constant - PHP constant name (generates Model::STATUS_ACTIVE)
label - Human-readable display name
order - Sort order (defaults to 0)
selectable - Whether shown in dropdowns (defaults to true)
Custom Properties
You can add any custom properties for business logic:
'status_id' => [
1 => [
'constant' => 'STATUS_PUBLISHED',
'label' => 'Published',
'badge' => 'bg-success', // CSS class for styling
'visible_frontend' => true, // Visibility control
'can_edit' => false, // Business rule
'icon' => 'fa-check', // Icon class
],
];
PHP MAGIC PROPERTIES (Instance)
For a model instance with an enum field, these properties are automatically available:
field_label Returns the label for the current value
$user->status_id = 1;
echo $user->status_label; // "Active"
field_constant Returns the constant name for the current value
echo $user->status_constant; // "STATUS_ACTIVE"
field_enum_val Returns ALL properties for the current value
$props = $user->status_enum_val;
// ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', ...]
field_[property] Returns any custom property for the current value
echo $user->status_badge; // "bg-success"
echo $user->status_visible_frontend; // true
PHP STATIC METHODS
Model::field_enum()
Returns all enum definitions for a field with all properties:
$statuses = User_Model::status_id_enum();
// [1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', ...], ...]
Model::field_enum_select()
Returns key/label pairs for dropdowns, respecting 'selectable' and 'order':
$options = User_Model::status_id_enum_select();
// [1 => 'Active', 2 => 'Inactive']
Model::field_enum_ids()
Returns all possible enum values as an array:
$ids = User_Model::status_id_enum_ids();
// [1, 2, 3]
PHP CONSTANTS
Constants are automatically generated via rsx:migrate:document_models command:
class User_Model extends Rsx_Model_Abstract
{
const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 2;
}
Usage:
if ($user->status_id === User_Model::STATUS_ACTIVE) {
// User is active
}
JAVASCRIPT ACCESS
The manifest system generates JavaScript stub classes with enum support:
Constants
User_Model.STATUS_ACTIVE // 1
User_Model.STATUS_INACTIVE // 2
Static Methods
User_Model.status_id_enum_val() // Full enum definitions
User_Model.status_id_enum_select() // Filtered for dropdowns
User_Model.status_id_label_list() // All labels keyed by value
AJAX/JSON EXPORT
When models are converted to arrays/JSON, enum properties are automatically included:
$user->toArray() returns:
[
'id' => 1,
'status_id' => 1,
'status_id_label' => 'Active', // Added automatically
'status_id_constant' => 'STATUS_ACTIVE', // Added automatically
'status_id_badge' => 'bg-success', // Custom properties too
// ... all enum properties for current value
]
ADVANCED FEATURES
Ordering
Use the 'order' property to control sort order in dropdowns:
'priority' => [
3 => ['label' => 'Low', 'order' => 3],
1 => ['label' => 'High', 'order' => 1],
2 => ['label' => 'Medium', 'order' => 2],
]
// enum_select() returns: [1 => 'High', 2 => 'Medium', 3 => 'Low']
Selective Options
Use 'selectable' => false to hide options from dropdowns while keeping them valid:
3 => [
'constant' => 'STATUS_ARCHIVED',
'label' => 'Archived',
'selectable' => false, // Won't appear in new dropdowns
]
Context-Specific Labels
Define different labels for different contexts:
'label' => 'New Listing', // Backend label
'label_frontend' => 'Coming Soon', // Frontend label
'label_short' => 'New', // Abbreviated version
PRACTICAL APPLICATIONS
Populating Select Boxes
<!-- Blade template -->
<select name="status_id">
@foreach(User_Model::status_id_enum_select() as $id => $label)
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</select>
Dynamic CSS Classes
<span class="badge {{ $auction->auction_status_badge }}">
{{ $auction->auction_status_label }}
</span>
Business Logic Flags
if ($auction->auction_status_can_bid) {
// Show bidding interface
}
Permission Systems
'role' => [
1 => [
'constant' => 'ROLE_ADMIN',
'label' => 'Administrator',
'permissions' => ['users.create', 'users.delete'],
'can_admin_roles' => [2, 3, 4], // Can manage these role IDs
]
]
// Check permissions
if (in_array('users.create', $user->role_permissions)) {
// User can create users
}
Visual Indicators
'priority' => [
1 => [
'label' => 'Critical',
'color' => '#FF0000',
'icon' => 'fa-exclamation-circle',
'badge_class' => 'badge-danger pulse-animation',
]
]
BOOLEAN FIELDS
For boolean fields, use 0/1 as keys:
'is_verified' => [
0 => ['label' => 'Not Verified'],
1 => ['label' => 'Verified'],
]
BEST PRACTICES
1. Always define constants for code readability
2. Use descriptive labels that make sense to end users
3. Add 'order' when dropdown order matters
4. Use 'selectable' => false for deprecated/archived values
5. Keep enum values immutable - add new values, don't change existing
6. Document custom properties in your model
7. Run rsx:migrate:document_models after adding enums
EXAMPLE IMPLEMENTATION
// rsx/models/project_model.php
class Project_Model extends Rsx_Model_Abstract
{
public static $enums = [
'status' => [
1 => [
'constant' => 'STATUS_PLANNING',
'label' => 'Planning',
'badge' => 'badge-secondary',
'can_edit' => true,
'order' => 1,
],
2 => [
'constant' => 'STATUS_IN_PROGRESS',
'label' => 'In Progress',
'badge' => 'badge-primary',
'can_edit' => true,
'order' => 2,
],
3 => [
'constant' => 'STATUS_REVIEW',
'label' => 'Under Review',
'badge' => 'badge-warning',
'can_edit' => false,
'order' => 3,
],
4 => [
'constant' => 'STATUS_COMPLETE',
'label' => 'Complete',
'badge' => 'badge-success',
'can_edit' => false,
'visible_in_reports' => true,
'order' => 4,
],
5 => [
'constant' => 'STATUS_CANCELLED',
'label' => 'Cancelled',
'badge' => 'badge-danger',
'can_edit' => false,
'selectable' => false, // Hide from new projects
'order' => 5,
],
],
'priority' => [
1 => ['constant' => 'PRIORITY_LOW', 'label' => 'Low', 'days' => 30],
2 => ['constant' => 'PRIORITY_MEDIUM', 'label' => 'Medium', 'days' => 14],
3 => ['constant' => 'PRIORITY_HIGH', 'label' => 'High', 'days' => 7],
4 => ['constant' => 'PRIORITY_CRITICAL', 'label' => 'Critical', 'days' => 1],
],
];
}
// Usage in controller
if ($project->status === Project_Model::STATUS_IN_PROGRESS) {
if ($project->priority_days < 3) {
// Escalate critical project
}
}
// Usage in Blade view
<div class="{{ $project->status_badge }}">
{{ $project->status_label }}
@if($project->status_can_edit)
<button>Edit</button>
@endif
</div>
SEE ALSO
php artisan rsx:migrate:document_models - Generate constants and type hints
php artisan rsx:man model_fetch - Model fetching from JavaScript
php artisan rsx:man models - RSX model system overview
RSpade 1.0 September 2025 ENUMS(7)

289
app/RSpade/man/error_handling.txt Executable file
View File

@@ -0,0 +1,289 @@
NAME
error_handling - RSX framework error handling patterns and shouldnt_happen()
SYNOPSIS
Error handling philosophy, fail-loud patterns, and sanity check functions
DESCRIPTION
The RSX framework enforces strict error handling that prioritizes early
detection and clear failure over silent continuation. This document covers
error handling patterns, the shouldnt_happen() function, and examples of
proper failure handling in security-critical and framework contexts.
CORE PHILOSOPHY
Fail Loud, Not Silent:
- Errors must be immediately apparent during development
- No silent fallbacks or alternative code paths
- Exception handlers should ONLY format error display, not provide alternatives
- One deterministic way to do things
No Defensive Coding for Core Classes:
- Core framework classes in /app/RSpade/Core/ are ALWAYS present
- Never check if core classes exist: if (typeof Rsx_Manifest !== 'undefined')
- Just use them directly: Rsx_Manifest.define()
- The build system guarantees core classes are present
SHOULDNT_HAPPEN() FUNCTION
Purpose:
Explicitly fail when encountering "impossible" conditions that indicate
broken assumptions or corrupted application state.
When to Use:
- After loading files, if expected classes don't exist
- When required files are missing after checking they should exist
- When database operations return unexpected null values
- When array keys that must exist are missing
- Any time you write a comment like "this shouldn't happen"
- When critical resources fail to initialize
- When configuration validation fails in unexpected ways
PHP Usage Examples:
// Class loading validation
if (!class_exists($expected_class)) {
shouldnt_happen("Class {$expected_class} should have been loaded in Phase 3");
}
// File system validation
if (!file_exists($required_file)) {
shouldnt_happen("Required config file missing: {$required_file}");
}
// Database state validation
$user = User::find($user_id);
if (!$user && $user_id > 0) {
shouldnt_happen("User ID {$user_id} should exist based on session data");
}
// Array key validation
if (!isset($config['required_setting'])) {
shouldnt_happen("Config missing required_setting after validation");
}
// Resource initialization
if (!$this->manifest_data) {
shouldnt_happen("Manifest data not loaded after successful build");
}
JavaScript Usage Examples:
// DOM element validation
if (!element) {
shouldnt_happen(`Required DOM element #${elementId} not found`);
}
// API response validation
if (!data.requiredField) {
shouldnt_happen('API response missing required field: requiredField');
}
// Framework state validation
if (!Rsx._initialized) {
shouldnt_happen('Rsx framework not initialized before use');
}
// Component validation
if (!this.template_rendered) {
shouldnt_happen('Component ready() called before template render');
}
SECURITY-CRITICAL ERROR HANDLING
Never Continue on Security Failures:
Security validation must fail loudly. Never provide fallback paths
that could compromise security.
String Sanitization Example:
// ❌ CATASTROPHIC - Silent security failure
try {
$clean = Sanitizer::sanitize($user_input);
} catch (Exception $e) {
$clean = $user_input; // DISASTER - unsanitized data continues
}
// ✅ CORRECT - Fail loudly on security failure
$clean = Sanitizer::sanitize($user_input); // Let it throw if it fails
Authentication Example:
// ❌ BAD - Silent fallback to unauthenticated
try {
$user = Auth::validateToken($token);
} catch (Exception $e) {
$user = null; // Dangerous - proceeds without authentication
}
// ✅ GOOD - Fail loudly on auth failure
$user = Auth::validateToken($token); // Let it throw if invalid
Authorization Example:
// ❌ BAD - Silent permission failure
if (!$user->hasPermission('admin')) {
// Log error but continue anyway
Log::error('Permission denied');
return $default_view; // WRONG - shows content without permission
}
// ✅ GOOD - Fail loudly on permission failure
if (!$user->hasPermission('admin')) {
throw new UnauthorizedException('Admin access required');
}
FRAMEWORK ERROR PATTERNS
No Alternative Code Paths:
The framework should have ONE deterministic way to do things.
No redundant fallback systems.
Examples of Anti-Patterns:
// ❌ BAD - Alternative fallback system
try {
$result = NewApiService::process($data);
} catch (Exception $e) {
$result = LegacyApiService::process($data); // WRONG
}
// ❌ BAD - Silent failure continuation
if (!$critical_resource) {
return; // WRONG - silent continue
}
// ❌ BAD - Defensive existence checks for core classes
if (class_exists('Rsx_Manifest')) {
Rsx_Manifest::register(); // WRONG - core classes always exist
}
Correct Patterns:
// ✅ GOOD - Single path, fail loudly
$result = NewApiService::process($data); // Let it throw
// ✅ GOOD - Explicit failure on missing resources
if (!$critical_resource) {
shouldnt_happen("Critical resource not initialized");
}
// ✅ GOOD - Direct usage of core classes
Rsx_Manifest::register(); // Core classes guaranteed present
EXCEPTION HANDLING GUIDELINES
Exception Handlers Should Only Format:
Exception handlers should format error display, not provide
alternative functionality.
Web Error Pages:
// ✅ GOOD - Format error for user display
try {
return $controller->handle($request);
} catch (Exception $e) {
return view('errors.500', ['error' => $e->getMessage()]);
}
API Error Responses:
// ✅ GOOD - Format error for API response
try {
$data = $service->processRequest($request);
} catch (ValidationException $e) {
return response()->json(['error' => $e->errors()], 422);
}
Development Error Display:
// ✅ GOOD - Enhanced error display for development
try {
return $result;
} catch (Exception $e) {
if (config('app.debug')) {
return $this->formatDevelopmentError($e);
}
throw $e; // Re-throw in production
}
DEBUGGING ERROR PATTERNS
Use rsx_dump_die() for Development:
Preferred debugging function that halts execution after output.
Use liberally during debugging, remove after fixing issues.
Examples:
rsx_dump_die($user); // Debug single value
rsx_dump_die($request, $params, $result); // Debug multiple values
Never Hide Debugging Information:
// ❌ BAD - Hidden debug information
try {
rsx_dump_die($debug_data);
} catch (Exception $e) {
// Silent ignore - WRONG
}
// ✅ GOOD - Let debug output show
rsx_dump_die($debug_data); // Always visible
VALIDATION ERROR HANDLING
Input Validation:
Input validation should throw exceptions immediately when validation
fails. Never continue with invalid data.
Model Validation:
// ✅ GOOD - Fail on validation error
$user = new User($data);
$user->validate(); // Throws if invalid
$user->save();
// ❌ BAD - Continue with invalid data
$user = new User($data);
if (!$user->validate()) {
$user->email = 'default@example.com'; // WRONG
}
$user->save();
Configuration Validation:
// ✅ GOOD - Fail on invalid config
if (!config('app.key')) {
shouldnt_happen('Application key not configured');
}
// ❌ BAD - Default fallback config
$key = config('app.key') ?: 'default-key'; // WRONG
TESTING ERROR SCENARIOS
Always Test the Failure Path:
Ensure errors are visible and properly formatted during development.
Error Testing Examples:
// Test that exceptions are thrown
$this->expectException(ValidationException::class);
$service->processInvalidData($bad_data);
// Test error message content
try {
$service->processData(null);
$this->fail('Expected exception not thrown');
} catch (Exception $e) {
$this->assertStringContains('Data cannot be null', $e->getMessage());
}
// Test shouldnt_happen() scenarios
try {
$service->processWithMissingDependency();
$this->fail('shouldnt_happen not triggered');
} catch (RuntimeException $e) {
$this->assertStringContains('shouldnt_happen', $e->getMessage());
}
COMMON ANTI-PATTERNS TO AVOID
Silent Continues:
Never continue execution when critical operations fail.
Redundant Systems:
Never implement fallback systems that do the same thing differently.
Defensive Core Checks:
Never check if core framework classes exist.
Security Fallbacks:
Never provide fallback authentication or authorization.
Default Values on Failure:
Never substitute default values when validation fails.
Try-Catch Workarounds:
Never use try-catch to work around expected failures.
SEE ALSO
coding_standards - General coding conventions and standards
console_debug - Debug output and filtering system
config_rsx - Framework configuration patterns

403
app/RSpade/man/framework_fork.txt Executable file
View File

@@ -0,0 +1,403 @@
# Forking the RSpade Framework
## NAME
framework_fork - Taking full ownership of the RSpade framework codebase
## SYNOPSIS
Breaking out of managed project mode to directly modify framework code:
# Mark framework as forked
touch .rspade-forked-framework
# Switch to selfupdate branch (no demo rsx)
git fetch rspade_upstream
git checkout -B rspade_selfupdate rspade_upstream/rspade_selfupdate --force
# Your ./rsx remains independent
# Framework files are now yours to modify
## DESCRIPTION
RSpade offers two development approaches:
1. **Project Mode** (managed) - Framework is read-only, updated automatically
2. **Forked Framework** (full control) - Entire codebase is yours to modify
This document covers forked framework mode for developers who need to customize
framework internals or integrate deeply with Laravel.
## RSPADE'S RELATIONSHIP TO LARAVEL
RSpade is built within Laravel with minimal divergence:
- Laravel framework provides the foundation (Illuminate ORM, routing, etc.)
- RSpade-specific code isolated in `app/RSpade/`
- Application code lives in `./rsx` directory
- Surrounding Laravel structure meant to be pristine and replaceable
**The abstraction:**
- VB6 apps run in VB6 runtime, which runs in Windows
- RSX apps run in RSpade runtime, which runs in Laravel
**Developer experience:**
For most developers, Laravel's existence is transparent. You work in `./rsx`,
use Laravel helpers (Illuminate ORM, validation, etc.), and never touch files
outside `./rsx`. Framework updates replace everything except `./rsx`.
**But you CAN break the abstraction:**
You can write Laravel routes, controllers, commands, services directly in `./app`.
You can modify RSpade itself in `app/RSpade/`. You can customize anything.
The cost: automatic framework updates no longer work safely.
## WHEN TO FORK
**Stay in project mode if:**
- Building standard applications with RSX
- Want automatic framework updates
- Don't need to modify framework internals
- Comfortable with framework conventions
**Fork the framework if:**
- Need to modify RSpade core functionality
- Want to customize Laravel foundation
- Building deep framework integrations
- Willing to manually merge upstream changes
- Have team maintaining framework modifications
**Critical decision point:**
Once forked, you own all framework changes. Upstream updates require manual
review and merging. This is permanent divergence from managed updates.
## PROJECT MODE VS FORKED FRAMEWORK
Project Mode (managed):
./rsx - Independent git repo (your application)
Everything else - Managed by framework, auto-updated
Framework modifications - Blocked by pre-commit hooks
Updates - php artisan rsx:framework:pull
Merge conflicts - Impossible (./rsx protected)
Forked Framework (full control):
./rsx - Independent git repo (your application)
Everything else - Your git repo, your modifications
Framework modifications - Allowed and expected
Updates - Manual git merge from upstream
Merge conflicts - Your responsibility
**Key difference:**
Project mode: Framework is a black box, updated atomically
Forked mode: Framework is source code, you maintain modifications
## SETUP PROCEDURE
### 1. Ensure ./rsx is independent
Your ./rsx directory should already be an independent git repository:
cd rsx
git remote add origin <your-application-repo>
git push -u origin master
cd ..
Your application code must be independent before forking framework.
### 2. Create fork marker file
touch .rspade-forked-framework
This marker:
- Disables automatic framework updates
- Signals intentional framework ownership
- Checked by update scripts
### 3. Switch to selfupdate branch
The `rspade_selfupdate` branch contains framework without demo `./rsx`:
# Backup .env
cp .env .env.backup
# Fetch and switch
git fetch rspade_upstream
git checkout -B rspade_selfupdate rspade_upstream/rspade_selfupdate --force
# Restore .env
cp .env.backup .env
**Why selfupdate branch?**
- Master branch includes demo application in `./rsx`
- Selfupdate branch has no `./rsx` directory
- Prevents git conflicts with your independent `./rsx` repo
- Cleaner base for framework modifications
### 4. Verify configuration
git branch -vv
# Should show: * rspade_selfupdate [rspade_upstream/rspade_selfupdate]
ls -la .rspade-forked-framework
# File should exist
ls -la rsx/.git
# Directory should exist (independent repo)
### 5. Commit fork marker
git add .rspade-forked-framework
git commit -m "Mark framework as forked for custom modifications"
You now own the entire framework codebase.
## MAKING FRAMEWORK MODIFICATIONS
You can now modify any file outside `./rsx`:
**Common modifications:**
- `app/RSpade/` - Core RSpade functionality
- `app/Http/` - Laravel HTTP layer
- `config/` - Framework configuration
- `routes/` - Additional Laravel routes
- `app/Console/` - Laravel commands
**Example: Custom framework feature**
# Create custom feature in RSpade core
nano app/RSpade/Core/MyFeature/MyFeature.php
# Commit to your fork
git add app/RSpade/Core/MyFeature/
git commit -m "Add custom MyFeature to RSpade core"
**The ./rsx boundary still exists:**
Your `./rsx` directory remains an independent git repository for application code.
Framework modifications go in the root repository.
## UPDATING FROM UPSTREAM
Upstream continues releasing framework improvements. You must manually merge them.
### Update procedure
# 1. Fetch upstream changes
git fetch rspade_upstream
# 2. Review what changed upstream
git log HEAD..rspade_upstream/rspade_selfupdate
git diff HEAD..rspade_upstream/rspade_selfupdate
# 3. Check for conflicts with your modifications
git diff HEAD..rspade_upstream/rspade_selfupdate app/RSpade/
# 4. Merge upstream changes
git merge rspade_upstream/rspade_selfupdate
# 5. Resolve any conflicts
# Edit conflicted files, keeping your modifications where needed
# 6. Test thoroughly
php artisan rsx:check
php artisan rsx:clean
php artisan rsx:manifest:build
php artisan rsx:bundle:compile
# Run your application tests
# 7. Commit merge
git add -A
git commit -m "Merge upstream framework updates"
### Handling merge conflicts
When upstream modifies files you've changed:
# During merge, git will mark conflicts
git status
# Shows: both modified: app/RSpade/Core/Something.php
# Edit file, resolve conflicts
nano app/RSpade/Core/Something.php
# Look for conflict markers:
<<<<<<< HEAD
Your modifications
=======
Upstream changes
>>>>>>> rspade_upstream/rspade_selfupdate
# Decide what to keep:
# - Your changes only
# - Upstream changes only
# - Combination of both (most common)
# Mark as resolved
git add app/RSpade/Core/Something.php
### Selective merging
Don't want all upstream changes?
# Cherry-pick specific commits
git log rspade_upstream/rspade_selfupdate
git cherry-pick <commit-hash>
# Or manually copy specific files
git checkout rspade_upstream/rspade_selfupdate -- path/to/specific/file.php
## BLOCKED OPERATIONS
Once `.rspade-forked-framework` exists:
**Blocked: php artisan rsx:framework:pull**
ERROR: Framework is in forked mode.
You have taken full ownership of the RSpade framework codebase.
Automatic updates are disabled to prevent overwriting your changes.
To manually update from upstream:
1. git fetch rspade_upstream
2. git diff rspade_upstream/rspade_selfupdate
3. Manually merge desired changes
4. Test thoroughly
For detailed procedures: php artisan rsx:man framework_fork
Forked frameworks are permanent unless you explicitly revert to managed mode
(see "REVERTING TO PROJECT MODE" section below).
## REVERTING TO PROJECT MODE
If you want to return to managed project mode:
**⚠️ WARNING: This deletes all framework modifications**
# 1. Stash or commit any ./rsx changes
cd rsx
git add -A
git commit -m "Save application changes"
cd ..
# 2. Remove fork marker
rm .rspade-forked-framework
# 3. Hard reset to upstream
git fetch rspade_upstream
git reset --hard rspade_upstream/rspade_selfupdate
# 4. Verify
php artisan rsx:framework:pull
# Should work again
Your framework modifications are lost. Only do this if you're certain.
## DEPLOYMENT
Forked framework requires different deployment strategy:
**Project mode deployment:**
rsync ./rsx/ production:/var/www/html/rsx/
ssh production 'php artisan rsx:framework:pull'
**Forked framework deployment:**
# Deploy entire repository
git push production master
ssh production 'git pull origin master'
ssh production 'php artisan rsx:clean'
ssh production 'php artisan rsx:manifest:build'
ssh production 'php artisan rsx:bundle:compile'
You're deploying framework changes, not just application code.
## TEAM WORKFLOW
Managing a forked framework with a team:
# 1. Designate framework maintainer(s)
# Only certain developers modify app/RSpade/
# 2. Use feature branches for framework changes
git checkout -b feature/custom-bundle-processor
# Modify framework code
git commit -m "Add custom bundle processor"
git push origin feature/custom-bundle-processor
# 3. Code review framework changes carefully
# Framework bugs affect all team members
# 4. Document framework modifications
# Keep FRAMEWORK_CHANGES.md listing all divergences
# 5. Periodically merge upstream
# Scheduled maintenance window to merge framework updates
## TROUBLESHOOTING
### "Framework is in forked mode" error
You marked framework as forked but want automatic updates:
rm .rspade-forked-framework
php artisan rsx:framework:pull
### Merge conflicts every update
Your modifications conflict with upstream changes frequently:
**Option 1:** Reconsider forking
Can you achieve goals without modifying framework?
**Option 2:** Contribute upstream
Submit your modifications to RSpade project, eliminate divergence.
**Option 3:** Freeze framework version
Stop merging upstream updates, maintain fixed framework version.
### Lost track of modifications
Can't remember what you changed:
# Show all commits since initial fork
git log rspade_upstream/rspade_selfupdate..HEAD
# Show diff of all modifications
git diff rspade_upstream/rspade_selfupdate HEAD
### Want to undo specific modification
# Revert specific commit
git revert <commit-hash>
# Or reset specific file to upstream
git checkout rspade_upstream/rspade_selfupdate -- path/to/file.php
## BEST PRACTICES
1. **Document everything**
Keep FRAMEWORK_CHANGES.md listing every modification and why.
2. **Minimize divergence**
Only fork if absolutely necessary. Minimize number of modified files.
3. **Contribute upstream**
Submit useful modifications to RSpade project.
4. **Test rigorously**
Framework bugs affect entire application. Test after every change.
5. **Version lock dependencies**
Check composer.lock and package-lock.json into git.
6. **Regular upstream merges**
Don't let fork drift too far. Merge upstream monthly.
7. **Separate concerns**
Application code in ./rsx, framework modifications outside.
## SEE ALSO
rsx_upstream(7) - Project mode and automatic updates
rsx_architecture(7) - RSX application structure
coding_standards(7) - RSpade development conventions

1373
app/RSpade/man/jqhtml.txt Executable file

File diff suppressed because it is too large Load Diff

468
app/RSpade/man/jqhtmldoc.txt Executable file
View File

@@ -0,0 +1,468 @@
JQHTMLDOC(3) RSX Framework Manual JQHTMLDOC(3)
NAME
JQHTMLDOC - Component documentation standard for .jqhtml files
SYNOPSIS
<!--
Component_Name
$required_arg - Description of required argument
$optional_arg="default" - Optional argument with default value
this.data.field - Data structure after on_load()
this.args.param - Input parameters from component attributes
method_name() - Public methods for external interaction
CONTENT BLOCKS:
<Block:Custom_Block>
Extension point for customization
Variables: Custom_Block.variable_name
-->
DESCRIPTION
JQHTML component documentation uses HTML comments at the top of
.jqhtml template files. The documentation serves as a contract:
defining inputs (arguments), outputs (data/methods), and extension
points (content blocks).
Philosophy: Treat <Define> like a class definition. Document what
goes in, what comes out, and how it can be extended. Scale the
documentation to component complexity - simple components need
minimal docs, complex ones need comprehensive coverage.
This is code documentation, not user documentation. Write for
developers who need to understand how to use the component in their
code, not for end users who click buttons.
DOCUMENTATION STRUCTURE
The documentation comment appears immediately before the <Define> tag
and follows this structure:
1. Component Name
First line: PascalCase component name matching <Define> tag
2. Arguments Section (if component accepts parameters)
List all $ attributes the component expects
Show default values where applicable
Mark required vs optional
3. Data Section (if component has on_load() or uses this.data)
Document this.data structure after on_load() completes
Document this.args for reference to component parameters
Show data shape and expected types
4. Methods Section (if component has public methods)
List methods that external code should call
Brief description of what each method does
5. Content Blocks Section (if component uses slots)
Document each <Block:Name> the component accepts
List variables available in each block's scope
AVAILABLE SECTIONS
Only include sections relevant to your component. Most components
use 2-3 sections maximum.
Component Name (Required)
First line of comment, PascalCase matching <Define> tag.
Arguments (Use when component accepts $parameters)
Format: $arg_name="default" - Description
Omit ="default" for required arguments
Show actual default values from template
Data (Use when component loads or manipulates data)
this.data.field - What's available after on_load()
this.args.field - Reference to input parameters
Document structure, not implementation
Methods (Use when component has callable public methods)
method_name() - What it does
Include parameters if method signature is important
Only document methods external code should call
Content Blocks (Use when component accepts slots)
<Block:Block_Name>
What goes here
Variables: Block_Name.var1, Block_Name.var2
Notes (Optional, use sparingly)
Implementation notes
Gotchas or non-obvious behavior
Performance considerations
SCALING TO COMPLEXITY
The documentation standard scales to match component complexity.
Simple components need minimal documentation. Complex components
with many parameters, data dependencies, and extension points need
comprehensive documentation.
Simple Component (1-2 sections):
<!--
Text_Input
$name="field_name" - Form field name
$value="" - Initial value (optional)
-->
Medium Component (3-4 sections):
<!--
User_Card
$user_id - User ID to load
$theme="light" - Card theme (light/dark)
this.data.user - User object from API
this.data.user.name - User's display name
this.data.user.email - User's email address
refresh() - Reload user data from server
-->
Complex Component (5+ sections):
<!--
DataGrid
$api="Controller" - Controller with datagrid_fetch() endpoint
$columns=[] - Column definitions (optional, auto-detected)
$per_page="25" - Rows per page (default: 25)
$sortable="true" - Enable column sorting
this.data.records - Array of records from API
this.data.total - Total record count
this.data.page - Current page number
this.data.columns - Resolved column definitions
reload_data() - Refresh grid data
goto_page(n) - Navigate to page
sort_by(column) - Sort by column
CONTENT BLOCKS:
<Block:Datagrid_Row_Block>
Custom row rendering
Variables: Datagrid_Row_Block.record, Datagrid_Row_Block.index
<Block:Datagrid_Empty_Block>
Shown when no records found
-->
DECISION GUIDE
Should I document this component?
YES - if it accepts arguments or loads data
YES - if other developers will use it
YES - if it has public methods or content blocks
MAYBE - if it's very simple (single argument, no data)
NO - if it's a throwaway test component
What should I document?
Arguments - ALWAYS document if component accepts any
Data - Document if on_load() exists or this.data is used
Methods - Document public methods only (not internals)
Content Blocks - Document all slots
Notes - Only when behavior is non-obvious
How much detail?
Arguments - Show name, default, one-line description
Data - Show structure, not implementation details
Methods - Show signature, one-line description
Content Blocks - List available variables
Notes - Keep brief, focus on gotchas
EXAMPLES
Simple Input Component:
<!--
Text_Input
$name="field_name" - Form field name
$value="initial" - Initial value
$placeholder="" - Input placeholder (optional)
-->
<Define:Text_Input tag="input" type="text" class="form-control"
name="<%= this.args.name %>"
value="<%= this.args.value %>"
placeholder="<%= this.args.placeholder || '' %>"
/>
Card with Data Loading:
<!--
Product_Card
$product_id - Product ID to load
$show_price="true" - Display price (optional)
this.data.product - Product object from API
this.data.product.name - Product name
this.data.product.price - Product price
add_to_cart() - Add product to shopping cart
toggle_favorite() - Toggle favorite status
-->
<Define:Product_Card class="card">
<% if (Object.keys(this.data).length === 0) { %>
<div class="spinner">Loading...</div>
<% } else { %>
<h3><%= this.data.product.name %></h3>
<% if (this.args.show_price === "true") { %>
<p class="price">$<%= this.data.product.price %></p>
<% } %>
<button $id="cart_btn">Add to Cart</button>
<% } %>
</Define:Product_Card>
Complex Grid with Slots:
<!--
DataGrid
$api="Controller" - Controller with datagrid_fetch() endpoint
$per_page="25" - Rows per page (default: 25)
$sortable="true" - Enable column sorting
$columns=[] - Column definitions (optional)
this.data.records - Array of records from API
this.data.total - Total record count
this.data.page - Current page number
this.data.per_page - Records per page
this.data.sort_column - Current sort column
this.data.sort_direction - 'asc' or 'desc'
reload_data() - Refresh grid data
goto_page(n) - Navigate to page
sort_by(column) - Sort by column
set_per_page(n) - Change page size
CONTENT BLOCKS:
<Block:Datagrid_Row_Block>
Custom row rendering for each record
Variables: Datagrid_Row_Block.record, Datagrid_Row_Block.index
<Block:Datagrid_Empty_Block>
Shown when no records found
Variables: (none)
<Block:Datagrid_Header_Block>
Custom header row (optional)
Variables: Datagrid_Header_Block.columns
-->
<Define:DataGrid class="datagrid">
<table>
<thead>
<#header>
<% for (let col of this.data.columns) { %>
<th><%= col.title %></th>
<% } %>
</#header>
</thead>
<tbody>
<% if (this.data.records.length === 0) { %>
<tr><td colspan="100"><#empty>No records</#empty></td></tr>
<% } else { %>
<% for (let [idx, record] of this.data.records.entries()) { %>
<tr><#row /></tr>
<% } %>
<% } %>
</tbody>
</table>
</Define:DataGrid>
ARGUMENT DOCUMENTATION PATTERNS
Required Arguments (no default):
$user_id - User ID to load
$api_endpoint - API endpoint for data
Optional Arguments (with default):
$theme="light" - UI theme (light/dark/auto)
$per_page="25" - Records per page
$enabled="true" - Enable/disable feature
Boolean Arguments:
$sortable="true" - Enable column sorting
$show_header="false" - Display table header
Array/Object Arguments:
$columns=[] - Column definitions (optional)
$filters={} - Filter criteria (optional)
Complex Arguments:
$on_select=this.handle_select - Callback when row selected
$custom_renderer=this.render_custom - Custom cell renderer
DATA DOCUMENTATION PATTERNS
Simple Data Structure:
this.data.user - User object from API
this.data.user.name - User's display name
this.data.user.email - User's email address
Array Data:
this.data.items - Array of item objects
this.data.items[].id - Item ID
this.data.items[].name - Item name
Computed Data:
this.data.total_price - Sum of all item prices
this.data.is_valid - True if form passes validation
Referencing Arguments:
this.args.user_id - User ID from $user_id parameter
this.args.theme - Theme from $theme parameter
METHOD DOCUMENTATION PATTERNS
Simple Methods:
reload_data() - Refresh data from server
destroy() - Remove component from DOM
Methods with Parameters:
goto_page(page_number) - Navigate to specific page
sort_by(column_name, direction) - Sort by column
Async Methods:
async save_changes() - Save data to server
async load_user(user_id) - Load specific user
CONTENT BLOCK DOCUMENTATION PATTERNS
Simple Blocks (no variables):
<Block:Header_Block>
Custom header content
Blocks with Variables:
<Block:Row_Block>
Custom row rendering
Variables: Row_Block.record, Row_Block.index
Multiple Variables:
<Block:Cell_Block>
Custom cell content
Variables: Cell_Block.value, Cell_Block.column, Cell_Block.record
Optional Blocks:
<Block:Empty_Block>
Shown when no data (optional, has default)
WRITING STYLE
Write in imperative voice:
✓ "User ID to load"
✗ "This is the user ID that will be loaded"
Be concise:
✓ "Enable column sorting"
✗ "When set to true, enables the ability to sort columns"
Document contract, not implementation:
✓ "this.data.user - User object from API"
✗ "this.data.user - Populated by on_load() which calls fetch()"
Use consistent terminology:
✓ "Records per page" (consistent with this.data.per_page)
✗ "Number of rows to show" (inconsistent)
WHEN TO UPDATE DOCUMENTATION
Update documentation when:
- Adding new arguments
- Changing argument defaults
- Adding/removing data fields
- Adding public methods
- Adding/removing content blocks
- Changing block variable names
Treat documentation as code:
- Update in same commit as implementation changes
- Keep in sync with actual behavior
- Remove documentation for removed features
RELATIONSHIP TO JAVASCRIPT CLASS
The .jqhtml documentation covers the template contract. The
JavaScript class file may have additional JSDoc comments for
implementation details, but the HTML comment is the primary
documentation for component consumers.
.jqhtml comment documents:
- What arguments component accepts
- What data structure on_load() produces
- What public methods are callable
- What content blocks are available
.js JSDoc comments document:
- Implementation details
- Private methods
- Algorithm notes
- Performance considerations
VALIDATION AND ENFORCEMENT
The framework does not currently validate component documentation.
Documentation is enforced through code review and developer
discipline.
Future versions may add:
- Linting rules to require documentation
- Validation that documented arguments match template usage
- IDE integration for autocomplete from documentation
COMMON MISTAKES
Don't document internal implementation:
✗ "Calls fetch() to load data from /api/users endpoint"
✓ "this.data.user - User object from API"
Don't duplicate template code:
✗ Listing every single line of template
✓ High-level contract only
Don't over-document simple components:
✗ Verbose multi-paragraph description for a text input
✓ Brief one-line descriptions
Don't leave outdated documentation:
✗ Documenting removed arguments or data fields
✓ Keep docs in sync with implementation
Don't document private methods:
✗ "internal_helper() - Internal use only"
✓ Only document public API
INTEGRATION WITH DEVELOPMENT WORKFLOW
1. Create component with rsx:app:component:create
Generated stub includes basic documentation template
2. Implement component functionality
Add arguments, data loading, methods as needed
3. Update documentation comment
Document all public contract elements
4. Code review
Reviewer verifies documentation matches implementation
5. Maintenance
Update documentation when changing component behavior
RELATIONSHIP TO MAN PAGES
Component documentation (this standard) differs from man pages:
Component docs (.jqhtml comments):
- Document specific component contract
- Live with the component code
- Brief, focused on usage
- Written for developers using the component
Man pages (rsx:man topic):
- Document framework subsystems
- Comprehensive reference documentation
- Include examples, philosophy, troubleshooting
- Written for framework developers
SEE ALSO
jqhtml(3) - JQHTML component system overview
coding_standards(3) - RSX coding conventions
rsx_architecture(3) - RSX application structure
NOTES
This documentation standard was established 2025-10-10 to provide
consistent component documentation across RSX applications. The
standard emphasizes scaling documentation to component complexity
and treating documentation as code that must stay in sync with
implementation.
RSX Framework 2025-10-10 JQHTMLDOC(3)

343
app/RSpade/man/jquery.txt Executable file
View File

@@ -0,0 +1,343 @@
NAME
jQuery Extensions - RSpade framework extensions and overrides to jQuery
SYNOPSIS
// Standard click handler with automatic preventDefault
$('.btn').click(function(e) {
console.log('Button clicked');
});
// Escape hatch for native behavior (rare)
$('.nav-link').click_allow_default(function(e) {
analytics.track('click');
// Link navigation happens
});
// Other jQuery helpers
if ($('.element').exists()) { ... }
if ($('.element').is_in_viewport()) { ... }
$('.form').checkValidity();
DESCRIPTION
RSpade extends jQuery with utility methods and overrides default behaviors
to provide sensible defaults for modern web applications. The most significant
change is the .click() override that automatically calls e.preventDefault()
to prevent accidental page navigation and form submission.
Unlike vanilla jQuery where developers must remember to call preventDefault
in nearly every click handler, RSpade makes the correct behavior automatic.
This eliminates a common source of bugs where forgotten preventDefault calls
cause unwanted page reloads or navigation.
Philosophy:
- Prevent default is correct 95% of the time
- Escape hatch available for the 5% case
- Fail toward the safe behavior (no navigation)
- Make the common case trivial
CLICK OVERRIDE
.click(handler)
Attach click handler that automatically calls e.preventDefault()
This prevents:
- Link navigation (<a href>)
- Form submission (<button type="submit">)
- Default button actions
Usage:
$('.delete-btn').click(function(e) {
delete_record($(this).data('id'));
// No need for e.preventDefault() - automatic
});
$('a.modal-trigger').click(function(e) {
open_modal();
// Link won't navigate - preventDefault called
});
.click() (no handler)
Trigger click event - native behavior unchanged
Usage:
$('.hidden-button').click(); // Triggers click
.click_allow_default(handler)
Escape hatch for native click behavior without preventDefault
Use this ONLY when you explicitly need default browser behavior.
Examples: analytics tracking before navigation, conditional preventDefault
Usage:
$('.external-link').click_allow_default(function(e) {
analytics.track('external_click', this.href);
// Navigation happens after handler
});
$('button[type=submit]').click_allow_default(function(e) {
if (!validate_form()) {
e.preventDefault(); // Conditionally prevent
}
// Otherwise allows form submission
});
JQUERY HELPERS
Element Existence and State
.exists()
Returns true if jQuery selector matched any elements
if ($('.error-message').exists()) {
console.log('Error message present');
}
.is_visible()
Returns true if element is visible (not display:none or hidden)
if ($('.modal').is_visible()) {
$('.modal').fadeOut();
}
.is_in_dom()
Returns true if element exists in and is attached to the DOM
if ($('.dynamic-element').is_in_dom()) {
// Element is live in the page
}
.is_in_viewport()
Returns true if element is currently visible in the viewport
$('.lazy-image').each(function() {
if ($(this).is_in_viewport()) {
load_image($(this));
}
});
Scrolling
.scroll_up_to(speed = 0)
Scrolls page up to bring element into view if it's above viewport
$('.error-field').scroll_up_to(300);
Element Information
.tagname()
Returns lowercase tag name of element
if ($element.tagname() === 'a') {
// It's a link
}
.is_external()
Returns true if link href is external to current domain
Only works on <a> elements
if ($('a').is_external()) {
$(this).attr('target', '_blank');
}
Form Validation
.checkValidity()
Returns true if form/field passes HTML5 validation
if ($('form').checkValidity()) {
submit_form();
}
.reportValidity()
Triggers browser's native validation UI, returns validity
if (!$('form').reportValidity()) {
return; // Browser shows validation errors
}
.requestSubmit()
Programmatically submit form (triggers validation)
$('form').requestSubmit();
Focus Selector
:focus pseudo-selector
Select element that currently has focus
if ($('input').is(':focus')) {
// Input is focused
}
$('input:focus').addClass('active');
RSPADE VS VANILLA JQUERY
Click Handlers
Vanilla jQuery:
$('.btn').click(function(e) {
e.preventDefault(); // Must remember this
do_something();
});
RSpade:
$('.btn').click(function(e) {
do_something(); // preventDefault automatic
});
Existence Checks
Vanilla jQuery:
if ($('.element').length > 0) { ... }
RSpade:
if ($('.element').exists()) { ... }
Form Validation
Vanilla jQuery:
if ($('form')[0].checkValidity()) { ... }
RSpade:
if ($('form').checkValidity()) { ... }
WHEN TO USE .click_allow_default()
Valid use cases (rare):
1. Analytics tracking before navigation
$('a.external').click_allow_default(function(e) {
analytics.track('external_link');
// Let navigation happen
});
2. Conditional preventDefault
$('button[type=submit]').click_allow_default(function(e) {
if (!validate_form()) {
e.preventDefault();
}
});
3. Progressive enhancement fallbacks
$('a[href="/fallback"]').click_allow_default(function(e) {
if (ajax_available()) {
e.preventDefault();
load_via_ajax();
}
// Otherwise let href work
});
Invalid use cases (use standard .click() instead):
- Opening modals (don't need navigation)
- Triggering actions (don't need navigation)
- Ajax requests (don't need navigation)
- Any case where you don't want the browser's default behavior
MIGRATION FROM VANILLA JQUERY
When porting jQuery code to RSpade:
1. Remove explicit preventDefault calls in click handlers
Before:
$('.btn').click(function(e) {
e.preventDefault();
do_action();
});
After:
$('.btn').click(function(e) {
do_action();
});
2. Identify legitimate native behavior needs
If you have click handlers that SHOULD allow navigation/submission,
switch to .click_allow_default()
3. Replace existence checks
Before: if ($('.el').length > 0)
After: if ($('.el').exists())
4. Replace form validation
Before: $('form')[0].checkValidity()
After: $('form').checkValidity()
EXAMPLES
Modal Trigger
// Opens modal without navigating
$('a.open-modal').click(function(e) {
const modal_id = $(this).data('modal');
$(`#${modal_id}`).fadeIn();
});
Delete Button
// Deletes record without form submission
$('.delete-btn').click(function(e) {
const id = $(this).data('id');
if (confirm('Delete this record?')) {
delete_record(id);
}
});
External Link with Tracking
// Tracks click then allows navigation
$('a.external').click_allow_default(function(e) {
analytics.track('external_link', {
url: this.href,
text: $(this).text()
});
});
Conditional Form Submit
// Only prevents submit if validation fails
$('form button[type=submit]').click_allow_default(function(e) {
if (!validate_custom_rules()) {
e.preventDefault();
show_errors();
}
});
Lazy Loading on Scroll
$(window).on('scroll', function() {
$('.lazy-image').each(function() {
if ($(this).is_in_viewport() && !$(this).data('loaded')) {
const src = $(this).data('src');
$(this).attr('src', src).data('loaded', true);
}
});
});
TROUBLESHOOTING
Problem: Links not navigating when they should
Solution: Use .click_allow_default() instead of .click()
Problem: Form submitting unexpectedly
Solution: This shouldn't happen - .click() prevents submission by default
If using .click_allow_default(), add explicit e.preventDefault()
Problem: .exists() not working
Solution: Ensure Rsx_Jq_Helpers is loaded - check browser console for errors
Problem: Want to use .on('click') to avoid preventDefault
Solution: Don't do this - it defeats the framework's safety. If you need
native behavior, use .click_allow_default() to make intent explicit
IMPLEMENTATION NOTES
The click override is implemented in:
/app/RSpade/Core/Js/Rsx_Jq_Helpers.js
Native jQuery .click() is preserved as:
$.fn._click_native()
The override applies to all elements, not just links and buttons. This is
intentional - preventDefault is harmless on elements without default actions
and ensures consistency across the codebase.
SEE ALSO
jqhtml.txt - JQHTML component system
controller.txt - Controller click handlers
error_handling.txt - JavaScript error patterns

356
app/RSpade/man/js_decorators.txt Executable file
View File

@@ -0,0 +1,356 @@
NAME
js_decorators - RSX JavaScript decorator system with whitelist validation
SYNOPSIS
JavaScript decorators for enhancing static methods with explicit whitelist control
DESCRIPTION
The RSX framework supports JavaScript decorators for enhancing static methods.
Unlike standard JavaScript decorators, RSX requires explicit whitelisting
using the @decorator marker to prevent arbitrary code injection and maintain
framework security.
Key differences from standard decorators:
- Standard: Any function can be used as decorator
- RSX: Only functions marked with @decorator can be used
Benefits:
- Prevents accidental decorator misuse
- Framework validates decorator usage at build time
- Clear separation between decorator definitions and applications
- Bundle ordering ensures decorator.js loads first
DECORATOR WHITELIST SYSTEM
Whitelist Requirement:
Only functions/methods marked with @decorator can be used as decorators
elsewhere in the codebase.
Validation:
Decorator usage validated when the manifest builds.
Invalid decorator usage causes build failures with clear error messages.
Bundle Ordering:
Framework ensures decorator.js loads first in bundles to guarantee
availability of decorator definitions.
JAVASCRIPT FILE STRUCTURE REQUIREMENTS
Files Without ES6 Classes:
JavaScript files that don't contain ES6 classes (utility files)
may only contain these three types of code:
1. Function declarations (standard or arrow functions)
2. Const variables with static values (no function calls)
3. Decorator definitions (functions marked with @decorator)
Static values include:
- Literals (strings, numbers, booleans, null)
- Binary/unary expressions (e.g., 3 * 365, -100, !false)
- Static arrays and objects (with literal values)
NOT allowed:
- Function calls in const values (e.g., Math.random())
- let or var declarations at top level
- Any other code outside the three allowed types
DEFINING DECORATORS
Standalone Functions (Non-ES6):
@decorator
function myCustomDecorator(target, key, descriptor) {
// Decorator implementation
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}
@decorator
function logExecutionTime(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
const start = performance.now();
const result = original.apply(this, args);
const end = performance.now();
console.log(`${key} executed in ${end - start}ms`);
return result;
};
return descriptor;
}
ES6 Class Methods:
class MyDecorators {
@decorator
static validateParams(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
if (args.length === 0) {
throw new Error(`${key} requires at least one parameter`);
}
return original.apply(this, args);
};
return descriptor;
}
@decorator
static cacheResult(target, key, descriptor) {
const cache = new Map();
const original = descriptor.value;
descriptor.value = function(...args) {
const cacheKey = JSON.stringify(args);
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = original.apply(this, args);
cache.set(cacheKey, result);
return result;
};
return descriptor;
}
}
Framework Core Decorator:
The framework provides a core decorator function in
/app/RSpade/Core/Js/decorator.js that marks functions as
decorator implementations.
USING DECORATORS
Static Method Decoration:
Once whitelisted with @decorator, decorators can be used on static methods:
class MyClass {
@myCustomDecorator
static myMethod() {
return "result";
}
@logExecutionTime
@validateParams
static processData(data) {
// Process data with timing and validation
return data.map(item => item.toUpperCase());
}
}
Multiple Decorators:
Multiple decorators can be applied to the same method.
They execute in reverse order (bottom to top):
class DataProcessor {
@logExecutionTime // Executes second (outer)
@validateParams // Executes first (inner)
static transform(data) {
return data.transform();
}
}
DECORATOR RESTRICTIONS
Static Methods Only:
Decorators can only be applied to static methods, not instance methods.
class Example {
@myDecorator
static validMethod() { // ✅ Valid - static method
return "works";
}
@myDecorator
invalidMethod() { // ❌ Invalid - instance method
return "fails";
}
}
Global Function Limitations:
Global functions can only use @decorator, not other decorators.
@decorator
function validGlobalDecorator() { // ✅ Valid - marking as decorator
// implementation
}
@someOtherDecorator
function invalidGlobalUsage() { // ❌ Invalid - using decorator
// This will fail validation
}
Whitelist Validation:
Only functions/methods marked with @decorator can be used as decorators.
@decorator
function authorizedDecorator() { // ✅ Valid - explicitly whitelisted
// implementation
}
function unauthorizedFunction() { // Cannot be used as decorator
// implementation
}
class MyClass {
@authorizedDecorator
static validUsage() { // ✅ Valid - using whitelisted decorator
return "works";
}
@unauthorizedFunction
static invalidUsage() { // ❌ Invalid - not whitelisted
return "fails";
}
}
COMMON DECORATOR PATTERNS
Logging Decorator:
@decorator
function logCalls(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${target.name}.${key}`, args);
const result = original.apply(this, args);
console.log(`${target.name}.${key} returned`, result);
return result;
};
return descriptor;
}
Async Error Handling:
@decorator
function catchAsync(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
try {
return await original.apply(this, args);
} catch (error) {
console.error(`Error in ${target.name}.${key}:`, error);
throw error;
}
};
return descriptor;
}
Rate Limiting:
@decorator
function rateLimit(target, key, descriptor) {
const calls = new Map();
const original = descriptor.value;
descriptor.value = function(...args) {
const now = Date.now();
const lastCall = calls.get(key) || 0;
if (now - lastCall < 1000) {
throw new Error(`${key} called too frequently`);
}
calls.set(key, now);
return original.apply(this, args);
};
return descriptor;
}
Parameter Validation:
@decorator
function requireParams(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
if (args.some(arg => arg === undefined || arg === null)) {
throw new Error(`${key} requires all parameters to be defined`);
}
return original.apply(this, args);
};
return descriptor;
}
DECORATOR IMPLEMENTATION DETAILS
Decorator Function Signature:
function myDecorator(target, key, descriptor) {
// target: The class being decorated
// key: The method name being decorated
// descriptor: Property descriptor for the method
const original = descriptor.value;
descriptor.value = function(...args) {
// Pre-execution logic
const result = original.apply(this, args);
// Post-execution logic
return result;
};
return descriptor;
}
Preserving Method Context:
Always use original.apply(this, args) to preserve method context
and parameter passing.
Return Value Handling:
Decorators can modify return values, but should be careful not to
break expected return types.
Error Propagation:
Decorators should generally allow errors to propagate unless
specifically designed for error handling.
MANIFEST INTEGRATION
Build-Time Validation:
The manifest system validates decorator usage during build:
1. Scans for @decorator markers
2. Builds whitelist of authorized decorators
3. Validates all decorator usage against whitelist
4. Fails build if unauthorized decorators found
Bundle Processing:
During bundle compilation:
1. decorator.js loaded first
2. Decorator definitions processed
3. Decorator applications validated
4. Bundle compilation completes or fails with clear errors
TROUBLESHOOTING
Unauthorized Decorator Error:
Error: "Decorator 'functionName' not whitelisted"
Solution: Add @decorator marker to function definition
Decorator Not Found:
Error: "Decorator 'decoratorName' not found"
Solution: Ensure decorator definition loaded before usage
Instance Method Decoration:
Error: "Decorators can only be applied to static methods"
Solution: Change method to static or remove decorator
Build-Time Validation Failures:
Check manifest build output for specific decorator validation errors
Ensure all used decorators are properly whitelisted
TESTING DECORATORS
Unit Testing Decorator Logic:
// Test decorator implementation
const mockDescriptor = {
value: jest.fn(() => 'original')
};
const decorated = myDecorator({}, 'testMethod', mockDescriptor);
const result = decorated.value();
expect(result).toBe('original');
Integration Testing:
// Test decorated methods
class TestClass {
@myDecorator
static testMethod() {
return 'test';
}
}
expect(TestClass.testMethod()).toBe('test');
PERFORMANCE CONSIDERATIONS
Decorator Overhead:
Decorators add function call overhead. Use judiciously on
performance-critical methods.
Caching Decorators:
Implement caching carefully to avoid memory leaks with
dynamic data or large result sets.
Bundle Size:
Excessive decorator usage can increase bundle size.
Consider if simple function calls would be more appropriate.
SEE ALSO
bundle_api - Bundle loading and compilation
manifest_api - Build-time validation system
coding_standards - JavaScript coding patterns

349
app/RSpade/man/migrations.txt Executable file
View File

@@ -0,0 +1,349 @@
MIGRATIONS(7) RSX Framework Manual MIGRATIONS(7)
NAME
migrations - Database migration system with raw SQL enforcement
SYNOPSIS
php artisan make:migration:safe <name>
php artisan migrate:begin
php artisan migrate [--production]
php artisan migrate:commit
php artisan migrate:rollback
DESCRIPTION
The RSX framework enforces a forward-only migration strategy using raw SQL
statements. Laravel's Schema builder is prohibited to ensure clarity,
auditability, and prevent hidden behaviors.
PHILOSOPHY
1. Forward-only migrations - No rollbacks, no down() methods
2. Raw SQL only - Direct MySQL statements, no abstractions
3. Fail loud - Migrations must succeed or fail with clear errors
4. Snapshot safety - Development requires database snapshots before migrating
MIGRATION RULES
Schema Builder Prohibition
All migrations MUST use DB::statement() with raw SQL. The following are prohibited:
• Schema::create()
• Schema::table()
• Schema::drop()
• Schema::dropIfExists()
• Schema::rename()
• Blueprint class usage
• $table-> method chains
The migration validator automatically checks for these patterns and will prevent
migrations from running if violations are found.
Required Table Structure
ALL tables MUST have:
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
This is non-negotiable. Every table needs this exact ID column (SIGNED for
easier future migrations).
Data Type Standards - What You Need to Know
The framework automatically normalizes data types during migration, so you can
use simpler types and let the system handle optimization:
What You Can Use (System Auto-Converts):
• INT → automatically becomes BIGINT
• TEXT → automatically becomes LONGTEXT
• FLOAT → automatically becomes DOUBLE
• Any charset → automatically becomes UTF8MB4
• created_at/updated_at → automatically added with proper defaults
• created_by/updated_by → automatically added
• deleted_by → automatically added for soft-delete tables
What You MUST Be Careful About:
• Foreign key columns - Must match the referenced column type exactly
Example: If users.id is BIGINT, then orders.user_id must be BIGINT
• TINYINT(1) - Preserved for boolean values, won't be converted
• Column names ending in _id are assumed to be foreign keys
Recommended for Simplicity:
• Just use INT for integers (becomes BIGINT automatically)
• Just use TEXT for long content (becomes LONGTEXT automatically)
• Just use FLOAT for decimals (becomes DOUBLE automatically)
• Don't add created_at/updated_at (added automatically)
• Don't add created_by/updated_by (added automatically)
down() Method Removal
The migration system automatically removes down() methods from migration files.
Migrations are forward-only - database changes should never be reversed.
AUTOMATIC NORMALIZATION
What migrate:normalize_schema Does For You
After migrations run, the normalize_schema command automatically:
1. Type Conversions:
• INT columns → BIGINT (except TINYINT(1) for booleans)
• BIGINT UNSIGNED → BIGINT SIGNED
• TEXT → LONGTEXT
• FLOAT → DOUBLE
• All text columns → UTF8MB4 character set
2. Required Columns Added:
• created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)
• updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE
• created_by INT(11) NULL
• updated_by INT(11) NULL
• deleted_by INT(11) NULL (only for soft-delete tables)
3. Indexes Added:
• INDEX on created_at
• INDEX on updated_at
• INDEX on site_id (for Siteable models)
• INDEX on id+version (for Versionable models)
4. Trait-Specific Columns:
• site_id INT(11) - for models using Siteable trait
• version INT(11) DEFAULT 1 - for Versionable/Ajaxable traits
5. Precision Upgrades:
• All DATETIME/TIMESTAMP columns → precision (3) for milliseconds
This means you can write simpler migrations and let the system handle the
optimization and standardization. The only time you need to be explicit about
types is when creating foreign key columns that must match their referenced
column exactly.
VALIDATION SYSTEM
Automatic Validation
When running migrations in non-production mode, the system automatically:
1. Validates all pending migrations for Schema builder usage
2. Removes down() methods if present
3. Reports violations with colored output and remediation advice
4. Stops at the first violation to allow correction
Validation Output
When a violation is detected, you'll see:
❌ Migration Validation Failed
File: 2025_09_30_create_example_table.php
Line: 28
Violation: Found forbidden Schema builder usage: Schema::create
Code Preview:
────────────────────────────────────────
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
});
────────────────────────────────────────
Remediation: Use DB::statement("CREATE TABLE...") instead
Example:
DB::statement('CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
created_at TIMESTAMP NULL DEFAULT NULL
)');
Bypassing Validation
In production mode (--production flag or APP_ENV=production), validation is
skipped. This should only be used when absolutely necessary.
MIGRATION WORKFLOW
Development Workflow
1. Create snapshot: php artisan migrate:begin
2. Create migration: php artisan make:migration:safe <name>
3. Write migration using raw SQL
4. Run migrations: php artisan migrate
5. If successful: php artisan migrate:commit
6. If failed: System auto-rollbacks to snapshot
Production Workflow
1. Create migration: php artisan make:migration:safe <name>
2. Write migration using raw SQL
3. Test thoroughly in development/staging
4. Run migrations: php artisan migrate --production
Note: No snapshot protection in production mode. Ensure migrations are
thoroughly tested before running in production.
MIGRATION EXAMPLES
Creating a Table (Simple Version - Recommended)
public function up()
{
// You can write this simple version - system auto-normalizes types
DB::statement("
CREATE TABLE products (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -- becomes BIGINT
name VARCHAR(255) NOT NULL,
description TEXT, -- becomes LONGTEXT
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
stock_quantity INT NOT NULL DEFAULT 0, -- becomes BIGINT
is_active TINYINT(1) NOT NULL DEFAULT 1, -- stays TINYINT(1)
category_id INT NULL, -- becomes BIGINT
INDEX idx_category (category_id),
INDEX idx_active (is_active)
-- No need for created_at/updated_at - added automatically
)
");
}
Creating a Table (Explicit Version - If You Prefer)
public function up()
{
// Or be explicit about types if you prefer
DB::statement("
CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description LONGTEXT,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
stock_quantity BIGINT NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
category_id BIGINT NULL,
created_at TIMESTAMP NULL DEFAULT NULL,
updated_at TIMESTAMP NULL DEFAULT NULL,
INDEX idx_category (category_id),
INDEX idx_active (is_active),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
Adding Columns
public function up()
{
DB::statement("ALTER TABLE users ADD COLUMN age BIGINT NULL AFTER email");
DB::statement("ALTER TABLE users ADD INDEX idx_age (age)");
}
Modifying Columns
public function up()
{
// Change column type
DB::statement("ALTER TABLE products MODIFY COLUMN price DECIMAL(12,2)");
// Rename column
DB::statement("ALTER TABLE users CHANGE COLUMN username user_name VARCHAR(100)");
// Add default value
DB::statement("ALTER TABLE posts ALTER COLUMN status SET DEFAULT 'draft'");
}
Managing Indexes
public function up()
{
// Add index
DB::statement("CREATE INDEX idx_email ON users (email)");
// Add unique index
DB::statement("CREATE UNIQUE INDEX idx_unique_slug ON posts (slug)");
// Add composite index
DB::statement("CREATE INDEX idx_user_status ON orders (user_id, status)");
// Drop index
DB::statement("DROP INDEX idx_old_index ON table_name");
}
Foreign Keys (IMPORTANT - Match Types Exactly)
public function up()
{
// CRITICAL: Foreign key columns must match referenced column type
// If users.id is BIGINT, orders.user_id must also be BIGINT
// First, ensure the column has correct type (if not already created)
DB::statement("ALTER TABLE orders ADD COLUMN user_id BIGINT NULL");
// Then add the foreign key constraint
DB::statement("
ALTER TABLE orders
ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
");
// To drop a foreign key
DB::statement("ALTER TABLE orders DROP FOREIGN KEY orders_user_fk");
}
// Note: After normalization, all id columns are BIGINT, so foreign keys
// should always use BIGINT to avoid type mismatches
Data Migrations
public function up()
{
// Simple update
DB::statement("UPDATE users SET role = 'member' WHERE role IS NULL");
// Complex migration with temporary column
DB::statement("ALTER TABLE orders ADD COLUMN total_new DECIMAL(10,2)");
DB::statement("UPDATE orders SET total_new = quantity * price");
DB::statement("ALTER TABLE orders DROP COLUMN total");
DB::statement("ALTER TABLE orders CHANGE total_new total DECIMAL(10,2)");
}
ERROR MESSAGES
"Migration validation failed: Schema builder usage detected"
Your migration uses Laravel's Schema builder. Rewrite using DB::statement()
with raw SQL.
"Migration mode not active!"
You're in development mode and haven't created a snapshot. Run:
php artisan migrate:begin
"Unauthorized migrations detected!"
Migration files exist that weren't created via make:migration:safe.
Recreate them using the proper command.
SECURITY CONSIDERATIONS
SQL Injection
When using dynamic values in migrations, always use parameter binding:
✅ CORRECT:
DB::statement("UPDATE users SET status = ? WHERE created_at < ?", ['active', '2025-01-01']);
❌ WRONG:
DB::statement("UPDATE users SET status = '$status' WHERE created_at < '$date'");
Production Safety
• Always test migrations in development/staging first
• Keep migrations small and focused
• Never reference models or services in migrations
• Migrations must be self-contained and idempotent where possible
DEBUGGING
Viewing Pending Migrations
php artisan migrate:status
Testing a Migration
1. Create snapshot: php artisan migrate:begin
2. Run migration: php artisan migrate
3. If it fails, automatic rollback occurs
4. Fix the migration file
5. Try again: php artisan migrate
Common Issues
• "Class not found" - Don't reference models in migrations
• "Syntax error" - Check your SQL syntax, test in MySQL client first
• "Foreign key constraint" - Ensure referenced table/column exists
• "Duplicate column" - Check if column already exists before adding
SEE ALSO
rsx:man database - Database system overview
rsx:man coding_standards - General coding standards
rsx:man error_handling - Error handling patterns
AUTHORS
RSX Framework Team
RSX Framework September 2025 MIGRATIONS(7)

262
app/RSpade/man/model.txt Executable file
View File

@@ -0,0 +1,262 @@
RSpade Models (Rsx_Model_Abstract)
OVERVIEW
All RSX models must extend Rsx_Model_Abstract, which provides:
- Enum system with magic properties and methods
- Mass assignment prevention (explicit field assignment only)
- Eager loading prevention (explicit queries only)
- Ajax ORM fetch() system
- Automatic boolean casting for TINYINT(1) columns
- Relationship management via #[Relationship] attribute
ENUM SYSTEM
Define enums using public static $enums property:
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'order' => 1],
2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive', 'order' => 2]
],
'role_id' => [
1 => ['constant' => 'ROLE_ADMIN', 'label' => 'Administrator'],
2 => ['constant' => 'ROLE_USER', 'label' => 'User']
]
];
Magic Properties (Instance):
- $model->status_id_label - Get label for current enum value
- $model->status_id_constant - Get constant name for current value
- $model->status_id_order - Get sort order for current value
- $model->status_id_enum_val - Get all properties for current value
Magic Methods (Static):
- Model::status_id_enum() - Get all enum definitions (sorted by 'order')
- Model::status_id_enum_select() - Get key/label pairs for dropdowns
- Model::status_id_enum_ids() - Get all possible enum values
Enum properties in $enums can include any custom keys (label, order, constant,
color, icon, etc.). All properties are exported to JavaScript via toArray().
Optional 'selectable' property (default true) controls dropdown visibility:
2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false]
MASS ASSIGNMENT PREVENTION
Mass assignment is explicitly prohibited. Use explicit field assignment:
// CORRECT
$model = new User_Model();
$model->name = $request->input('name');
$model->email = $request->input('email');
$model->save();
// WRONG - throws MassAssignmentException
$model = User_Model::create(['name' => 'John', 'email' => 'john@example.com']);
$model->fill(['name' => 'John']);
$model->update(['name' => 'John']);
Blocked methods: fill(), forceFill(), create(), firstOrCreate(), firstOrNew(),
updateOrCreate(), update()
EAGER LOADING PREVENTION
All forms of eager loading throw exceptions. Use explicit queries:
// WRONG - throws exception
$users = User_Model::with('posts')->get();
$user->load('posts');
// CORRECT - explicit queries
$users = User_Model::all();
foreach ($users as $user) {
$posts = Post_Model::where('user_id', $user->id)->get();
}
Rationale: Premature optimization is the root of all evil. When you have
thousands of concurrent users in production and N+1 queries become a real
bottleneck, you'll be motivated enough to remove these restrictions yourself.
AJAX ORM (Model Fetch System)
Models can opt-in to client-side fetching by implementing fetch() with
#[Ajax_Endpoint_Model_Fetch] attribute:
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
// Check authorization
if (!RsxAuth::check()) {
return false;
}
// Fetch single record
$model = static::find($id);
return $model ?: false;
}
JavaScript usage:
const product = await Product_Model.fetch(1);
const products = await Product_Model.fetch([1, 2, 3]);
Framework automatically splits array IDs into individual fetch() calls for
security (no mass fetching).
See: php artisan rsx:man model_fetch
RELATIONSHIPS
Define relationships using #[Relationship] attribute to prevent Laravel from
auto-detecting non-relationship methods:
#[Relationship]
public function posts()
{
return $this->hasMany(Post_Model::class);
}
Only methods with #[Relationship] are considered relationships. This prevents
methods like is_active() from being mistaken for relationships.
Note: Eager loading is still blocked. Relationships are supported but must be
loaded with explicit queries.
AUTOMATIC BOOLEAN CASTING
TINYINT(1) columns are automatically cast to boolean without manual $casts
definition. Framework consults manifest database metadata to detect boolean
columns at runtime.
Manual $casts entries take precedence over automatic detection.
NEVER EXPORT FIELDS
Exclude sensitive fields from toArray() and Ajax responses:
protected $neverExport = ['password', 'api_token', 'remember_token'];
Fields in $neverExport are automatically removed before JavaScript export.
UTILITY METHODS
Static Methods:
- get_table_static() - Get table name without instantiating model
- getColumns() - Get array of column names for model's table
- hasColumn($column) - Check if column exists
- get_relationships() - Get array of relationship method names
- clear_table_cache() - Clear query cache for table
- clear_all_caches() - Clear all query caches
Instance Methods:
- make_new() - Create new model instance (recommended over new Model())
- get_mass_assignment_example() - Get helpful example code for this model
MODEL ORGANIZATION WITH TRAITS
Large models can be split into domain-specific trait files. This organization
strategy is primarily designed for models, though traits can be used elsewhere
in RSX applications.
Naming Convention:
Trait files should be prefixed with the model name they elaborate on:
Users_Model -> Users_Model_Authentication (trait for auth methods)
Users_Model -> Users_Model_Relationships (trait for relationship methods)
Users_Model -> Users_Model_Permissions (trait for permission logic)
This makes it immediately clear which model a trait belongs to.
Organization Strategy:
Group methods by problem domain within separate trait files:
- Authentication/authorization logic
- Query methods for specific use cases
- Relationship definitions
- Business logic for specific features
- Computed properties and accessors
Example Implementation:
// rsx/models/traits/users_model_authentication.php
trait Users_Model_Authentication
{
public static function find_by_email(string $email)
{
return static::where('email', $email)->first();
}
public static function find_by_api_token(string $token)
{
return static::where('api_token', $token)->first();
}
public function verify_password(string $password): bool
{
return password_verify($password, $this->password);
}
}
// rsx/models/traits/users_model_relationships.php
trait Users_Model_Relationships
{
#[Relationship]
public function posts()
{
return $this->hasMany(Posts_Model::class);
}
#[Relationship]
public function profile()
{
return $this->hasOne(User_Profiles_Model::class);
}
}
// rsx/models/users_model.php
class Users_Model extends Rsx_Model_Abstract
{
use Users_Model_Authentication;
use Users_Model_Relationships;
protected $table = 'users';
public static $enums;
protected $neverExport = ['password', 'api_token'];
}
Manifest Integration:
The manifest system automatically discovers and indexes methods from traits:
- Trait files are loaded before classes during manifest build
- Methods from traits appear in the model's metadata
- IDE helpers correctly locate trait method definitions
- Code quality rules validate trait methods same as class methods
Benefits:
- Keep models focused and maintainable
- Group related functionality by problem domain
- Easier to navigate large models with hundreds of methods
- Clear separation between different aspects of model behavior
- Traits are discovered and validated automatically by manifest
NAMING CONVENTIONS
Tables:
- Plural snake_case (users, user_logins, product_categories)
- Always include 'id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY'
Columns:
- snake_case for all column names
- Foreign keys must suffix with '_id' (user_id, site_id)
- Booleans must prefix with 'is_' (is_active, is_published)
- Timestamps must suffix with '_at' (created_at, updated_at, deleted_at)
- Use BIGINT for all integers, TINYINT(1) for booleans only
- All text columns use UTF-8 (utf8mb4_unicode_ci collation)
SEE ALSO
php artisan rsx:man model_fetch - Ajax ORM fetch system
php artisan rsx:man model_normalization - Database normalization process
php artisan rsx:man enums - Complete enum system documentation
php artisan rsx:man migrations - Migration system and safety

345
app/RSpade/man/model_fetch.txt Executable file
View File

@@ -0,0 +1,345 @@
NAME
model_fetch - RSX Ajax ORM with secure model fetching from JavaScript
SYNOPSIS
Secure, controlled access to ORM models from JavaScript with explicit opt-in
DESCRIPTION
The RSX model fetch system allows JavaScript to securely access ORM models
through an explicit opt-in mechanism. Unlike Laravel's default API routes
which expose all model data, RSX requires each model to implement its own
fetch() method with authorization and data filtering.
Key differences from Laravel:
- Laravel: API routes with resource controllers expose all fields
- RSX: Each model implements custom fetch() with authorization
Benefits:
- Explicit security control per model
- No mass exposure of sensitive data
- Individual authorization checks for each record
- Automatic JavaScript stub generation
SECURITY MODEL
Explicit Opt-In:
Models must deliberately implement fetch() with the attribute.
No models are fetchable by default.
Individual Authorization:
Each model controls who can fetch its records.
Authorization checked for every single record.
Data Filtering:
Models can filter sensitive fields before returning data.
Complete control over what data JavaScript receives.
No Mass Fetching:
Framework splits array requests into individual fetch calls.
Prevents bulk data extraction without individual authorization.
IMPLEMENTING FETCHABLE MODELS
Required Components:
1. Override static fetch() method
2. Add #[Ajax_Endpoint_Model_Fetch] attribute
3. Accept exactly one parameter: $id (single ID only)
4. Implement authorization checks
5. Return model object or false
Basic Implementation:
use Ajax_Endpoint_Model_Fetch;
class Product_Model extends Rsx_Model
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
// Authorization check
if (!RsxAuth::check()) {
return false;
}
// Fetch single record
$model = static::find($id);
return $model ?: false;
}
}
Advanced Authorization:
class Order_Model extends Rsx_Model
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
$user = RsxAuth::user();
if (!$user) {
return false;
}
$order = static::find($id);
if (!$order) {
return false;
}
// Only allow access to own orders or admin users
if ($order->user_id !== $user->id && !$user->is_admin) {
return false;
}
return $order;
}
}
Data Filtering:
class User_Model extends Rsx_Model
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!RsxAuth::check()) {
return false;
}
$user = static::find($id);
if (!$user) {
return false;
}
// Remove sensitive fields
unset($user->password_hash);
unset($user->remember_token);
unset($user->email_verification_token);
return $user;
}
}
JAVASCRIPT USAGE
Single Record Fetching:
// Fetch single record
const product = await Product_Model.fetch(1);
if (product) {
console.log(product.name);
console.log(product.price);
}
// Handle fetch failure
const order = await Order_Model.fetch(999);
if (!order) {
console.log('Order not found or access denied');
}
Multiple Record Fetching:
// Framework automatically splits array into individual calls
const products = await Product_Model.fetch([1, 2, 3]);
products.forEach(product => {
if (product) {
console.log(product.name);
}
});
// Mixed results (some succeed, some fail authorization)
const orders = await Order_Model.fetch([101, 102, 103]);
const validOrders = orders.filter(order => order !== false);
Error Handling:
try {
const user = await User_Model.fetch(userId);
if (user) {
updateUserInterface(user);
} else {
showAccessDeniedMessage();
}
} catch (error) {
console.error('Fetch failed:', error);
showErrorMessage();
}
ARRAY HANDLING
Framework Behavior:
When JavaScript passes an array to fetch(), the framework:
1. Splits array into individual IDs
2. Calls fetch() once for each ID
3. Collects results maintaining array order
4. Returns array with same length (false for failed fetches)
Implementation Rules:
- NEVER use is_array($id) checks in fetch() method
- Always handle exactly one ID parameter
- Framework handles array splitting automatically
- Results maintain original array order
Example Results:
// JavaScript call
const results = await Product_Model.fetch([1, 2, 999]);
// Results array (999 not found or unauthorized)
[
{id: 1, name: "Product A"}, // Successful fetch
{id: 2, name: "Product B"}, // Successful fetch
false // Failed fetch
]
STUB GENERATION
Automatic JavaScript Stubs:
The framework automatically generates JavaScript stub classes
for models with #[Ajax_Endpoint_Model_Fetch] attributes.
Stub Class Generation:
// Generated stub for Product_Model
class Product_Model {
static async fetch(id) {
// Generated implementation calls PHP fetch() method
return await Rsx._internal_api_call('Product_Model', 'fetch', {id});
}
}
Bundle Integration:
Stubs are automatically included in JavaScript bundles when
models are discovered in the bundle's include paths.
AUTHORIZATION PATTERNS
User-Specific Access:
class User_Profile_Model extends Rsx_Model
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
$current_user = RsxAuth::user();
// Users can only fetch their own profile
if (!$current_user || $current_user->id != $id) {
return false;
}
return static::find($id);
}
}
Role-Based Access:
class Admin_Report_Model extends Rsx_Model
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
$user = RsxAuth::user();
// Only admin users can fetch reports
if (!$user || !$user->hasRole('admin')) {
return false;
}
return static::find($id);
}
}
Public Data Access:
class Public_Article_Model extends Rsx_Model
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
// Public articles can be fetched by anyone
$article = static::find($id);
// But only return published articles
if ($article && $article->status === 'published') {
return $article;
}
return false;
}
}
BASE MODEL PROTECTION
Default Behavior:
The base Rsx_Model::fetch() method throws an exception with
clear instructions on how to implement fetch() in your model.
Exception Message:
"Model MyModel does not implement fetch() method. To enable Ajax
fetching, add #[Ajax_Endpoint_Model_Fetch] attribute and implement
static fetch($id) method with authorization checks."
Purpose:
- Prevents accidental exposure of model data
- Forces developers to explicitly handle security
- Provides clear implementation guidance
- Ensures no models are fetchable by default
TESTING FETCH METHODS
PHP Testing:
// Test authorization
$user = User_Model::factory()->create();
RsxAuth::login($user);
$product = Product_Model::fetch(1);
$this->assertNotFalse($product);
RsxAuth::logout();
$product = Product_Model::fetch(1);
$this->assertFalse($product);
JavaScript Testing (via rsx:debug):
php artisan rsx:debug /demo --eval="Product_Model.fetch(1).then(r => console.log(r))"
COMMON PATTERNS
Soft Delete Handling:
public static function fetch($id)
{
// Only return non-deleted records
$model = static::where('id', $id)->whereNull('deleted_at')->first();
return $model ?: false;
}
Relationship Preloading:
public static function fetch($id)
{
$model = static::with(['category', 'tags'])->find($id);
return $model ?: false;
}
Conditional Field Removal:
public static function fetch($id)
{
$model = static::find($id);
if (!$model) return false;
$user = RsxAuth::user();
if (!$user || !$user->is_admin) {
// Remove admin-only fields for non-admin users
unset($model->internal_notes);
unset($model->cost_price);
}
return $model;
}
TROUBLESHOOTING
Model Not Fetchable:
- Verify #[Ajax_Endpoint_Model_Fetch] attribute present
- Check fetch() method is static and public
- Ensure method accepts single $id parameter
- Verify model included in bundle manifest
Authorization Failures:
- Check RsxAuth::check() and RsxAuth::user() values
- Verify authorization logic in fetch() method
- Test with different user roles and permissions
- Use rsx_dump_die() to debug authorization flow
JavaScript Stub Missing:
- Verify model discovered during manifest build
- Check bundle includes model's directory
- Ensure bundle compiles without errors
- Confirm JavaScript bundle loads in browser
Array Handling Issues:
- Never use is_array($id) in fetch() method
- Framework handles array splitting automatically
- Check for typos in model class names
- Verify all IDs in array are valid integers
SEE ALSO
controller - Internal API attribute patterns
manifest_api - Model discovery and stub generation
coding_standards - Security patterns and authorization

444
app/RSpade/man/model_indexes.txt Executable file
View File

@@ -0,0 +1,444 @@
NAME
model_indexes - Database index analysis and recommendation system
SYNOPSIS
Automatic detection of required database indexes from model relationships
and query patterns
DESCRIPTION
The RSX index analysis system automatically discovers which database
indexes are required by analyzing model relationships and query patterns
in your codebase. It generates optimized index recommendations using
covering index logic and produces reviewable migration files.
Unlike manual index management where developers must remember to create
indexes for every foreign key and query pattern, RSX automatically
detects missing indexes by analyzing:
1. Model relationships - Foreign key columns from #[Relationship] methods
2. Query patterns - AST analysis of Model::where() chains in /rsx/
Key differences from manual approaches:
- Manual: Developer creates indexes as they remember
- RSX: Automatic detection from code analysis
- Manual: No way to know if indexes are missing
- RSX: Command reports all missing indexes
- Manual: Risk of redundant indexes
- RSX: Covering index logic optimizes recommendations
Benefits:
- Never forget to index foreign keys
- Automatic detection of query patterns
- Optimized recommendations via covering logic
- Quota management prevents too many indexes
- Source attribution shows why each index is needed
COMMAND USAGE
Basic Analysis:
php artisan rsx:db:check_indexes
Shows summary of missing indexes and recommendations.
Generate Migration:
php artisan rsx:db:check_indexes --generate-migration
Creates migration file in database/migrations/ with DDL statements.
Detailed Analysis:
php artisan rsx:db:check_indexes --show-details
Shows verbose output with file counts and analysis progress.
Analyze Specific Table:
php artisan rsx:db:check_indexes --table=users
Limit analysis to single table.
Skip Relationship Analysis:
php artisan rsx:db:check_indexes --no-relationships
Only analyze query patterns, ignore relationship indexes.
Skip Query Pattern Analysis:
php artisan rsx:db:check_indexes --no-queries
Only analyze relationships, ignore query patterns.
RELATIONSHIP INDEX DETECTION
How It Works:
The system scans all models extending Rsx_Model_Abstract and finds
methods with #[Relationship] attribute. For each relationship, it
determines which table needs an index on which foreign key column.
Detected Relationships:
- hasMany() / hasOne() - Index on related table's foreign key
- belongsTo() - No index needed (we have the foreign key)
- belongsToMany() - Indexes on both pivot table foreign keys
- morphMany() / morphOne() - Index on related table's morph_id
Priority:
Relationship indexes have HIGHEST priority and are never cut due to
quota limits. Foreign keys must always be indexed for performance.
Example Detection:
// In User_Model.php
#[Relationship]
public function posts()
{
return $this->hasMany(Post_Model::class);
}
// Detected requirement: posts table needs index on user_id column
// Source attribution: rsx/models/user_model.php:45
QUERY PATTERN DETECTION
How It Works:
The system uses AST parsing to scan all PHP files in /rsx/ that
define classes. It finds Model::where() method chains and extracts
the column names in order.
Detected Patterns:
// Single where clause
User_Model::where('status', 'active')->get();
// Detected: Index on (status)
// Chained where clauses
User_Model::where('status', 'active')
->where('created_at', '>', '2024-01-01')
->get();
// Detected: Index on (status, created_at)
// Multiple columns preserve order
Order_Model::where('user_id', $id)
->where('status', 'pending')
->where('created_at', '>', $date)
->get();
// Detected: Index on (user_id, status, created_at)
Not Detected (Documented Limitations):
- Variable column names: where($column, 'value')
- Complex WHERE clauses: whereIn(), whereRaw(), whereExists()
- Join conditions (handled separately via relationship analysis)
- Runtime-determined queries
- orderBy() clauses (result set ordering, not filtering)
Priority:
Query pattern indexes have MEDIUM priority and are subject to quota
limits. Multi-column indexes are preferred over single-column.
Column Order:
The system preserves the exact order of columns from your where()
chain because MySQL index order matters for left-prefix matching.
COVERING INDEX OPTIMIZATION
Left-Prefix Matching Rule:
MySQL can use an index (a, b, c) to satisfy queries filtering on:
- (a) alone
- (a, b) together
- (a, b, c) together
But it CANNOT use the index for:
- (b) alone
- (c) alone
- (b, c) together
Optimization Strategy:
The system uses covering index logic to reduce redundant indexes:
Requirements: (status), (status, created_at), (status, role)
Analysis:
- (status, created_at) covers (status) via left-prefix
- (status, role) covers (status) via left-prefix
- Neither covers the other (different second columns)
Result: Create both (status, created_at) and (status, role)
Drop standalone (status) - it's covered
Reordering for Coverage:
If you need both (status) and (user_id, status), the system could
theoretically create (status, user_id) to cover both. However, this
would hurt performance for queries filtering on user_id alone.
Current Implementation: Does not reorder. Preserves query patterns.
INDEX QUOTA MANAGEMENT
MySQL Limits:
MySQL InnoDB has a hard limit of 64 indexes per table.
RSX Quota:
The system reserves 32 indexes per table for auto-generated indexes,
leaving 32 for user-defined custom indexes.
When Quota Exceeded:
If recommendations exceed available quota, the system prioritizes:
Priority Tier 1: Relationship indexes (never cut)
Priority Tier 2: Multi-column query indexes (2+ columns)
Priority Tier 3: Single-column query indexes (cut first)
The command displays a warning showing which indexes were cut and
suggests manual review for those that couldn't be auto-applied.
Example Warning:
⚠️ WARNING: Table 'users' exceeds index quota
Recommended indexes: 45
Available quota: 12 (64 max - 52 existing user indexes)
The following 33 indexes cannot be auto-applied:
- idx_auto_status (single-column, cut)
- idx_auto_email (single-column, cut)
...
Consider reviewing existing user-defined indexes for redundancy.
MIGRATION GENERATION
Generated Migration Format:
php artisan rsx:db:check_indexes --generate-migration
Creates: database/migrations/{timestamp}_auto_index_recommendations.php
Migration Content:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up()
{
// Table: users
// ============================================
// DROP: Obsolete auto-generated indexes
DB::statement('DROP INDEX IF EXISTS idx_auto_old_column ON users');
// CREATE: New recommended indexes
// Source: rsx/app/frontend/users_controller.php:45
// Priority: query (where)
DB::statement('
CREATE INDEX idx_auto_status_created_at
ON users (status, created_at)
');
// Source: rsx/models/user_model.php:67 (#[Relationship])
// Priority: relationship (hasMany)
DB::statement('
CREATE INDEX idx_auto_organization_id
ON users (organization_id)
');
}
};
Index Naming Convention:
Auto-generated indexes use pattern: idx_auto_{col1}_{col2}_{col3}
Examples:
- idx_auto_user_id
- idx_auto_status_created_at
- idx_auto_status_role_created_at
Source Attribution:
Each CREATE statement includes comments showing:
- Source file and line number where requirement detected
- Priority level (relationship or query)
- Relationship type or query pattern type
Applying Migration:
After reviewing the generated migration:
php artisan migrate
The migration creates new indexes and drops obsolete ones that
are no longer needed.
AUTO-GENERATED INDEX MANAGEMENT
Identification:
The system identifies auto-generated indexes by the idx_auto_ prefix.
Lifecycle:
- System creates indexes with idx_auto_ prefix
- On subsequent runs, system checks which are still needed
- Obsolete auto-generated indexes are dropped
- User-defined indexes are NEVER touched
User-Defined Indexes:
Any index NOT starting with idx_auto_ is considered user-defined
and is completely ignored by the system. This allows developers
to manually create custom indexes without interference.
Idempotency:
Running the command multiple times without code changes produces
no recommendations. The system only suggests changes when:
- New relationships are added
- New query patterns are detected
- Existing relationships are removed
- Existing query patterns change
OUTPUT EXAMPLES
Success Output:
Database Index Analysis
==================================================
Found 12 indexes to create and 4 to drop
Table: users
+ Create index on (status, created_at) - rsx/controllers/users.php:23
+ Create index on (organization_id) - rsx/models/user_model.php:45
- Drop index idx_auto_old_status on (old_status)
Table: posts
+ Create index on (user_id) - rsx/models/post_model.php:12
To generate migration:
php artisan rsx:db:check_indexes --generate-migration
All Indexes Up To Date:
Database Index Analysis
==================================================
✅ All indexes are up to date
With Detailed Analysis:
Database Index Analysis
==================================================
Analyzing model relationships...
Found 23 relationships
Analyzing query patterns...
Scanning 47 PHP files for query patterns
Query pattern analysis complete
Total requirements collected: 87
Optimizing index requirements...
Found 12 indexes to create and 4 to drop
...
LIMITATIONS WARNING
The command always displays limitations to set expectations:
⚠️ LIMITATIONS
This analysis cannot detect:
✗ Dynamic column names: where($variable, ...)
✗ Join conditions (separate relationship analysis)
✗ Complex WHERE clauses (whereRaw, whereIn, etc.)
✗ Runtime-determined query patterns
Always benchmark critical queries and add custom indexes as needed.
Use EXPLAIN to verify query performance in production.
Why These Limitations:
Static analysis cannot predict runtime behavior. The system can
only analyze code patterns visible in source files.
Recommendations:
- Use EXPLAIN on critical queries in production
- Benchmark slow queries and add custom indexes
- Review query logs for patterns not detected by AST
- Run command periodically as codebase evolves
WORKFLOW
Regular Development:
1. Write models with #[Relationship] methods
2. Write controllers with Model::where() queries
3. Run: php artisan rsx:db:check_indexes
4. Review recommendations
5. Run: php artisan rsx:db:check_indexes --generate-migration
6. Review generated migration file
7. Run: php artisan migrate
Before Deployment:
Always run the index analysis before deploying to production to
ensure all required indexes exist. Missing indexes can cause
severe performance issues in production.
Periodic Audits:
Run the command periodically to catch missing indexes as your
codebase evolves. New features often introduce new relationships
and query patterns.
EXAMPLES
Analyze All Tables:
php artisan rsx:db:check_indexes
Generate Migration:
php artisan rsx:db:check_indexes --generate-migration
Check Specific Table:
php artisan rsx:db:check_indexes --table=users
Relationships Only:
php artisan rsx:db:check_indexes --no-queries
Queries Only:
php artisan rsx:db:check_indexes --no-relationships
Verbose Output:
php artisan rsx:db:check_indexes --show-details
TROUBLESHOOTING
No Indexes Detected:
- Verify models extend Rsx_Model_Abstract
- Check #[Relationship] attributes present on relationship methods
- Ensure PHP files in /rsx/ define classes
- Run with --show-details to see file count
Missing Query Pattern Detection:
- Verify Model::where() uses string literals, not variables
- Check model class names end with _Model suffix
- Ensure AST parser can parse file syntax
- Complex queries may not be detected (use custom indexes)
Quota Warnings:
- Review existing user-defined indexes for redundancy
- Consider if all existing indexes are still needed
- Manually select which auto-indexes to apply
- Some tables genuinely need many indexes
Migration Fails:
- Check MySQL version supports CREATE INDEX IF NOT EXISTS
- Verify table names are correct
- Ensure sufficient MySQL privileges
- Review migration file for syntax errors
BEST PRACTICES
Relationship Indexes:
Always use #[Relationship] attribute on relationship methods.
This ensures the system can detect foreign key requirements.
Query Patterns:
Use string literals in where() clauses when possible:
- Good: where('status', 'active')
- Bad: where($column, 'active')
Column Order Matters:
Order your where() clauses by selectivity:
- Most selective columns first
- Example: where('user_id', $id)->where('status', 'active')
- Better than: where('status', 'active')->where('user_id', $id)
Custom Indexes:
For queries the system can't detect, create custom indexes:
- Use standard naming: idx_users_status_created_at
- Avoid idx_auto_ prefix (reserved for system)
- Document why the index is needed
Regular Audits:
Run index analysis as part of deployment checklist.
Missing indexes are easier to add before production.
Migration Review:
Always review generated migrations before applying.
Verify recommendations make sense for your data patterns.
SEE ALSO
model - RSX model system with relationships
model_normalization - Schema normalization command
migrations - Database migration system
coding_standards - Code quality and standards

View File

@@ -0,0 +1,474 @@
DATABASE NORMALIZATION & MIGRATION SYSTEM
==========================================
RSpade Framework Database Normalization Documentation
OVERVIEW
--------
RSpade enforces an opinionated database schema through a two-layer system:
1. **SqlQueryTransformer** - Rewrites DDL statements during migrations to enforce
consistent column types and character sets at CREATE/ALTER time
2. **migrate:normalize_schema** - Adds framework-required columns and fixes drift
in existing tables
This eliminates foreign key type mismatches and ensures consistent schema across
all environments without manual ALTER statements.
ENFORCED COLUMN TYPE STANDARDS
-------------------------------
### Integer Types
✓ BIGINT - All integer columns (IDs, counts, foreign keys)
✓ TINYINT(1) - Boolean values only (true/false)
✗ INT, INTEGER - Automatically converted to BIGINT
✗ MEDIUMINT - Automatically converted to BIGINT
✗ SMALLINT - Automatically converted to BIGINT
✗ UNSIGNED - Removed (framework uses signed integers)
### Floating Point Types
✓ DOUBLE - All floating point numbers
✓ DECIMAL(p,s) - Exact precision (money, percentages)
✗ FLOAT, REAL - Automatically converted to DOUBLE
### String Types
✓ VARCHAR(n) - Variable length strings (with utf8mb4)
✓ LONGTEXT - Large text content (with utf8mb4)
✓ JSON - Structured data (when appropriate)
✗ CHAR(n) - Automatically converted to VARCHAR(n)
✗ TEXT, MEDIUMTEXT - Automatically converted to LONGTEXT
### Temporal Types
✓ DATE - Date only (YYYY-MM-DD)
✓ DATETIME(3) - Date and time with millisecond precision
✓ TIMESTAMP(3) - Unix timestamp with millisecond precision
✗ YEAR - FORBIDDEN (use INT or DATE)
✗ TIME - FORBIDDEN (use DATETIME)
### Binary Types
✓ BLOB, LONGBLOB - Binary data (images, files)
### FORBIDDEN Types (Throw Exception)
✗ ENUM - Use VARCHAR with validation instead
✗ SET - Use JSON or separate table
✗ YEAR - Use INT or DATE
✗ TIME - Use DATETIME
RATIONALE: ENUM and SET cannot be modified without table locks, causing deployment
issues in production. YEAR and TIME types are too limited and cause conversion issues.
CHARACTER SET ENFORCEMENT
-------------------------
All string columns (VARCHAR, TEXT) are automatically set to:
• CHARACTER SET: utf8mb4
• COLLATION: utf8mb4_unicode_ci
This ensures full Unicode support including emojis and prevents character set
mismatches in foreign keys.
MIGRATION FLOW
--------------
When you run `php artisan migrate`:
1. **Pre-Migration Normalization**
- Fixes existing tables created before transformer implementation
- Converts tables to utf8mb4 character set
- Ensures consistent schema before new migrations
2. **SqlQueryTransformer Enabled**
- Intercepts all DB::statement() calls
- Rewrites CREATE TABLE and ALTER TABLE queries
- Transforms types (INT→BIGINT, FLOAT→DOUBLE, etc.)
- Adds CHARACTER SET utf8mb4 to all string columns
- Validates forbidden types (throws exception for ENUM, SET, etc.)
3. **Run Migrations**
- Migrations execute with transformed SQL
- Tables created with correct types from the start
- Foreign keys work immediately (no type mismatches)
4. **Post-Migration Normalization**
- Adds framework-required columns to new tables:
• created_at, updated_at (with millisecond precision)
• created_by, updated_by (audit columns)
• deleted_by (for soft deletes)
• Trait-specific columns (site_id, version, etc.)
- Creates indexes on timestamp columns
- Upgrades DATETIME/TIMESTAMP precision to milliseconds
- Syncs Query/Join attribute indexes (see AUTOMATIC INDEX MANAGEMENT below)
5. **Regenerate Constants** (Dev Mode Only)
- Updates model constants from database schema
- Exports enum values to JavaScript
- Generates IDE type hints
6. **SqlQueryTransformer Disabled**
- Transformer only active during migration runs
- Normal queries unaffected
TRANSFORMATION EXAMPLES
-----------------------
### Example 1: Integer Type Transformation
User writes:
```sql
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
views INT UNSIGNED,
is_published TINYINT(1),
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
Transformer rewrites to:
```sql
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
views BIGINT,
is_published TINYINT(1),
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
Result: Foreign key works immediately (both columns BIGINT).
### Example 2: String Type Transformation
User writes:
```sql
CREATE TABLE users (
name VARCHAR(255),
bio TEXT,
code CHAR(10)
);
```
Transformer rewrites to:
```sql
CREATE TABLE users (
name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
bio LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
code VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);
```
Result: Consistent character sets, no FK issues on string columns.
### Example 3: Forbidden Type Rejection
User writes:
```sql
CREATE TABLE products (
status ENUM('draft', 'published', 'archived')
);
```
Result: Migration fails with exception:
```
ENUM column type is forbidden in RSpade. Use VARCHAR with validation instead.
ENUMs cannot be modified without table locks and cause deployment issues.
```
Developer fixes by using VARCHAR:
```sql
CREATE TABLE products (
status VARCHAR(20) -- Add validation in model
);
```
FRAMEWORK-REQUIRED COLUMNS
--------------------------
normalize_schema ensures these columns exist on ALL tables (except excluded tables):
### Audit Columns
• created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)
• updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
• created_by BIGINT NULL
• updated_by BIGINT NULL
• deleted_by BIGINT NULL (if deleted_at exists)
### Indexes
• INDEX created_at(created_at)
• INDEX updated_at(updated_at)
### Trait-Specific Columns
• site_id BIGINT (for Siteable trait)
• version BIGINT (for Versionable/Ajaxable traits)
Excluded Tables: migrations, api_clients, sessions
AUTOMATIC INDEX MANAGEMENT
---------------------------
RSX automatically creates and maintains database indexes based on #[Query] and
#[Join] attributes declared in model methods. This eliminates manual index
management and ensures all queries have appropriate indexes.
### How It Works
During database normalization (and manifest rebuild in dev mode), the framework:
1. **Scans Models** - Finds all methods with #[Query] or #[Join] attributes
2. **Collects Required Indexes** - Extracts index column lists from attributes
3. **Detects Covering Indexes** - Checks if manual indexes already cover requirements
4. **Removes Redundancy** - Filters overlapping index requirements
5. **Creates Missing Indexes** - Adds only indexes not covered by existing ones
6. **Removes Obsolete Indexes** - Drops auto-generated indexes no longer needed
### Index Naming Convention
Auto-generated indexes are prefixed with `__rsx_query_autoindex_` followed by
a sequential number. These indexes are managed automatically by the framework.
Manual indexes (without this prefix) are never modified. If a manual index
covers the columns required by a Query attribute, no auto-index is created.
### Covering Index Detection
The framework understands that index (a,b,c) can efficiently support queries on:
- (a) alone
- (a,b) together
- (a,b,c) together
But NOT efficiently on:
- (b) alone
- (c) alone
- (b,c) together
If you have #[Query(index: ['user_id', 'status'])] and a manual index already
exists on (user_id, status, created_at), the framework recognizes the manual
index covers your requirement and does not create an auto-index.
### Redundant Index Removal
If multiple Query attributes would create overlapping indexes, the framework
keeps only the more specific ones:
- #[Query(index: ['user_id'])]
- #[Query(index: ['user_id', 'status'])]
Result: Only create index on (user_id, status) because it covers both queries.
### Query Attribute Syntax
Declare required indexes directly in model methods:
```php
class User_Model extends Rsx_Model_Abstract
{
// Simple single-column index
#[Query(index: ['status'])]
public static function find_by_status(int $status)
{
return static::where('status', $status)->get();
}
// Composite index (order matters!)
#[Query(index: ['category_id', 'status'])]
public static function find_by_category(int $category_id)
{
return static::where('category_id', $category_id)
->where('status', static::STATUS_ACTIVE)
->get();
}
}
```
### Join Attribute Syntax
Joins specify the target table and index to create on that table:
```php
class User_Model extends Rsx_Model_Abstract
{
#[Join(
target_table: 'user_logins',
index: ['user_id', 'logged_in_at']
)]
public static function join_user_logins_recent($query, array $conditions = [])
{
return $query->leftJoin('user_logins', function($join) {
$join->on('user_logins.user_id', '=', 'users.id')
->where('user_logins.logged_in_at', '>',
DB::raw('NOW() - INTERVAL 30 DAY'));
});
}
}
```
The index is created on the TARGET table (user_logins), not the source table.
### When Indexes Are Created
**Development Mode**: Indexes sync automatically during manifest rebuild
```bash
php artisan rsx:manifest:build # Auto-syncs indexes
```
**Production Mode**: Indexes sync during database normalization
```bash
php artisan migrate:normalize_schema
```
### Viewing Required Indexes
```bash
php artisan rsx:db:check_indexes
```
Shows all Query and Join attributes with their required indexes and whether
those indexes exist in the database.
For complete documentation of the Query/Join system, see:
```bash
php artisan rsx:man database_queries
```
BENEFITS OF THIS SYSTEM
------------------------
1. **No Foreign Key Mismatches**
- Types match from the start (both BIGINT)
- No need to drop/recreate foreign keys
- Migrations run faster in production
2. **Write Natural SQL**
- Developer writes INT, framework uses BIGINT
- Developer writes TEXT, framework uses LONGTEXT
- No need to remember framework conventions
3. **Consistent Schema**
- All environments have identical schema
- No drift between dev and production
- Database migrations are deterministic
4. **Fast Production Migrations**
- No column type modifications (no row rewrites)
- No foreign key drop/recreate (metadata only)
- Tables created correctly on first try
5. **Fail Fast on Bad Types**
- ENUM throws exception immediately
- Developer gets clear error message
- No silent failures or runtime issues
COMMANDS
--------
### php artisan migrate
Main migration command. Runs the full cycle:
pre-normalize → migrations → post-normalize → regenerate constants (dev)
### php artisan migrate:begin
Creates MySQL snapshot before migrations (dev mode only).
Required for safety in development.
### php artisan migrate:commit
Deletes snapshot and exits migration mode.
Runs schema quality checks before committing.
### php artisan migrate:rollback
Restores database to snapshot state.
Stays in migration mode for retry.
### php artisan migrate:normalize_schema
Manually run normalization (rarely needed).
Add --production flag to skip snapshot requirement.
### php artisan migrate:check
Check database schema for standards compliance.
Reports violations without making changes.
SNAPSHOT-BASED DEVELOPMENT
--------------------------
Development migrations require snapshots for safety:
1. `php artisan migrate:begin` - Create snapshot
2. `php artisan migrate` - Run migrations
3. `php artisan migrate:commit` - Keep changes, delete snapshot
OR
`php artisan migrate:rollback` - Discard changes, restore snapshot
Production migrations skip snapshots and run directly.
WHY NO FOREIGN KEY DROP/RECREATE?
---------------------------------
Previous system:
1. User creates table with INT columns
2. normalize_schema converts INT→BIGINT (slow, locks table)
3. Must drop ALL FKs first, recreate after (complex, error-prone)
New system:
1. Transformer rewrites INT→BIGINT before execution
2. Table created with BIGINT from the start
3. normalize_schema finds nothing to change (fast)
4. No FK drop/recreate needed
The only remaining use case for column modifications is converting pre-existing
tables (created before transformer) to utf8mb4 character set. This is rare and
only affects legacy tables.
TROUBLESHOOTING
---------------
### Migration fails with "ENUM column type is forbidden"
Solution: Replace ENUM with VARCHAR and add validation in your model.
### Migration fails with "SET column type is forbidden"
Solution: Use JSON column or create a separate table.
### Foreign key fails with type mismatch
Check: Are you running migrations outside the framework? Use `php artisan migrate`.
The transformer only runs during framework migration commands.
### Table has INT instead of BIGINT
This means the table was created before the transformer was implemented or
created outside the migration system. Run `php artisan migrate:normalize_schema`
to fix it, or drop and recreate the table.
### Character set mismatch on foreign keys
Run `php artisan migrate:normalize_schema` to convert tables to utf8mb4.
ADVANCED: HOW THE TRANSFORMER WORKS
------------------------------------
SqlQueryTransformer uses regex-based pattern matching on SQL statements:
1. Whitespace Normalization
- Collapses whitespace while preserving quoted strings
- Handles single quotes, double quotes, and backticks
2. DDL Detection
- Only transforms CREATE TABLE and ALTER TABLE
- Other queries (SELECT, INSERT, etc.) pass through unchanged
3. Type Validation
- Checks for forbidden types first (ENUM, SET, YEAR, TIME)
- Throws exception immediately if found
4. Type Transformation (in order)
- Integer types: INT/INTEGER/MEDIUMINT/SMALLINT → BIGINT
- Float types: FLOAT/REAL → DOUBLE
- Text types: TEXT/MEDIUMTEXT/CHAR → LONGTEXT/VARCHAR
- Charset enforcement: Add CHARACTER SET utf8mb4 to string columns
5. Query Execution
- Transformed query executes via DB::statement()
- Database never sees the original types
The transformer is enabled only during `php artisan migrate` and disabled after
completion. Normal application queries are not affected.
SEE ALSO
--------
- php artisan rsx:man database_queries - Query/Join attribute system
- php artisan rsx:man migrations - Migration guidelines
- php artisan rsx:man rsx_architecture - Framework architecture
- php artisan rsx:man coding_standards - Code standards
- /app/RSpade/Core/Database/CLAUDE.md - Database system docs
AUTHOR
------
RSpade Framework - October 2025

View File

@@ -0,0 +1,306 @@
NAME
prod_feature_plans - Production deployment feature planning for RSpade framework
SYNOPSIS
Planned features and optimizations for production deployment mode
DESCRIPTION
This document tracks planned features for RSpade framework production deployment.
These features will be implemented when the production build/export system is
fully developed.
Production mode focuses on:
- Security hardening
- Performance optimization
- Minimal footprint
- Production-ready asset delivery
- Error handling for end users
Current Status:
Production mode is partially implemented. This document tracks additional
features to be added when the full "export prod build" system is created.
PLANNED FEATURES
Minified Source Code:
- Minify all JavaScript bundles
- Minify CSS files
- Remove comments and whitespace
- Obfuscate variable names (optional)
- Reduce bundle size by 60-80%
Implementation Notes:
- Use webpack/esbuild minification
- Preserve source maps for error tracking
- Keep class/function names for reflection
- Test bundle integrity after minification
Public Directory Asset Migration:
- Copy all rsx/public/* to Laravel public/ directory
- Serve static files directly from public/
- Bypass PHP entirely for static assets
- Maintain public_ignore.json restrictions
- Set proper cache headers (30 days with ?v= versioning)
Implementation Notes:
- Run during deployment/build process
- Update asset URLs in compiled bundles
- Verify no conflicts with existing public/ files
- Document migration process for custom deployments
IDE Helper Routes Disabled:
- Disable all /_idehelper/* routes in production
- Return 404 for any IDE helper endpoint access
- Remove route registrations entirely
- Prevent information disclosure
Implementation Notes:
- Already partially implemented (endpoints check environment)
- Need to remove route registration in production
- Ensure VS Code extension gracefully handles disabled endpoints
- Document production vs development differences
Fatal Error Logging (No User Display):
- Log all fatal errors to storage/logs/
- Show generic error page to users
- Never expose stack traces or file paths
- Include error ID for support reference
Implementation Notes:
- Implement custom error handler
- Create user-friendly error template
- Log detailed errors with context
- Provide error ID for support lookups
- Consider integration with error tracking services (Sentry, Bugsnag)
IIFE JavaScript Namespace Protection:
- Wrap all bundle JavaScript in IIFE (Immediately Invoked Function Expression)
- Prevent global namespace pollution
- Make internal variables inaccessible from browser console
- Protect against console-based tampering
Example:
(function() {
'use strict';
// All bundle code here
// Variables not accessible via window.variable_name
})();
Implementation Notes:
- Configure webpack/esbuild to use IIFE format
- Test framework functionality with wrapped code
- Ensure exposed APIs still accessible (Rsx.Route, etc.)
- Balance security with debugging needs
Additional Security Measures:
- Disable debug mode completely
- Remove all console_debug() output
- Disable manifest rebuilding
- Set secure cookie flags
- Enable HTTPS-only mode
- Configure CSP headers
Implementation Notes:
- config/rsx.php production overrides
- Middleware for security headers
- Document required environment variables
- Create production checklist
Performance Optimizations:
- Enable OPcache in production
- Precompile all manifest data
- Cache bundle assets with long expiration
- Enable gzip/brotli compression
- Optimize database queries
- Implement Redis caching
Implementation Notes:
- Server configuration documentation
- Automatic OPcache warming on deploy
- Cache warming artisan command
- Performance testing before/after
Asset Versioning:
- Automatic ?v=hash query strings
- Cache busting on every deployment
- Manifest-based asset URL generation
- Long-term browser caching (30 days)
Implementation Notes:
- Generate asset hashes during build
- Store hash in manifest
- Helper function for versioned URLs
- Document cache invalidation strategy
DEPLOYMENT WORKFLOW
Planned deployment process for production builds:
1. Build Phase:
php artisan rsx:build:prod
- Compile all bundles with minification
- Generate asset hashes
- Copy public/ directory assets
- Precompile manifest data
- Run code quality checks
- Create deployment package
2. Package Phase:
- Include only necessary files
- Exclude development dependencies
- Exclude test directories
- Exclude IDE helper system
- Create tarball or zip
3. Deploy Phase:
- Upload package to production server
- Extract to web root
- Run migrations (forward-only)
- Warm caches (OPcache, manifest, routes)
- Restart PHP-FPM
- Verify deployment
4. Verification Phase:
- Test critical user paths
- Verify asset loading
- Check error logging
- Monitor performance
- Rollback if issues detected
CONFIGURATION CHANGES
Production-specific config/rsx.php overrides:
'production' => [
'minify_assets' => true,
'disable_ide_helper' => true,
'show_error_details' => false,
'enable_console_debug' => false,
'rebuild_manifest' => false,
'use_iife_bundles' => true,
'asset_versioning' => true,
'long_term_caching' => true,
]
SECURITY CONSIDERATIONS
Information Disclosure Prevention:
- Never expose file paths in errors
- Remove Laravel version headers
- Disable directory listings
- Hide framework version information
- Generic error pages only
Access Control:
- Disable gatekeeper (use real authentication)
- Enforce HTTPS only
- Set secure session cookies
- Implement rate limiting
- Add CSRF protection
Code Protection:
- Minify and obfuscate JavaScript
- Remove source maps (or serve separately)
- Disable debug endpoints
- Remove development tools
PERFORMANCE TARGETS
Target metrics for production builds:
Bundle Size:
- JavaScript: < 200KB per bundle (minified + gzipped)
- CSS: < 50KB per bundle (minified + gzipped)
- Total page weight: < 500KB initial load
Load Times:
- First Contentful Paint: < 1.5s
- Time to Interactive: < 3s
- Largest Contentful Paint: < 2.5s
Server Performance:
- Response time: < 200ms (p95)
- Requests per second: > 100 (single server)
- Memory usage: < 128MB per PHP worker
MONITORING AND LOGGING
Production monitoring requirements:
Error Tracking:
- Log all errors to persistent storage
- Include request context and stack trace
- Generate unique error IDs
- Alert on error rate thresholds
- Integration with monitoring services
Performance Monitoring:
- Track response times
- Monitor database query performance
- Track bundle load times
- Monitor server resource usage
User Analytics:
- Page view tracking
- Error rate by page
- User flow analysis
- Performance metrics by region
BACKWARD COMPATIBILITY
Ensure production build maintains:
- All public API compatibility
- Database migration forward-compatibility
- Configuration file structure
- Bundle loading mechanisms
- Route definitions and URLs
Breaking changes require:
- Major version bump
- Migration documentation
- Deprecation warnings in previous version
- Automated migration tools where possible
TESTING REQUIREMENTS
Before production deployment:
- All unit tests passing
- Integration tests passing
- End-to-end tests on production build
- Performance benchmarks met
- Security audit completed
- Load testing completed
Automated checks:
- Code quality (rsx:check)
- Bundle integrity
- Asset versioning
- Route accessibility
- Database migrations
ROLLBACK STRATEGY
Production deployment must support:
- One-command rollback to previous version
- Database migration rollback (where possible)
- Asset cache invalidation
- Zero-downtime deployment
- Health check verification
FUTURE CONSIDERATIONS
Long-term production features:
- Multi-server deployment support
- CDN integration for static assets
- Database read replicas
- Redis cluster support
- Horizontal scaling capabilities
- Blue-green deployment support
- Canary deployments
- Automated rollback on errors
SEE ALSO
config_rsx - Framework configuration system
storage_directories - Storage organization and deployment
manifest_build - Manifest compilation and caching
bundle_api - Bundle system and asset compilation

272
app/RSpade/man/routing.txt Executable file
View File

@@ -0,0 +1,272 @@
NAME
routing - RSX type-safe URL generation and route patterns
SYNOPSIS
Type-safe URL generation in PHP and JavaScript with automatic route discovery
DESCRIPTION
The RSX framework provides type-safe URL generation that ensures routes
are valid at build time and prevents broken links. Unlike Laravel's
route() helper which uses string names, RSX uses controller class names
and actions, providing IDE autocompletion and refactoring safety.
Key differences from Laravel:
- Laravel: route('user.profile', $user) using named routes
- RSX: Rsx::Route('User_Controller', 'profile')->url(['id' => $user->id])
Benefits:
- No route name management required
- Automatic route discovery from controller attributes
- Same syntax works in both PHP and JavaScript
- IDE autocompletion for controller names
- Refactoring-safe (renaming controllers updates routes)
BASIC USAGE
PHP Syntax:
use App\RSpade\Core\Rsx;
// Create route proxy (action defaults to 'index')
$route = Rsx::Route('Demo_Index_Controller');
$route = Rsx::Route('Demo_Index_Controller', 'show');
// Generate URLs
$url = $route->url(); // /demo
$url = $route->url(['id' => 123]); // /demo/123 or /demo?id=123
$absolute = $route->absolute_url(); // https://site.com/demo
// Navigate (redirect)
$route->navigate(); // Sends Location header and exits
JavaScript Syntax:
// Create route proxy (action defaults to 'index')
const route = Rsx.Route('Demo_Index_Controller');
const route = Rsx.Route('Demo_Index_Controller', 'show');
// Generate URLs
const url = route.url(); // /demo
const url = route.url({id: 123}); // /demo/123 or /demo?id=123
const absolute = route.absolute_url(); // https://site.com/demo
// Navigate
route.navigate(); // Sets window.location.href
ROUTE PATTERNS
Route Definition:
Routes are defined using #[Route] attributes on controller methods:
#[Auth('Permission::authenticated()')]
#[Route('/users/:division/:id')]
public static function show_user(Request $request, array $params = [])
{
// $params['division'] and $params['id'] contain URL values
$division = $params['division'];
$user_id = $params['id'];
}
Parameter Syntax:
- :param - Required parameter in URL path
- Routes without parameters use controller/action mapping
- URL generation validates required parameters are provided
URL Parameter Handling:
- Required parameters (in pattern) must be provided or exception thrown
- Extra parameters become query string (?key=value)
- All values automatically URL encoded
- Supports complex parameter patterns
ROUTE ACCESS CONTROL
All routes MUST have #[Auth] attribute for access control:
#[Auth('Permission::anybody()')]
#[Route('/')]
public static function index(Request $request, array $params = [])
{
return rsx_view('Landing');
}
Access Control Placement:
1. On route method - Applies to specific route
2. On pre_dispatch() - Applies to all routes in controller
3. Both - pre_dispatch checked first, then route
Common Patterns:
// Public route
#[Auth('Permission::anybody()')]
// Authenticated only
#[Auth('Permission::authenticated()',
message: 'Please log in',
redirect: '/login')]
// Custom permission with argument
#[Auth('Permission::has_role("admin")')]
// Multiple requirements (all must pass)
#[Auth('Permission::authenticated()')]
#[Auth('Permission::has_permission("edit")')]
See controller(3) for complete #[Auth] documentation.
ROUTE DISCOVERY
Automatic Discovery:
The manifest system automatically discovers routes by scanning
for #[Route] attributes on controller static methods.
Discovery Process:
1. Manifest scanner finds Route attributes during build
2. Routes extracted and cached in manifest
3. Bundle compilation injects routes into JavaScript
4. Both PHP and JS use same routing logic
Controller Mapping:
- Demo_Index_Controller maps to /demo route by default
- User_Profile_Controller maps to /user/profile
- Action methods create sub-routes: show -> /demo/show
BUNDLE INTEGRATION
Automatic Route Inclusion:
Routes are automatically included in JavaScript bundles when
controllers are in the bundle's include paths.
Bundle Configuration:
class My_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => ['rsx/app/myapp'],
'include_routes' => ['rsx/app/admin'], // Additional route scanning
];
}
}
Route Availability:
- Routes available in JavaScript when bundle renders
- Same URL generation logic in both PHP and JavaScript
- No manual route registration required
ADVANCED PATTERNS
Complex Parameter Examples:
// Multiple parameters
#[Route('/api/v1/users/:company/:division/:id')]
$route->url(['company' => 'acme', 'division' => 'sales', 'id' => 123]);
// Result: /api/v1/users/acme/sales/123
// Query parameters for extra values
$route->url(['id' => 123, 'format' => 'json', 'include' => 'profile']);
// Result: /demo/123?format=json&include=profile
// Complex objects as parameters
$route->url(['filter' => ['status' => 'active', 'type' => 'user']]);
// Result: /demo?filter[status]=active&filter[type]=user
Route Groups and Prefixes:
Routes inherit prefixes from controller structure:
- Backend_User_Controller -> /backend/user
- Api_V1_User_Controller -> /api/v1/user
- Admin_Reports_Controller -> /admin/reports
Custom Route Patterns:
#[Route('/custom/path/:id')] // Custom path
#[Route('/api/:version/users/:id')] // Multiple segments
#[Route('/files/:path*')] // Wildcard parameters
JAVASCRIPT BUNDLE ROUTES
Route Availability in JavaScript:
Routes are injected into JavaScript bundles as part of the Rsx
framework initialization. No manual configuration required.
Bundle Route Extraction:
// Routes automatically extracted from included controllers
class Frontend_Bundle extends Rsx_Bundle_Abstract
{
public static function define(): array
{
return [
'include' => [
'rsx/app/frontend', // Controllers here get routes extracted
'rsx/app/shared', // Additional controller directories
],
];
}
}
Runtime Route Access:
// Routes available after bundle loads
if (typeof Rsx !== 'undefined') {
const route = Rsx.Route('Demo_Index_Controller');
const url = route.url();
}
ERROR HANDLING
Missing Route Errors:
- PHP: RouteNotFoundException when controller/action not found
- JavaScript: Error thrown when route not available in bundle
- Development: Clear error messages with suggestions
Parameter Validation:
- Required parameters must be provided
- Invalid parameter types rejected
- URL encoding applied automatically
Bundle Configuration Errors:
- Routes not available if controllers not in bundle includes
- Clear error messages when routes missing from JavaScript
DEBUGGING ROUTES
View All Routes:
php artisan rsx:routes # List all discovered routes
Test Route Generation:
php artisan rsx:debug /demo --eval="Rsx.Route('Demo_Index_Controller').url()"
Route Information:
php artisan rsx:manifest:show # View route cache in manifest
COMMON PATTERNS
Controller Navigation:
// Redirect to another controller
public static function handle_form(Request $request, array $params = [])
{
// Process form...
Rsx::Route('Dashboard_Index_Controller')->navigate();
}
AJAX URL Generation:
// Generate URLs for AJAX calls
const apiUrl = Rsx.Route('Api_User_Controller', 'update').url({id: userId});
fetch(apiUrl, {method: 'POST', body: formData});
Form Action URLs:
// Generate form action URLs
<form action="<?= Rsx::Route('User_Profile_Controller', 'update')->url() ?>" method="POST">
Link Generation:
// Generate navigation links
<a href="<?= Rsx::Route('Dashboard_Index_Controller')->url() ?>">Dashboard</a>
TROUBLESHOOTING
Route Not Found:
- Verify controller exists and has #[Route] attribute
- Check controller is in manifest-scanned directories
- Ensure manifest rebuilt after adding new routes
JavaScript Route Unavailable:
- Verify controller included in bundle configuration
- Check bundle compiles without errors
- Ensure bundle renders in HTML output
Parameter Errors:
- Required parameters must match route pattern exactly
- Parameter names are case-sensitive
- Use array syntax for multiple parameters
URL Generation Issues:
- Check for typos in controller names
- Verify action method exists on controller
- Ensure route pattern matches expected parameters
SEE ALSO
controller - Controller patterns and route attributes
bundle_api - Bundle system and JavaScript integration
manifest_api - Route discovery and caching system

View File

@@ -0,0 +1,282 @@
NAME
rsx_architecture - RSX application structure and organization
SYNOPSIS
RSX application structure, module organization, and development patterns
DESCRIPTION
RSX applications follow a standardized module-based architecture that
separates framework code from application code. Understanding this
structure is essential for organizing files, creating modules, and
understanding the development workflow.
RSX DIRECTORY STRUCTURE
The /rsx/ directory contains all application code organized by modules:
/rsx/
├── app/ # Application modules
│ ├── frontend/ # Public website (route: /)
│ │ ├── frontend_controller.php
│ │ ├── frontend_bundle.php
│ │ ├── frontend_view.blade.php
│ │ ├── frontend.scss
│ │ ├── frontend.js
│ │ └── layout.blade.php
│ ├── backend/ # Admin panel (route: /admin)
│ │ └── (same structure as frontend)
│ ├── dashboard/ # User dashboard (route: /dashboard)
│ │ └── (same structure as frontend)
│ ├── root/ # Super admin (route: /root)
│ │ └── (same structure as frontend)
│ ├── login/ # Authentication (routes: /login, /logout)
│ │ └── (same structure as frontend)
│ ├── api/ # Public API endpoints
│ │ └── CLAUDE.md # API documentation
│ └── demo/ # Demo/example code
├── lib/ # Shared libraries and utilities
├── models/ # Database models
├── theme/ # Global theme assets
│ ├── variables.scss # Global SCSS variables
│ ├── components/ # Reusable UI components
│ └── layouts/ # Shared layouts (if needed)
└── main.php # Application-wide middleware hooks
MODULE STRUCTURE CONVENTION
Each module is self-contained with its own files:
File Naming:
- Always lowercase: frontend_controller.php
- Underscore separation: user_profile_view.blade.php
Class Naming:
- Noun_Type format: Frontend_Controller, Backend_Bundle
- Matches file name pattern
View Identifiers:
- Noun_Type format: @rsx_id('Frontend_View')
- Used for CSS scoping and identification
Bundle Rendering:
- Required format: {!! Module_Bundle::render() !!}
- Example: {!! Frontend_Bundle::render() !!}
API ORGANIZATION
Two types of API endpoints:
Public APIs (/rsx/app/api/):
- External-facing endpoints
- Include versioning
- Public documentation
Internal APIs:
- Place in module directories: /rsx/app/frontend/api/
- Or in shared location: /rsx/lib/
- Module-specific functionality
MODULE HIERARCHY
RSX uses a four-level hierarchy for organizing complex applications:
Level 1 - Module:
- Top-level application section
- Route: /module
- Has own layout and styling
- Example: frontend, backend, dashboard
Level 2 - Submodule:
- Sub-section within module with embedded layout
- Route: /module/submodule
- Extends parent module layout
- Example: /backend/users, /dashboard/reports
Level 3 - Feature:
- Specific functionality page
- Route: /module/feature or /module/submodule/feature
- Individual page within module/submodule
- Example: /backend/create, /dashboard/reports/monthly
Level 4 - Subfeature:
- Sub-page within feature
- Route: /module/feature/subfeature or /module/submodule/feature/subfeature
- Detailed or step-based pages
- Example: /backend/create/step2, /dashboard/reports/monthly/export
MODULE FEATURES
Each level creates specific file structures:
Index Feature (default):
- Route: /module
- Files in module root: module_index_controller.php
- Default landing page for module
Additional Features:
- Route: /module/feature
- Files in subdirectory: module/feature/module_feature_controller.php
- Each feature is self-contained
Feature Files:
- Controller: handles routing and logic
- View: Blade template for HTML
- JS Class: client-side behavior
- SCSS: styling specific to feature
SUBMODULE ORGANIZATION
Submodules provide hierarchical organization:
Layout Structure:
- Submodules have embedded layouts extending parent layouts
- Allows consistent styling within module family
- Parent module controls overall structure
File Placement:
- Submodule files in: /rsx/app/module/submodule/
- Features within submodule: /rsx/app/module/submodule/feature/
- Maintains clear separation of concerns
MODULE CREATION COMMANDS
Generate modules with standardized structure using hierarchy commands:
Level 1 - Module Commands:
php artisan rsx:app:module:create signup
Creates: /rsx/app/signup/ with index feature
Route: /signup
php artisan rsx:app:module:feature:create signup verify_email
Creates: /rsx/app/signup/verify_email/
Route: /signup/verify_email
Level 2 - Submodule Commands:
php artisan rsx:app:submodule:create backend users
Creates: /rsx/app/backend/users/ with embedded layout
Route: /backend/users
php artisan rsx:app:submodule:feature:create backend users create
Creates: /rsx/app/backend/users/create/
Route: /backend/users/create
Level 3 - Subfeature Commands:
php artisan rsx:app:subfeature:create signup verify_email confirm
Creates: /rsx/app/signup/verify_email/confirm/
Route: /signup/verify_email/confirm
php artisan rsx:app:submodule:subfeature:create backend users create step2
Creates: /rsx/app/backend/users/create/step2/
Route: /backend/users/create/step2
Component Commands:
php artisan rsx:app:component:create --name=my_widget --path=rsx/theme/components
Creates reusable jqhtml component in theme directory
php artisan rsx:app:component:create --name=user_card --module=dashboard
Creates component within specific module
php artisan rsx:app:component:create --name=form_field --module=backend --feature=users
Creates component within module feature
COMMAND NAMING RULES
All names must be lowercase with underscores:
- Correct: user_profile, email_verify, admin_panel
- Incorrect: UserProfile, emailVerify, admin-panel
Examples of full hierarchy:
Module: ecommerce
Submodule: ecommerce/products
Feature: ecommerce/products/create
Subfeature: ecommerce/products/create/images
JAVASCRIPT INTEGRATION
JavaScript classes auto-initialize:
- Extend appropriate base classes
- Use static on_app_ready() method for initialization
- Use static on_jqhtml_ready() to wait for JQHTML components to load
- No manual registration required
Lifecycle timing:
- on_app_ready(): Runs when page initializes, before JQHTML components finish
- on_jqhtml_ready(): Runs after all JQHTML components loaded and rendered
Important limitation:
JavaScript only executes when bundle is rendered in HTML output.
Won't work with:
- JSON API responses
- Raw HTML responses
- Views without bundle includes
SHARED RESOURCES
Theme Directory (/rsx/theme/):
- variables.scss: Global SCSS variables and mixins
- components/: Reusable jqhtml components
- layouts/: Shared layout templates (if needed)
Library Directory (/rsx/lib/):
- Shared utility classes
- Common business logic
- Internal API helpers
Models Directory (/rsx/models/):
- Database model classes
- Data validation logic
- Model relationships
MIDDLEWARE INTEGRATION
Application-wide hooks via /rsx/main.php:
- Extends Main_Abstract
- Provides init(), pre_dispatch(), unhandled_route() methods
- Handles cross-cutting concerns
DEVELOPMENT WORKFLOW
1. Plan module hierarchy (module > submodule > feature > subfeature)
2. Create structure with appropriate artisan commands
3. Implement controllers with route attributes
4. Create view templates with bundle includes
5. Add JavaScript classes for interactivity
6. Style with SCSS in theme or module files
7. Test with rsx:debug command
HIERARCHY PLANNING
Choose appropriate level based on complexity:
Simple Applications:
- Use modules and features only
- Example: /login, /dashboard, /profile
Complex Applications:
- Use full hierarchy for organization
- Example: /admin/users/create/permissions
Planning Questions:
- Does this need its own layout? (Use submodule)
- Is this a multi-step process? (Use subfeatures)
- Will users bookmark this page? (Consider hierarchy depth)
BEST PRACTICES
Hierarchy Organization:
- Start simple: use modules and features first
- Add submodules when you need distinct layouts
- Use subfeatures for multi-step processes
- Avoid deep nesting (4 levels maximum)
Module Organization:
- Keep modules focused on single responsibility
- Use shared /lib/ for cross-module functionality
- Place components in /theme/ if truly reusable
File Organization:
- Follow naming conventions strictly (lowercase, underscores)
- Keep related files together in module directories
- Use features for complex modules with multiple pages
- Group related submodules under logical modules
API Design:
- Public APIs in /api/ with versioning
- Internal APIs close to consuming code
- Document API endpoints in module CLAUDE.md files
Layout Strategy:
- Module layouts for major sections
- Submodule embedded layouts for consistent styling
- Share common elements through parent layouts
SEE ALSO
bundle_api - Bundle system documentation
controller - Controller patterns and routing
manifest_api - Manifest system reference

229
app/RSpade/man/rsx_upstream.txt Executable file
View File

@@ -0,0 +1,229 @@
# RSpade Framework Updates
## NAME
rsx_upstream - Managing RSpade framework updates from upstream
## SYNOPSIS
Updating the RSpade framework while preserving your application code:
php artisan rsx:framework:pull
## DESCRIPTION
The RSpade framework is distributed as a git repository that receives regular
updates with bug fixes, new features, and improvements. The framework update
system allows you to pull these updates while preserving your application code
in the ./rsx directory.
## COMMAND
### php artisan rsx:framework:pull
Pulls RSpade framework updates from upstream repository.
**What it does:**
- Fetches latest changes from `rspade_upstream` remote
- Merges framework updates into your local repository
- Preserves `./rsx` directory completely unchanged
- Cleans and rebuilds the manifest
- Recompiles all bundles
**Safety features:**
- Shows what files will be updated before proceeding
- Requires confirmation before applying updates
- Fails loud on merge conflicts
- Never modifies ./rsx directory
**No git cleanliness checks:**
- ./rsx is never tracked by framework repository
- Framework files are read-only in application development mode
- You may WANT to update broken framework files
- Update will fail loud on real conflicts
**Usage:**
php artisan rsx:framework:pull
**Time required:**
Framework updates take 2-5 minutes to complete, including:
- Pulling latest framework code from upstream
- Cleaning and rebuilding the manifest
- Recompiling all bundles
- Updating dependencies
## GIT MERGE PROTECTION
The framework is configured to always preserve your ./rsx directory during
updates, even if upstream has historical commits containing ./rsx files.
**How it works:**
1. **`.gitignore` excludes ./rsx** - Framework doesn't track ./rsx changes
2. **`.gitattributes` merge strategy** - `/rsx/** merge=ours` ensures your
version is always kept during merges
3. **Defense-in-depth** - `rsx:framework:pull` explicitly preserves ./rsx
**Why this matters:**
The upstream framework repository may have historical commits with ./rsx files
(demo applications, examples, etc.). Without merge protection, git merge could
attempt to merge those changes into your application code. The multi-layer
protection ensures your ./rsx directory is never touched by framework updates.
## REMOTE CONFIGURATION
Framework updates require a git remote named `rspade_upstream` pointing to the
RSpade framework repository.
**Check current remotes:**
git remote -v
**Expected output:**
rspade_upstream <upstream-url> (fetch)
rspade_upstream <upstream-url> (push)
**Add upstream remote if missing:**
git remote add rspade_upstream <upstream-url>
## TYPICAL WORKFLOW
### Regular Framework Updates
# Check for updates
git fetch rspade_upstream
git log HEAD..rspade_upstream/master
# Pull framework updates
php artisan rsx:framework:pull
# Test your application
php artisan rsx:check
# Run your tests
# Commit if framework changes were applied
git add -A
git commit -m "Update RSpade framework to latest version"
### Handling Merge Conflicts
If framework updates conflict with local changes:
# During rsx:framework:pull
ERROR: Merge conflict detected
# View conflicted files
git status
# Resolve conflicts manually
nano path/to/conflicted/file.php
# Mark as resolved
git add path/to/conflicted/file.php
# Complete the merge
git commit -m "Merge framework updates, resolved conflicts"
# Rebuild framework
php artisan rsx:clean
php artisan rsx:manifest:build
## FORKING THE FRAMEWORK
The framework update system assumes the RSpade framework code remains managed
by upstream and updated automatically. This is recommended for most developers.
If you need to modify framework internals or customize the Laravel foundation,
you can take full ownership of the entire codebase:
php artisan rsx:man framework_fork
This documents:
- When and why to fork the framework
- How to maintain a forked framework
- Procedures for manually merging upstream updates
- Trade-offs between managed updates and full control
**Consider forking only if:**
- You need to modify RSpade core functionality
- You want to customize Laravel foundation
- You're building deep framework integrations
- You're willing to manually maintain framework modifications
## TROUBLESHOOTING
### "Remote rspade_upstream not configured"
Add the upstream remote:
git remote add rspade_upstream <upstream-url>
### "Framework is in forked mode"
You have created a `.rspade-forked-framework` marker file indicating you've
taken full ownership of the framework codebase. Automatic updates are disabled
to prevent overwriting your modifications.
To manually update from upstream:
git fetch rspade_upstream
git diff rspade_upstream/master
# Manually merge desired changes
# Test thoroughly
For detailed procedures:
php artisan rsx:man framework_fork
### Merge conflicts every update
Your local changes to framework files conflict with upstream changes frequently.
**Solutions:**
1. **Minimize framework modifications** - Keep changes to ./rsx only
2. **Fork the framework** - Take full control (see framework_fork)
3. **Reset local changes** - Discard modifications and re-apply after update
### Updates take too long / timeout
Framework updates include rebuilding manifest and recompiling bundles, which
can take 2-5 minutes. Ensure sufficient timeout when running the command.
For LLM agents: Use 5-minute timeout when executing framework updates.
## FOR FRAMEWORK DEVELOPERS
When distributing RSpade:
1. Ensure `.gitignore` excludes ./rsx directory
2. Configure `.gitattributes` with `/rsx/** merge=ours`
3. Set up `merge.ours.driver` git config
4. Document the framework update workflow
5. Provide upstream repository URL to users
## FOR APPLICATION DEVELOPERS
When using RSpade:
1. Clone or receive RSpade framework
2. Configure `rspade_upstream` remote
3. Work entirely in ./rsx directory
4. Update framework regularly with `php artisan rsx:framework:pull`
5. Never modify files outside ./rsx (unless forking)
Your ./rsx directory is independent and can be:
- Pushed to your own git repository
- Deployed independently
- Shared with your team
- Managed with standard git workflows
## SEE ALSO
framework_fork(7) - Forking the framework for custom modifications
rsx_architecture(7) - RSX application structure
coding_standards(7) - RSpade development conventions

526
app/RSpade/man/session.txt Executable file
View File

@@ -0,0 +1,526 @@
NAME
session - RSX session management and authentication system
SYNOPSIS
Static session interface with lazy initialization and CLI mode support
DESCRIPTION
The RSX Session class provides a unified interface for session management
and authentication. Unlike Laravel's session() helper which returns a
session store, RSX uses static methods for direct access to session data.
Sessions are always persistent (365 days) - there is no "remember me"
option. The framework handles session creation, authentication, and
security automatically.
Key differences from Laravel:
- Laravel: session()->put('user_id', $id) or Auth::login($user)
- RSX: Session::set_user($user) or Session::set_user_id($user_id)
Benefits:
- Static interface accessible from anywhere
- Automatic session creation only when needed
- CSRF token management built-in
- Secure cookie settings by default
- CLI mode support (no database operations)
- No session overhead until actually used
LAZY INITIALIZATION
Critical Concept:
Sessions are NOT created until a method forces them to exist. Simply
calling Session::get_user_id() or Session::get_site_id() will NOT
create a session if none exists - they return null/0.
Session is created when:
- Session::set_user() or Session::set_user_id() is called
- Session::set_site() or Session::set_site_id() is called
- Session::get_session_id() is called
- Session::get_session() is called
Session is NOT created when:
- Session::get_user_id() - returns null if no session
- Session::get_site_id() - returns 0 if no session
- Session::get_user() - returns null if no session
- Session::get_site() - returns null if no session
- Session::is_logged_in() - returns false if no session
- Session::has_session() - returns false if no session
- Session::init() - only loads existing session, never creates
Example Flow:
// Page load - no session created yet
$user_id = Session::get_user_id(); // null, no session created
// User logs in - NOW session is created
Session::set_user_id(123); // Creates session with user_id=123
// Subsequent requests load the existing session
$user_id = Session::get_user_id(); // 123, loads from cookie
This lazy initialization prevents unnecessary database writes for
unauthenticated visitors who never log in or interact with site-specific
features.
CLI MODE
Behavior in CLI Context:
When running in CLI mode (artisan commands, tests), Session operates
in a degraded "mock" mode using static properties only:
- No database reads or writes
- No cookie operations
- No HTTP headers sent
- Session data stored in static properties only
- get_client_ip() returns "CLI"
Usage in CLI:
// Set user and site for command context
Session::set_user_id(123);
Session::set_site_id(456);
// All getter methods work normally
$user_id = Session::get_user_id(); // 123
$site_id = Session::get_site_id(); // 456
$logged_in = Session::is_logged_in(); // true
$has_session = Session::has_session(); // true
CLI mode automatically detects when running via php_sapi_name() === 'cli'
and requires external code to explicitly set user_id/site_id.
BASIC USAGE
Check Authentication:
use App\RSpade\Core\Session\Session;
// Check if user is logged in
if (Session::is_logged_in()) {
$user = Session::get_user();
$user_id = Session::get_user_id();
}
// Check if session exists at all
if (Session::has_session()) {
// Session record exists or CLI values set
}
Login User:
// Option 1: Pass user ID
Session::set_user_id(123);
// Option 2: Pass user model
$user = User_Model::find(123);
Session::set_user($user);
// Both options:
// - Create session if none exists
// - Regenerate session token (prevent session fixation)
// - Update user's last_login timestamp
// - Set secure cookie
Logout:
Session::logout();
// OR
Session::set_user(null);
Site Management:
// Get current site
$site_id = Session::get_site_id(); // Returns 0 if not set
$site = Session::get_site(); // Returns Site_Model or null
// Set site
Session::set_site_id(456);
// OR
Session::set_site($site_model);
CSRF Protection:
// Get CSRF token for forms
$token = Session::get_csrf_token();
// Verify submitted token
if (Session::verify_csrf_token($submitted_token)) {
// Token is valid
}
API REFERENCE
Authentication Methods:
Session::is_logged_in(): bool
Returns true if user_id is set, false otherwise.
Does NOT create session if none exists.
Session::has_session(): bool
Returns true if session exists (web mode) or if site_id/user_id
set (CLI mode). Does NOT create session if none exists.
User Methods:
Session::get_user_id(): int|null
Returns current user ID or null. Does NOT create session.
Session::get_user(): User_Model|null
Returns User_Model for current user or null. Caches result.
Does NOT create session.
Session::set_user(User_Model|int|null $user): void
Set current user. Pass null or 0 to logout.
CREATES session if none exists.
Regenerates session token on login (security).
Session::set_user_id(int|null $user_id): void
Convenience method, same as set_user().
CREATES session if none exists.
Session::logout(): void
Clears user_id from session. Same as set_user(null).
Site Methods:
Session::get_site_id(): int
Returns current site ID or 0. Does NOT create session.
Session::get_site(): Site_Model|null
Returns Site_Model for current site or null. Caches result.
Does NOT create session.
Session::set_site(Site_Model|int $site): void
Set current site ID. CREATES session if none exists.
Session::set_site_id(int $site_id): void
Convenience method, same as set_site().
CREATES session if none exists.
Site User Methods:
Session::get_site_user(): Site_User_Model|null
Returns Site_User_Model matching both current user_id and site_id,
or null if either is not set or no matching record exists.
Does NOT create session.
Session Access Methods:
Session::get_session_id(): int
Returns session record ID. CREATES session if none exists.
Session::get_session(): Session
Returns Session model. CREATES session if none exists.
CSRF Methods:
Session::get_csrf_token(): string|null
Returns CSRF token for current session or null.
Does NOT create session.
Session::verify_csrf_token(string $token): bool
Verifies submitted CSRF token using constant-time comparison.
Returns false if no session exists.
Administrative Methods:
Session::find_by_token(string $token): Session|null
Find active session by token (for API/external access).
Session::cleanup_expired(int $days = 365): int
Delete sessions older than specified days. Returns count deleted.
Run periodically via scheduled command.
Session::reset(): void
Logout and clear all session data. Marks session inactive.
Clears cookie.
CONTROLLER AUTHENTICATION
Using pre_dispatch:
class Admin_Controller extends Rsx_Controller_Abstract
{
public static function pre_dispatch(Request $request, array $params = [])
{
// Require authentication for all admin routes
if (!Session::is_logged_in()) {
return redirect('/login');
}
// Require specific user role
$user = Session::get_user();
if (!$user || $user->role !== 'admin') {
return redirect('/');
}
return null; // Continue to route
}
}
Application-wide Authentication:
In rsx/main.php:
public static function pre_dispatch(Request $request, array $params = [])
{
// Require auth for /app/* routes
if (str_starts_with($request->path(), 'app/')) {
if (!Session::is_logged_in()) {
return redirect('/login');
}
}
return null;
}
SECURITY
Automatic Security Features:
- Session tokens: 64-character cryptographically secure random
- Token regeneration on login prevents session fixation
- Constant-time token comparison prevents timing attacks
- Secure cookie flags: httponly, secure, samesite=Lax
- CSRF tokens generated automatically
- 365-day expiration with last_active tracking
Cookie Settings:
- Name: rsx_session
- HttpOnly: true (no JavaScript access)
- Secure: true (HTTPS only)
- SameSite: Lax (CSRF protection)
- Path: /
- Expires: 365 days from last activity
Session Fixation Prevention:
Session tokens are regenerated when:
- User logs in (set_user called with non-null value)
This prevents attackers from forcing a victim to use a
known session token.
EXAMPLES
Login Form Processing:
#[Route('/login')]
public static function login(Request $request, array $params = [])
{
if ($request->method() === 'POST') {
$email = $request->input('email');
$password = $request->input('password');
$user = User_Model::where('email', $email)->first();
if ($user && password_verify($password, $user->password)) {
// Login successful - this creates/regenerates session
Session::set_user($user);
Rsx::flash_success('Login successful!');
return redirect('/dashboard');
}
Rsx::flash_error('Invalid credentials');
return redirect('/login');
}
return view('auth/login');
}
Require Authentication:
public static function dashboard(Request $request, array $params = [])
{
// Check authentication at start of method
if (!Session::is_logged_in()) {
Rsx::flash_error('Please login to continue');
return redirect('/login');
}
$user = Session::get_user();
return view('dashboard/index', [
'user' => $user,
]);
}
Multi-tenant Site Selection:
public static function select_site(Request $request, array $params = [])
{
$site_id = $params['site_id'] ?? null;
// Verify user has access to this site
$site_user = Site_User_Model::where('user_id', Session::get_user_id())
->where('site_id', $site_id)
->first();
if (!$site_user) {
Rsx::flash_error('Access denied to this site');
return redirect('/sites');
}
// Set active site
Session::set_site_id($site_id);
Rsx::flash_success('Switched to ' . $site_user->site->name);
return redirect('/dashboard');
}
CSRF Protection in Forms:
In controller:
return view('forms/example', [
'csrf_token' => Session::get_csrf_token(),
]);
In Blade view:
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ $csrf_token }}">
<!-- form fields -->
</form>
In POST handler:
$submitted = $request->input('csrf_token');
if (!Session::verify_csrf_token($submitted)) {
Rsx::flash_error('Invalid CSRF token');
return redirect()->back();
}
CLI Command with User Context:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\RSpade\Core\Session\Session;
class ProcessUserData extends Command
{
protected $signature = 'data:process {user_id}';
public function handle()
{
$user_id = $this->argument('user_id');
// Set CLI session context
Session::set_user_id($user_id);
// Now all code that checks Session::get_user_id() will work
$this->process_data();
$this->info('Processed data for user ' . $user_id);
}
private function process_data()
{
// This works because we set user_id above
$user_id = Session::get_user_id();
// ... process data for user ...
}
}
RSX VS LARAVEL
Session Access:
Laravel:
session()->put('key', 'value');
$value = session()->get('key');
session()->forget('key');
RSX:
Session::set_user_id(123);
$user_id = Session::get_user_id();
Session::logout();
RSX uses typed methods instead of generic key/value storage.
Authentication:
Laravel:
Auth::login($user);
Auth::logout();
$user = Auth::user();
$id = Auth::id();
if (Auth::check()) { }
RSX:
Session::set_user($user);
Session::logout();
$user = Session::get_user();
$id = Session::get_user_id();
if (Session::is_logged_in()) { }
RSX combines session and auth into one unified interface.
Session Creation:
Laravel:
Session starts automatically on every request via middleware.
RSX:
Session only created when explicitly needed via set_user(),
set_site(), get_session_id(), or get_session(). Getter methods
like get_user_id() do NOT create sessions.
This prevents unnecessary database writes for unauthenticated traffic.
CSRF Protection:
Laravel:
@csrf in Blade templates
$request->session()->token()
Automatic verification via middleware
RSX:
Session::get_csrf_token() in controllers
Session::verify_csrf_token($token) manual verification
Manual implementation gives explicit control
Remember Me:
Laravel:
Auth::login($user, $remember = true);
Optional remember functionality
RSX:
All sessions are persistent (365 days)
No "remember me" checkbox needed
TROUBLESHOOTING
Session Not Persisting Across Requests:
Problem: Session data is lost between requests
Solutions:
- Check that cookies are enabled in browser
- Verify HTTPS is being used (secure flag requires HTTPS)
- Check session token in cookie: rsx_session
- Verify session record exists in database
- Check last_active timestamp is being updated
User Logged Out Unexpectedly:
Problem: User is logged out without calling logout()
Solutions:
- Check session expiration (365 days by default)
- Verify session.active = true in database
- Check for code calling Session::reset() or Session::logout()
- Verify session token hasn't changed
CSRF Token Validation Failing:
Problem: verify_csrf_token() returns false
Solutions:
- Ensure token is being submitted in POST data
- Check session exists (get_csrf_token returns null if no session)
- Verify token wasn't regenerated between form display and submit
- Check for session fixation prevention regenerating token
CLI Commands Not Working:
Problem: Session methods return null/0 in artisan commands
Solution:
- Explicitly set user_id/site_id in CLI context:
Session::set_user_id($user_id);
Session::set_site_id($site_id);
- CLI mode requires manual context setting, does not load from DB
Session Created for Anonymous Users:
Problem: Every visitor gets a session record
Solution:
- Only call methods that CREATE sessions when needed:
CREATES: set_user(), set_site(), get_session_id(), get_session()
NO CREATE: get_user_id(), get_site_id(), is_logged_in()
- Don't call get_session_id() or get_session() in templates/layouts
- Use has_session() to check without creating
GARBAGE COLLECTION
Expired Session Cleanup:
Sessions older than 365 days should be deleted periodically.
Create scheduled command:
php artisan make:command CleanupSessions
In handle() method:
$deleted = Session::cleanup_expired(365);
$this->info("Deleted $deleted expired sessions");
Schedule in app/Console/Kernel.php:
$schedule->command('sessions:cleanup')->daily();
The cleanup_expired() method deletes sessions where last_active
is older than the specified number of days.
SEE ALSO
rsx:man routing - Type-safe URL generation
rsx:man model_fetch - Ajax ORM with security
rsx:man rsx_architecture - RSX application structure

359
app/RSpade/man/ssr_fpc.txt Executable file
View File

@@ -0,0 +1,359 @@
================================================================================
SSR FULL PAGE CACHE (FPC)
================================================================================
OVERVIEW
The SSR (Server-Side Rendered) Full Page Cache system pre-renders static pages
via headless Chrome (Playwright) and caches the resulting HTML in Redis for
optimal SEO and performance. Routes marked with #[Static_Page] attribute are
automatically cached and served to unauthenticated users.
This system is ideal for:
- Public-facing pages (landing pages, about, blog posts, documentation)
- Content that changes infrequently but is accessed frequently
- Pages requiring optimal SEO (search engines get pre-rendered HTML)
- Reducing server load for high-traffic public pages
CORE CONCEPTS
1. STATIC PAGE MARKING
Routes opt-in to FPC by adding the #[Static_Page] attribute to their
controller method. Only routes with this attribute are eligible for caching.
2. AUTHENTICATION-AWARE CACHING
FPC only serves cached content to unauthenticated users (no active session).
Logged-in users always get dynamic content, ensuring personalized experiences.
3. BUILD-KEY AUTO-INVALIDATION
Cache keys incorporate the application's build_key, ensuring all cached
pages are automatically invalidated on deployment. No manual cache clearing
needed during deployments.
4. ETAG VALIDATION
Each cached page includes a 30-character ETag for HTTP cache validation.
Browsers can send If-None-Match headers to receive 304 Not Modified
responses, reducing bandwidth usage.
5. REDIRECT CACHING
If a route redirects (300-399 status codes), the FPC system caches the
first redirect response without following it, preserving redirect behavior.
6. QUERY PARAMETER STRIPPING
GET parameters are stripped from URLs before cache key generation, ensuring
/about and /about?utm_source=email serve the same cached content.
ATTRIBUTE SYNTAX
Add the #[Static_Page] attribute to any route method:
#[Static_Page]
#[Route('/about')]
public static function about(Request $request, array $params = [])
{
return view('frontend.about');
}
Requirements:
- Route must be GET only (POST, PUT, DELETE not supported)
- Route must not require authentication (served to unauthenticated users only)
- Route should produce consistent output regardless of GET parameters
CACHE LIFECYCLE
1. ROUTE DISCOVERY
During request dispatch, the Dispatcher checks if the matched route has
the #[Static_Page] attribute.
2. SESSION CHECK
If the user has an active session, FPC is bypassed and the route executes
normally to provide personalized content.
3. CACHE LOOKUP
For unauthenticated users, the Dispatcher queries Redis for a cached version
using the key format: ssr_fpc:{build_key}:{sha1(url)}
4. CACHE GENERATION (ON MISS)
If no cache exists, the system automatically runs:
php artisan rsx:ssr_fpc:create /route
This command:
- Launches headless Chrome via Playwright
- Sets X-RSpade-FPC-Client header to prevent infinite loops
- Navigates to the route and waits for _debug_ready event
- Captures the fully rendered DOM or redirect response
- Stores the result in Redis with metadata (ETag, build_key, etc.)
5. CACHE SERVING
The cached HTML is served with appropriate headers:
- ETag: {30-char hash} for cache validation
- Cache-Control: public, max-age=300 (5 min in production)
- Cache-Control: no-cache, must-revalidate (0s in development)
6. CACHE INVALIDATION
Caches are automatically invalidated when:
- Application is deployed (build_key changes)
- Redis LRU eviction policy removes old entries
- Manual reset via rsx:ssr_fpc:reset command
COMMANDS
php artisan rsx:ssr_fpc:create <url>
Generate static cache for a specific URL.
Arguments:
url The URL path to cache (e.g., /about)
Behavior:
- Only available in non-production environments
- Requires rsx.ssr_fpc.enabled = true
- Strips GET parameters from URL
- Launches Playwright to render page
- Stores result in Redis
- Runs under GENERATE_STATIC_CACHE exclusive lock
Examples:
php artisan rsx:ssr_fpc:create /
php artisan rsx:ssr_fpc:create /about
php artisan rsx:ssr_fpc:create /blog/post-title
php artisan rsx:ssr_fpc:reset
Clear all SSR FPC cache entries from Redis.
Behavior:
- Available in all environments (local, staging, production)
- Safe to run - only affects ssr_fpc:* keys
- Does NOT affect application caches, sessions, or queue jobs
- Reports count of cleared cache entries
When to use:
- After major content updates across the site
- When troubleshooting caching issues
- Before deployment to ensure fresh cache generation (optional)
- When build_key changes (auto-invalidation should handle this)
CONFIGURATION
Configuration is stored in config/rsx.php under the 'ssr_fpc' key:
'ssr_fpc' => [
// Enable SSR Full Page Cache system
'enabled' => env('SSR_FPC_ENABLED', false),
// Playwright generation timeout in milliseconds
'generation_timeout' => env('SSR_FPC_TIMEOUT', 30000),
],
Environment Variables:
SSR_FPC_ENABLED Enable/disable the FPC system (default: false)
SSR_FPC_TIMEOUT Page generation timeout in ms (default: 30000)
To enable FPC, add to .env:
SSR_FPC_ENABLED=true
REDIS CACHE STRUCTURE
Cache Key Format:
ssr_fpc:{build_key}:{sha1(url)}
Example:
ssr_fpc:abc123def456:5d41402abc4b2a76b9719d911017c592
Cache Value (JSON):
{
"url": "/about",
"code": 200,
"page_dom": "<!DOCTYPE html>...",
"redirect": null,
"build_key": "abc123def456",
"etag": "abc123def456789012345678901234",
"generated_at": "2025-10-17 14:32:10"
}
For redirect responses:
{
"url": "/old-page",
"code": 301,
"page_dom": null,
"redirect": "/new-page",
"build_key": "abc123def456",
"etag": "abc123def456789012345678901234",
"generated_at": "2025-10-17 14:32:10"
}
Cache Eviction:
Redis LRU (Least Recently Used) policy automatically removes old entries
when memory limits are reached. No manual TTL management required.
SECURITY CONSIDERATIONS
1. INFINITE LOOP PREVENTION
The Playwright script sets X-RSpade-FPC-Client: 1 header to identify itself
as a cache generation request. Session::__is_fpc_client() checks this header
to prevent FPC from serving cached content to the generation process.
2. PRODUCTION COMMAND BLOCKING
The rsx:ssr_fpc:create command throws a fatal error in production to prevent
accidental cache generation in production environments. Cache generation
should happen automatically on first request or via staging/local environments.
3. AUTHENTICATION BYPASS
FPC only serves to unauthenticated users. Auth checks still run for logged-in
users, ensuring secure routes remain protected even if accidentally marked
with #[Static_Page].
4. QUERY PARAMETER STRIPPING
GET parameters are stripped before caching to prevent cache poisoning via
malicious query strings. Routes that depend on GET parameters should NOT
use #[Static_Page].
PERFORMANCE CHARACTERISTICS
Cache Hit (Unauthenticated User):
- Served directly from Redis before route execution
- No PHP controller execution
- No database queries
- Minimal memory usage
- Response time: ~1-5ms
Cache Miss (First Request):
- Playwright launches headless Chrome
- Page renders fully (waits for _debug_ready + network idle)
- DOM captured and stored in Redis
- Response time: ~500-2000ms (one-time cost)
Authenticated User:
- FPC bypassed completely
- Normal route execution
- No performance impact
DEBUGGING AND TROUBLESHOOTING
Enable console_debug output for FPC:
CONSOLE_DEBUG_FILTER=SSR_FPC php artisan rsx:debug /about
Common issues:
1. Cache not being served
- Check: rsx.ssr_fpc.enabled = true
- Check: Route has #[Static_Page] attribute
- Check: User is unauthenticated (no active session)
- Check: Redis is running and accessible
2. Cache generation fails
- Check: Playwright is installed (npm install)
- Check: Timeout setting (increase SSR_FPC_TIMEOUT)
- Check: Route returns valid HTML (not JSON/PDF/etc.)
- Check: Route doesn't redirect to external domains
3. Stale cache after deployment
- Cache should auto-invalidate (build_key changed)
- Manual reset: php artisan rsx:ssr_fpc:reset
- Check: build_key is being regenerated on deployment
4. 304 responses not working
- Check: Browser sends If-None-Match header
- Check: ETag matches exactly (case-sensitive)
- Check: Response includes ETag header
FUTURE ROADMAP
Planned enhancements:
1. SITEMAP INTEGRATION
Automatically generate static caches for all routes in sitemap.xml during
deployment. Ensures all public pages are pre-cached before first user visit.
2. PARALLEL GENERATION
Generate multiple static caches concurrently using a worker pool, reducing
total cache generation time for large sites.
3. SHARED SECRET KEYS
Support deployment across multiple servers with shared cache keys, enabling
cache pre-generation on staging before production deployment.
4. EXTERNAL CACHE SERVICE
Move cache generation to a separate service (e.g., AWS Lambda, dedicated
worker) to reduce load on application servers during cache regeneration.
5. PROGRAMMATIC CACHE RESET
Add programmatic methods to clear specific route caches after content updates:
Rsx_Static_Cache::clear('/blog/post-slug')
Rsx_Static_Cache::clear_pattern('/blog/*')
RELATED DOCUMENTATION
- php artisan rsx:man routing (Route system and attributes)
- php artisan rsx:man caching (General caching concepts)
- php artisan rsx:man console_debug (Debug output configuration)
- php artisan rsx:man auth (Authentication system)
EXAMPLES
Basic static page:
#[Static_Page]
#[Route('/')]
#[Auth('Permission::anybody()')]
public static function index(Request $request, array $params = [])
{
return view('frontend.index', [
'featured_products' => Product_Model::where('featured', true)->get(),
]);
}
Static page with redirect:
#[Static_Page]
#[Route('/old-page')]
#[Auth('Permission::anybody()')]
public static function old_page(Request $request, array $params = [])
{
return redirect('/new-page', 301);
}
NOT suitable for FPC (query-dependent):
// ❌ DO NOT use #[Static_Page] - depends on GET parameters
#[Route('/search')]
#[Auth('Permission::anybody()')]
public static function search(Request $request, array $params = [])
{
$query = $params['q'] ?? '';
return view('search.results', [
'results' => Product_Model::search($query)->get(),
]);
}
Pre-generate cache in deployment script:
#!/bin/bash
# deployment.sh
# Deploy application
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate
# Pre-generate static caches for high-traffic pages
php artisan rsx:ssr_fpc:create /
php artisan rsx:ssr_fpc:create /about
php artisan rsx:ssr_fpc:create /pricing
php artisan rsx:ssr_fpc:create /contact
# Note: This is optional - caches will auto-generate on first request
================================================================================