Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
306
app/RSpade/man/database_schema_architecture.txt
Executable file
306
app/RSpade/man/database_schema_architecture.txt
Executable file
@@ -0,0 +1,306 @@
|
||||
DATABASE SCHEMA ARCHITECTURE
|
||||
============================
|
||||
|
||||
OVERVIEW
|
||||
--------
|
||||
This document describes the RSpade database schema organization and the architectural
|
||||
principles governing how framework and application data is stored.
|
||||
|
||||
Date: 2024-11-18
|
||||
Status: Active framework architecture documentation
|
||||
|
||||
TABLE NAMING CONVENTIONS
|
||||
-------------------------
|
||||
RSpade uses table name prefixes to distinguish between different layers of the system:
|
||||
|
||||
1. SYSTEM TABLES (underscore prefix: _tablename)
|
||||
- Internal framework infrastructure
|
||||
- Managed entirely by framework subsystems
|
||||
- NOT meant to be accessed directly by application developers
|
||||
- May use direct DB::table() queries (exempt from ORM requirement)
|
||||
- Implementation details hidden from application layer
|
||||
|
||||
2. CORE TABLES (no prefix: tablename)
|
||||
- Built-in application infrastructure
|
||||
- Provided by framework but application-facing
|
||||
- Developers query, extend, and build upon these
|
||||
- MUST use ORM models (DB::table() prohibited)
|
||||
- Part of the developer-facing API
|
||||
|
||||
3. APPLICATION TABLES (no prefix: tablename)
|
||||
- Created by developers for their specific application
|
||||
- Business domain entities
|
||||
- MUST use ORM models (DB::table() prohibited)
|
||||
- Not shipped with framework (starter template only)
|
||||
|
||||
DATABASE ACCESS RULES
|
||||
----------------------
|
||||
The underscore prefix has enforcement implications:
|
||||
|
||||
SYSTEM TABLES (_tablename):
|
||||
- Direct queries allowed: DB::table('_task_queue')->where(...)->get()
|
||||
- No ORM model required
|
||||
- Code quality checker (PHP-DB-01) automatically skips these
|
||||
- Framework code optimizes for performance over abstraction
|
||||
|
||||
CORE & APPLICATION TABLES (tablename):
|
||||
- Direct queries PROHIBITED
|
||||
- ORM model REQUIRED
|
||||
- Code quality checker enforces this rule
|
||||
- Example: Client_Model::where(...)->get()
|
||||
|
||||
CURRENT SYSTEM TABLES
|
||||
----------------------
|
||||
|
||||
_migrations
|
||||
Purpose: Laravel migration tracking system
|
||||
Managed by: Laravel framework migration subsystem
|
||||
Records: Migration batch history, execution timestamps
|
||||
|
||||
_task_queue
|
||||
Purpose: Background job queue and scheduled task execution
|
||||
Managed by: RSpade Task system (Task.php, Task_Instance.php)
|
||||
Records: Queued jobs, scheduled tasks, execution logs, status tracking
|
||||
Schema: class, method, params, status, logs, scheduled_for, worker_pid
|
||||
|
||||
_file_storage
|
||||
Purpose: Content-addressable blob storage backend
|
||||
Managed by: RSpade File attachment system
|
||||
Records: Deduplicated file blobs indexed by SHA-256 hash
|
||||
Implementation: Single-instance storage pattern - multiple attachments can
|
||||
reference the same blob. Developers never access this directly.
|
||||
|
||||
_file_attachments
|
||||
Purpose: Polymorphic file attachment tracking
|
||||
Managed by: RSpade File attachment API
|
||||
Records: Attachment metadata, relationships, dimensions, session tracking
|
||||
Schema: key (UUID), file_storage_id (FK), fileable_type/id (polymorphic),
|
||||
fileable_category, site_id, session_id
|
||||
Developer API: Attachment methods, not direct table access
|
||||
|
||||
_flash_alerts
|
||||
Purpose: Transient flash message queue
|
||||
Managed by: Flash_Alert system
|
||||
Records: Session-scoped UI messages (success, error, warning, info)
|
||||
Lifecycle: Created on one request, displayed on next, then deleted
|
||||
Schema: session_id, type_id, message, timestamps
|
||||
|
||||
_search_indexes
|
||||
Purpose: Full-text search index storage
|
||||
Managed by: RSpade Search system
|
||||
Records: Extracted searchable content from polymorphic entities
|
||||
Schema: indexable_type/id (polymorphic), content (full-text), metadata (JSON),
|
||||
extraction_method, language, site_id
|
||||
Implementation: Automatic extraction from models, search API queries this
|
||||
|
||||
CURRENT CORE TABLES
|
||||
--------------------
|
||||
|
||||
sites
|
||||
Purpose: Multi-tenancy root table
|
||||
Managed by: Multi-tenant subsystem
|
||||
Records: Tenant organizations, each with isolated data space
|
||||
Usage: Developers query to show "available sites", build tenant switchers
|
||||
Schema: name, domain, settings, active status
|
||||
|
||||
users
|
||||
Purpose: User account storage
|
||||
Managed by: Authentication subsystem
|
||||
Records: User credentials, profile data, authentication state
|
||||
Usage: Developers extend with additional fields, query for user listings
|
||||
Schema: email, password_hash, name, site_id (FK), active status
|
||||
|
||||
login_users
|
||||
Purpose: Authentication session/token tracking
|
||||
Managed by: RsxAuth system
|
||||
Records: Active login sessions with 365-day persistence
|
||||
Usage: Developers query for "active sessions", implement logout-all
|
||||
Schema: user_id (FK), session_token, ip_address, last_activity, expires_at
|
||||
|
||||
user_profiles
|
||||
Purpose: Extended user profile information
|
||||
Managed by: User management system
|
||||
Records: Additional user data beyond core authentication
|
||||
Usage: Developers customize profile fields for their application
|
||||
Schema: user_id (FK), bio, avatar, preferences (JSON), custom fields
|
||||
|
||||
user_verifications
|
||||
Purpose: Email verification and password reset token storage
|
||||
Managed by: Authentication subsystem
|
||||
Records: Temporary verification codes with expiration
|
||||
Lifecycle: Created on request, validated once, then deleted or expired
|
||||
Schema: user_id (FK), token, type (email_verify, password_reset), expires_at
|
||||
|
||||
user_invites
|
||||
Purpose: User invitation system
|
||||
Managed by: User invitation subsystem
|
||||
Records: Pending invitations with expiration (48 hours default)
|
||||
Lifecycle: Created by admin, accepted by invitee, then marked used or expired
|
||||
Schema: site_id (FK), email, token, invited_by (FK), expires_at, accepted_at
|
||||
|
||||
ip_addresses
|
||||
Purpose: IP address tracking and audit trail
|
||||
Managed by: Security/audit subsystem
|
||||
Records: IP addresses associated with user actions
|
||||
Usage: Developers query for login history, security audit reports
|
||||
Schema: ip_address, user_id (FK), action_type, created_at
|
||||
|
||||
file_attachments (DEPRECATED - will become _file_attachments)
|
||||
See SYSTEM TABLES section above. Currently core, moving to system.
|
||||
|
||||
CURRENT APPLICATION TABLES
|
||||
---------------------------
|
||||
These tables are part of the starter template demonstration application.
|
||||
Developers typically replace these with their own domain entities.
|
||||
|
||||
clients
|
||||
Purpose: Demo business entity - client/customer records
|
||||
Business domain: B2B SaaS starter template
|
||||
Schema: name, email, phone, address, site_id (FK), status
|
||||
|
||||
client_departments
|
||||
Purpose: Demo organizational structure
|
||||
Business domain: Client organization hierarchy
|
||||
Schema: client_id (FK), name, parent_id (FK for nested structure)
|
||||
|
||||
contacts
|
||||
Purpose: Demo contact management
|
||||
Business domain: People associated with clients
|
||||
Schema: client_id (FK), name, email, phone, title, site_id (FK)
|
||||
|
||||
projects
|
||||
Purpose: Demo project tracking
|
||||
Business domain: Client projects/engagements
|
||||
Schema: client_id (FK), name, description, status, priority, due_date
|
||||
|
||||
tasks (NOTE: Confusing name - different from _task_queue!)
|
||||
Purpose: Demo user TODO/task list
|
||||
Business domain: User task management
|
||||
Schema: title, description, taskable_type/id (polymorphic), status, priority,
|
||||
assigned_to_user_id (FK), due_date, site_id (FK)
|
||||
WARNING: Name collision with _task_queue (background jobs). Consider renaming to
|
||||
user_task_queue or todo_items in future versions.
|
||||
|
||||
demo_products
|
||||
Purpose: Demo product catalog
|
||||
Business domain: Product/service offerings
|
||||
Schema: name, description, price, sku, category, active status
|
||||
|
||||
countries
|
||||
Purpose: Geographic reference data
|
||||
Business domain: International address support
|
||||
Records: ISO country codes and names
|
||||
Note: Reference data table, could be considered infrastructure
|
||||
|
||||
regions
|
||||
Purpose: Geographic subdivision reference data
|
||||
Business domain: State/province/region for addresses
|
||||
Records: Country subdivisions
|
||||
Note: Reference data table, could be considered infrastructure
|
||||
|
||||
ARCHITECTURAL PRINCIPLES
|
||||
-------------------------
|
||||
|
||||
1. SEPARATION OF CONCERNS
|
||||
System tables implement framework features invisibly. Core tables provide
|
||||
developer-facing infrastructure. Application tables express business domain.
|
||||
|
||||
2. ABSTRACTION BOUNDARIES
|
||||
System table schemas are implementation details. Core table schemas are API
|
||||
contracts. System tables can change without migration pain if APIs remain stable.
|
||||
|
||||
3. PERFORMANCE VS CONSISTENCY
|
||||
System tables prioritize performance (direct queries, denormalization OK).
|
||||
Core/Application tables prioritize data integrity (ORM, relationships, validation).
|
||||
|
||||
4. MIGRATION STRATEGY
|
||||
All migrations run chronologically when `php artisan migrate` is executed,
|
||||
regardless of whether they affect system, core, or application tables.
|
||||
|
||||
System tables: Migrations written and shipped by framework developers
|
||||
Core tables: Migrations written and shipped by framework developers
|
||||
Application tables: Migrations written by application developers
|
||||
|
||||
When a developer does `git pull` and new framework migrations exist, they
|
||||
run alongside application migrations in date order when migrate is executed.
|
||||
|
||||
5. DOCUMENTATION LEVEL
|
||||
System tables: Internal documentation only
|
||||
Core tables: Full API documentation required
|
||||
Application tables: Developer-documented
|
||||
|
||||
FUTURE CONSIDERATIONS
|
||||
---------------------
|
||||
|
||||
Planned Changes:
|
||||
- Rename 'migrations' to '_migrations' (Laravel supports configuring this)
|
||||
- Move 'file_attachments' to '_file_attachments' (pure system implementation detail)
|
||||
- Consider renaming 'tasks' to 'user_task_queue' or 'todo_items' to avoid confusion with '_task_queue'
|
||||
|
||||
Potential System Tables:
|
||||
- _cache (if implementing database cache driver)
|
||||
- _jobs (if using database queue driver - currently using '_task_queue')
|
||||
- _notifications (if implementing notification queue)
|
||||
- _websocket_connections (if implementing WebSocket presence tracking)
|
||||
|
||||
MIGRATION MECHANICS
|
||||
-------------------
|
||||
|
||||
When renaming a table to add underscore prefix:
|
||||
|
||||
1. Create migration to rename table
|
||||
2. Update all Model classes: protected $table = '_newtablename';
|
||||
3. Update direct queries in framework code
|
||||
4. Update foreign key references in other tables (if any)
|
||||
5. Update seeders, factories, tests
|
||||
6. Run code quality checker to find missed references
|
||||
7. Test all affected subsystems
|
||||
|
||||
Example migration:
|
||||
Schema::rename('file_storage', '_file_storage');
|
||||
|
||||
The underscore prefix is purely a naming convention - database engines treat
|
||||
these identically to non-prefixed tables. The convention exists for developer
|
||||
clarity and code quality enforcement.
|
||||
|
||||
DEVELOPER GUIDANCE
|
||||
------------------
|
||||
|
||||
When creating new tables, ask:
|
||||
|
||||
Q: Will application developers query this directly?
|
||||
YES → Core/Application table (no prefix)
|
||||
NO → System table (underscore prefix)
|
||||
|
||||
Q: Is this an implementation detail of a framework subsystem?
|
||||
YES → System table (underscore prefix)
|
||||
NO → Core/Application table (no prefix)
|
||||
|
||||
Q: Would changing the schema break the developer-facing API?
|
||||
YES → Core table (no prefix) - schema is part of the API contract
|
||||
NO → System table (underscore prefix) - implementation detail
|
||||
|
||||
Examples:
|
||||
- User login sessions? System (_login_sessions) - API is RsxAuth methods
|
||||
- User accounts? Core (users) - developers extend and query this
|
||||
- Search index? System (_search_indexes) - API is Search::query()
|
||||
- Client records? Application (clients) - business domain entity
|
||||
|
||||
CONCLUSION
|
||||
----------
|
||||
|
||||
The underscore prefix system creates a clear architectural boundary between
|
||||
framework implementation details and developer-facing data structures. This
|
||||
enables the framework to optimize system tables for performance while maintaining
|
||||
API stability, and helps developers understand which tables are theirs to use
|
||||
versus which are managed by framework subsystems.
|
||||
|
||||
For questions or clarification, consult:
|
||||
- /system/app/RSpade/Core/Database/CLAUDE.md - Database subsystem documentation
|
||||
- /system/app/RSpade/Core/Task/CLAUDE.md - Task system documentation
|
||||
- /system/app/RSpade/Core/Files/CLAUDE.md - File attachment documentation
|
||||
- /system/app/RSpade/CodeQuality/CLAUDE.md - Code quality rules including PHP-DB-01
|
||||
|
||||
---
|
||||
Document maintained by: Framework development team
|
||||
Last updated: 2024-11-18
|
||||
@@ -234,41 +234,69 @@ EVENT BINDING (⚠️ VERIFY FUNCTIONALITY)
|
||||
⚠️ TODO: Verify @event binding syntax is functional in current jqhtml version.
|
||||
|
||||
THIS.ARGS VS THIS.DATA
|
||||
Two distinct data sources in components:
|
||||
Two distinct data sources with strict lifecycle rules:
|
||||
|
||||
this.args - Component Input Parameters:
|
||||
- Source: Attributes from component invocation ($attr=value)
|
||||
- When Set: During component construction
|
||||
- Purpose: Configuration and input parameters
|
||||
- Mutability: Treat as read-only
|
||||
- Examples: User ID, theme, callback functions
|
||||
this.args - Component State (what to load):
|
||||
- Source: Passed from parent via $attr=value
|
||||
- Purpose: Component state that determines what on_load() fetches
|
||||
- Mutability: Modifiable in all methods EXCEPT on_load()
|
||||
- Usage: Page numbers, filters, sort order, configuration
|
||||
- Examples: this.args.page, this.args.filter, this.args.user_id
|
||||
|
||||
this.data - Async Loaded Data:
|
||||
- Source: on_load() lifecycle method
|
||||
- When Set: During load phase (after render, before ready)
|
||||
- Purpose: Dynamic data from APIs or computations
|
||||
- Mutability: Can be modified, triggers re-render
|
||||
this.data - Loaded Data (from APIs):
|
||||
- Source: Set in on_load() lifecycle method
|
||||
- Purpose: Data fetched from APIs based on this.args
|
||||
- Mutability: ONLY modifiable in on_create() and on_load()
|
||||
- Freeze Cycle: Frozen after on_create(), unfrozen during on_load(),
|
||||
frozen again after on_load() completes
|
||||
- Initial State: Empty object {} on first render
|
||||
- Examples: API responses, processed data, cached values
|
||||
- Examples: API responses, fetched records, computed results
|
||||
|
||||
Lifecycle Restrictions (ENFORCED):
|
||||
- on_create(): Can modify this.data (set defaults)
|
||||
- on_load(): Can ONLY access this.args and this.data
|
||||
Cannot access this.$, this.$id(), or any other properties
|
||||
Can modify this.data freely
|
||||
- on_ready() / event handlers: Can modify this.args, read this.data
|
||||
CANNOT modify this.data (frozen)
|
||||
|
||||
State Management Pattern:
|
||||
class Product_List extends Jqhtml_Component {
|
||||
on_create() {
|
||||
// Set default state for on_load()
|
||||
this.args.filter = this.args.filter || 'all';
|
||||
this.args.page = this.args.page || 1;
|
||||
|
||||
// Set default data for template
|
||||
this.data.products = [];
|
||||
this.data.loading = false;
|
||||
}
|
||||
|
||||
Example usage:
|
||||
class UserCard extends Jqhtml_Component {
|
||||
async on_load() {
|
||||
// this.data starts as {}
|
||||
const userId = this.args.userId; // From $user_id=...
|
||||
// Read state from this.args
|
||||
const filter = this.args.filter;
|
||||
const page = this.args.page;
|
||||
|
||||
// Load data and store in this.data
|
||||
this.data = await fetch(`/api/users/${userId}`)
|
||||
.then(r => r.json());
|
||||
// Triggers automatic re-render with populated data
|
||||
// Fetch and set this.data
|
||||
this.data = await Product_Controller.list({
|
||||
filter: filter,
|
||||
page: page
|
||||
});
|
||||
}
|
||||
|
||||
on_ready() {
|
||||
console.log(this.data.name); // Loaded data
|
||||
console.log(this.args.theme); // Input parameter
|
||||
// Modify state, then reload
|
||||
this.$id('filter_btn').on('click', () => {
|
||||
this.args.filter = 'active'; // Change state
|
||||
this.reload(); // Re-fetch with new state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Error Messages:
|
||||
Violations throw runtime errors with detailed fix suggestions.
|
||||
Example: "Cannot modify this.data outside of on_create() or on_load()."
|
||||
|
||||
COMMON PATTERNS AND PITFALLS
|
||||
Correct usage examples:
|
||||
<!-- Pass object reference -->
|
||||
@@ -299,20 +327,8 @@ COMMON PATTERNS AND PITFALLS
|
||||
}
|
||||
|
||||
CONTROL FLOW AND LOOPS
|
||||
Templates support full JavaScript control flow with two syntax styles:
|
||||
Templates support full JavaScript control flow using brace syntax:
|
||||
|
||||
Colon Syntax (PHP-like):
|
||||
<% if (this.data.show): %>
|
||||
<div>Visible content</div>
|
||||
<% else: %>
|
||||
<div>Hidden content</div>
|
||||
<% endif; %>
|
||||
|
||||
<% for (let item of this.data.items): %>
|
||||
<li><%= item.name %></li>
|
||||
<% endfor; %>
|
||||
|
||||
Brace Syntax (JavaScript-like):
|
||||
<% if (this.data.show) { %>
|
||||
<div>Visible content</div>
|
||||
<% } else { %>
|
||||
@@ -385,12 +401,15 @@ COMPONENT LIFECYCLE
|
||||
- Parent completes before children
|
||||
|
||||
4. on_load() (bottom-up, siblings in parallel, CAN be async)
|
||||
- Load async data
|
||||
- Load async data based on this.args
|
||||
- ONLY access this.args and this.data (RESTRICTED)
|
||||
- CANNOT access this.$, this.$id(), or any other properties
|
||||
- ONLY modify this.data - NEVER DOM
|
||||
- NO child component access
|
||||
- Siblings at same depth execute in parallel
|
||||
- Children complete before parent
|
||||
- If this.data changes, triggers automatic re-render
|
||||
- Runtime enforces access restrictions with clear errors
|
||||
|
||||
5. on_ready() (bottom-up)
|
||||
- All children guaranteed ready
|
||||
@@ -737,14 +756,14 @@ DOM CLASS CONVENTION
|
||||
All instantiated jqhtml components receive CSS classes on their
|
||||
DOM elements:
|
||||
|
||||
- 'Jqhtml_Component' - All components
|
||||
- 'Component' - All components
|
||||
- Component name (e.g., 'User_Card') - For targeting
|
||||
|
||||
// Select all jqhtml components on the page
|
||||
const components = $('.Jqhtml_Component');
|
||||
const components = $('.Component');
|
||||
|
||||
// Check if an element is a jqhtml component
|
||||
if ($element.hasClass('Jqhtml_Component')) {
|
||||
if ($element.hasClass('Component')) {
|
||||
// This is a jqhtml component
|
||||
}
|
||||
|
||||
@@ -887,13 +906,13 @@ LIFECYCLE MANIPULATION METHODS
|
||||
|
||||
Method Summary (jqhtml v2.2.182+):
|
||||
|
||||
reload() - Calls on_load() then render() - always re-renders
|
||||
reload() - Restore this.data to defaults, call on_load(), then render()
|
||||
render() - Re-renders with full lifecycle (waits for children, calls on_ready)
|
||||
redraw() - Alias for render()
|
||||
|
||||
reload()
|
||||
Re-fetch data and re-render - Calls on_load() then render() with full
|
||||
lifecycle. Always re-renders after loading data.
|
||||
Re-fetch data and re-render - Restores this.data to on_create() state,
|
||||
calls on_load() to fetch fresh data, then renders.
|
||||
|
||||
Usage:
|
||||
class User_Card extends Jqhtml_Component {
|
||||
@@ -901,16 +920,24 @@ LIFECYCLE MANIPULATION METHODS
|
||||
await this.reload(); // Fetches fresh data
|
||||
console.log('User data updated');
|
||||
}
|
||||
|
||||
on_ready() {
|
||||
this.$id('filter_btn').on('click', async () => {
|
||||
this.args.filter = 'active'; // Update state
|
||||
await this.reload(); // Re-fetch with new state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Behavior:
|
||||
- Calls on_load() to fetch fresh data
|
||||
- Restores this.data to snapshot from on_create()
|
||||
- Calls on_load() to fetch fresh data based on current this.args
|
||||
- Always re-renders (calls render() with full lifecycle)
|
||||
- Waits for children and calls on_ready()
|
||||
- Returns promise - await for completion
|
||||
- Use when: Need fresh data from server/API
|
||||
- Use when: this.args changed and need to re-fetch data
|
||||
|
||||
Lifecycle: on_load() → render() → on_ready() → trigger('ready')
|
||||
Lifecycle: restore this.data → on_load() → render() → on_ready() → trigger('ready')
|
||||
|
||||
render()
|
||||
Re-render component - Re-executes template with full lifecycle,
|
||||
@@ -936,23 +963,9 @@ LIFECYCLE MANIPULATION METHODS
|
||||
|
||||
Note: redraw() is an alias for render()
|
||||
|
||||
reinitialize()
|
||||
Full component reset - Restarts entire lifecycle from stage 0
|
||||
(nuclear option).
|
||||
|
||||
Usage:
|
||||
class Dashboard extends Jqhtml_Component {
|
||||
async switch_user(new_user_id) {
|
||||
this.args.user_id = new_user_id;
|
||||
await this.reinitialize(); // Complete reset
|
||||
}
|
||||
}
|
||||
|
||||
Behavior:
|
||||
- Stops current component state
|
||||
- Re-runs entire lifecycle: render → on_render → create → load → ready
|
||||
- Use when: Component needs complete rebuild
|
||||
- Rare use case - usually reload() or render() sufficient
|
||||
NOTE: reinitialize() method removed in v2.2.201.
|
||||
Use reload() for re-fetching data (99% of cases).
|
||||
For complete reset, remove component and create new instance.
|
||||
|
||||
stop()
|
||||
Component stopping - Removes component and all children from DOM.
|
||||
@@ -1035,9 +1048,26 @@ DOM UTILITIES
|
||||
This is genuine jQuery - all methods work directly
|
||||
|
||||
this.$id(name)
|
||||
Get element by scoped ID
|
||||
Get scoped element as jQuery object
|
||||
Example: this.$id('edit') gets element with $id="edit"
|
||||
Returns jQuery object
|
||||
Returns jQuery object, NOT component instance
|
||||
|
||||
this.id(name)
|
||||
Get scoped child component instance directly
|
||||
Example: this.id('my_component') gets component instance
|
||||
Returns component instance, NOT jQuery object
|
||||
|
||||
CRITICAL: this.$id() vs this.id() distinction
|
||||
- this.$id('foo') → jQuery object (for DOM manipulation)
|
||||
- this.id('foo') → Component instance (for calling methods)
|
||||
|
||||
Common mistake:
|
||||
const comp = this.id('foo').component(); // ❌ WRONG
|
||||
const comp = this.id('foo'); // ✅ CORRECT
|
||||
|
||||
Getting component from jQuery:
|
||||
const $elem = this.$id('foo');
|
||||
const comp = $elem.component(); // ✅ CORRECT (jQuery → component)
|
||||
|
||||
this.data
|
||||
Component data object
|
||||
|
||||
@@ -12,7 +12,7 @@ DESCRIPTION
|
||||
|
||||
Key differences from Laravel:
|
||||
- Laravel: route('user.profile', $user) using named routes
|
||||
- RSX: Rsx::Route('User_Controller', 'profile', ['id' => $user->id])
|
||||
- RSX: Rsx::Route('User_Controller::profile', ['id' => $user->id])
|
||||
|
||||
Benefits:
|
||||
- No route name management required
|
||||
@@ -22,24 +22,33 @@ DESCRIPTION
|
||||
- Refactoring-safe (renaming controllers updates routes)
|
||||
|
||||
BASIC USAGE
|
||||
Signature:
|
||||
Rsx::Route($action, $params = null)
|
||||
Rsx.Route(action, params = null)
|
||||
|
||||
Where:
|
||||
- $action: Controller class, SPA action, or "Class::method"
|
||||
Defaults to 'index' method if no :: present
|
||||
- $params: Integer sets 'id', array/object provides named params
|
||||
|
||||
PHP Syntax:
|
||||
use App\RSpade\Core\Rsx;
|
||||
|
||||
// Generate URLs (returns string directly)
|
||||
$url = Rsx::Route('Demo_Index_Controller'); // /demo
|
||||
$url = Rsx::Route('Demo_Index_Controller', 'show'); // /demo/show
|
||||
$url = Rsx::Route('Demo_Index_Controller', 'show', ['id' => 123]); // /demo/123
|
||||
$url = Rsx::Route('Demo_Index_Controller', 'show', 123); // /demo/123 (shorthand)
|
||||
// Generate URLs (defaults to 'index' method)
|
||||
$url = Rsx::Route('Demo_Index_Controller'); // /demo
|
||||
$url = Rsx::Route('Demo_Index_Controller::show'); // /demo/show
|
||||
$url = Rsx::Route('Demo_Index_Controller::show', 123); // /demo/123
|
||||
$url = Rsx::Route('Demo_Index_Controller::show', ['id' => 123]); // /demo/123
|
||||
|
||||
// Use in redirects
|
||||
return redirect(Rsx::Route('Demo_Index_Controller'));
|
||||
|
||||
JavaScript Syntax:
|
||||
// Generate URLs (returns string directly)
|
||||
const url = Rsx.Route('Demo_Index_Controller'); // /demo
|
||||
const url = Rsx.Route('Demo_Index_Controller', 'show'); // /demo/show
|
||||
const url = Rsx.Route('Demo_Index_Controller', 'show', {id: 123}); // /demo/123
|
||||
const url = Rsx.Route('Demo_Index_Controller', 'show', 123); // /demo/123 (shorthand)
|
||||
// Generate URLs (defaults to 'index' method)
|
||||
const url = Rsx.Route('Demo_Index_Controller'); // /demo
|
||||
const url = Rsx.Route('Demo_Index_Controller::show'); // /demo/show
|
||||
const url = Rsx.Route('Demo_Index_Controller::show', 123); // /demo/123
|
||||
const url = Rsx.Route('Demo_Index_Controller::show', {id: 123}); // /demo/123
|
||||
|
||||
// Use in navigation
|
||||
window.location.href = Rsx.Route('Demo_Index_Controller');
|
||||
@@ -143,17 +152,17 @@ ADVANCED PATTERNS
|
||||
Complex Parameter Examples:
|
||||
// Multiple parameters
|
||||
#[Route('/api/v1/users/:company/:division/:id')]
|
||||
$url = Rsx::Route('Api_V1_Users_Controller', 'show',
|
||||
$url = Rsx::Route('Api_V1_Users_Controller::show',
|
||||
['company' => 'acme', 'division' => 'sales', 'id' => 123]);
|
||||
// Result: /api/v1/users/acme/sales/123
|
||||
|
||||
// Query parameters for extra values
|
||||
$url = Rsx::Route('Demo_Controller', 'show',
|
||||
$url = Rsx::Route('Demo_Controller::show',
|
||||
['id' => 123, 'format' => 'json', 'include' => 'profile']);
|
||||
// Result: /demo/123?format=json&include=profile
|
||||
|
||||
// Complex objects as parameters
|
||||
$url = Rsx::Route('Demo_Controller', 'index',
|
||||
$url = Rsx::Route('Demo_Controller',
|
||||
['filter' => ['status' => 'active', 'type' => 'user']]);
|
||||
// Result: /demo?filter[status]=active&filter[type]=user
|
||||
|
||||
@@ -230,12 +239,12 @@ COMMON PATTERNS
|
||||
|
||||
AJAX URL Generation:
|
||||
// Generate URLs for AJAX calls
|
||||
const apiUrl = Rsx.Route('Api_User_Controller', 'update', {id: userId});
|
||||
const apiUrl = Rsx.Route('Api_User_Controller::update', {id: userId});
|
||||
fetch(apiUrl, {method: 'POST', body: formData});
|
||||
|
||||
Form Action URLs:
|
||||
// Generate form action URLs
|
||||
<form action="<?= Rsx::Route('User_Profile_Controller', 'update') ?>" method="POST">
|
||||
<form action="<?= Rsx::Route('User_Profile_Controller::update') ?>" method="POST">
|
||||
|
||||
Link Generation:
|
||||
// Generate navigation links
|
||||
|
||||
@@ -504,21 +504,31 @@ TROUBLESHOOTING
|
||||
- Use has_session() to check without creating
|
||||
|
||||
GARBAGE COLLECTION
|
||||
Expired Session Cleanup:
|
||||
Sessions older than 365 days should be deleted periodically.
|
||||
Automatic Session Cleanup:
|
||||
Sessions are automatically cleaned up via scheduled task that runs
|
||||
daily at 3 AM. No manual configuration required.
|
||||
|
||||
Create scheduled command:
|
||||
php artisan make:command CleanupSessions
|
||||
Cleanup Rules:
|
||||
- Logged-in sessions (login_user_id set): Deleted after 365 days
|
||||
- Anonymous sessions (login_user_id null): Deleted after 14 days
|
||||
|
||||
In handle() method:
|
||||
$deleted = Session::cleanup_expired(365);
|
||||
$this->info("Deleted $deleted expired sessions");
|
||||
The cleanup uses last_active timestamp to determine session age.
|
||||
Sessions are permanently deleted from the database.
|
||||
|
||||
Schedule in app/Console/Kernel.php:
|
||||
$schedule->command('sessions:cleanup')->daily();
|
||||
Implementation:
|
||||
Service: Session_Cleanup_Service::cleanup_sessions()
|
||||
Schedule: Daily at 3 AM (via #[Schedule] attribute)
|
||||
Automatic: No cron configuration needed
|
||||
|
||||
The cleanup_expired() method deletes sessions where last_active
|
||||
is older than the specified number of days.
|
||||
Manual Trigger:
|
||||
To manually run cleanup outside the schedule:
|
||||
|
||||
php artisan rsx:task:run Session_Cleanup_Service cleanup_sessions
|
||||
|
||||
This is useful for:
|
||||
- Testing cleanup logic
|
||||
- Emergency cleanup when disk space is low
|
||||
- One-time cleanup after changing retention policies
|
||||
|
||||
SEE ALSO
|
||||
rsx:man routing - Type-safe URL generation
|
||||
|
||||
688
app/RSpade/man/spa.txt
Executable file
688
app/RSpade/man/spa.txt
Executable file
@@ -0,0 +1,688 @@
|
||||
SPA(3) RSX Framework Manual SPA(3)
|
||||
|
||||
NAME
|
||||
spa - Single Page Application routing for authenticated areas
|
||||
|
||||
SYNOPSIS
|
||||
PHP Bootstrap Controller (one per SPA module):
|
||||
|
||||
class Frontend_Spa_Controller extends Rsx_Controller_Abstract
|
||||
{
|
||||
#[SPA]
|
||||
#[Auth('Permission::authenticated()')]
|
||||
public static function index(Request $request, array $params = [])
|
||||
{
|
||||
return rsx_view(SPA);
|
||||
}
|
||||
}
|
||||
|
||||
JavaScript Action (many per module):
|
||||
|
||||
@route('/contacts')
|
||||
@layout('Frontend_Layout')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
class Contacts_Index_Action extends Spa_Action {
|
||||
async on_load() {
|
||||
this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
|
||||
}
|
||||
}
|
||||
|
||||
DESCRIPTION
|
||||
The RSX SPA system provides client-side routing for authenticated application
|
||||
areas, enabling navigation without page reloads. Unlike traditional Laravel
|
||||
views where each navigation triggers a full page load, SPA modules bootstrap
|
||||
once and handle all subsequent navigation client-side through JavaScript
|
||||
actions.
|
||||
|
||||
This approach is fundamentally different from traditional server-side routing:
|
||||
- Traditional: Every navigation loads new HTML from server
|
||||
- SPA: First request bootstraps application, subsequent navigation is client-side
|
||||
|
||||
Key differences from Laravel:
|
||||
- Laravel: Full page reload per navigation, routes in routes/ files
|
||||
- RSX: Bootstrap once, routes defined in JavaScript with @route() decorator
|
||||
|
||||
- Laravel: Separate frontend frameworks (Vue, React) require build tooling
|
||||
- RSX: Integrated JQHTML component system, no external build required
|
||||
|
||||
- Laravel: API routes separate from view routes
|
||||
- RSX: Controllers provide Ajax endpoints, actions consume them
|
||||
|
||||
Benefits:
|
||||
- No page reloads after initial bootstrap
|
||||
- Persistent layout across navigation
|
||||
- Automatic browser history integration
|
||||
- Same Rsx::Route() syntax for both traditional and SPA routes
|
||||
- No frontend build tooling required
|
||||
|
||||
When to use:
|
||||
- Authenticated areas (dashboards, admin panels)
|
||||
- Applications with frequent navigation
|
||||
- Features requiring persistent state
|
||||
|
||||
When to avoid:
|
||||
- Public pages requiring SEO
|
||||
- Simple static content
|
||||
- Forms without complex interactions
|
||||
|
||||
SPA ARCHITECTURE
|
||||
SPA Module Structure:
|
||||
An SPA module consists of six component types:
|
||||
|
||||
1. PHP Bootstrap Controller (ONE per feature/bundle)
|
||||
- Single entry point marked with #[SPA]
|
||||
- Performs authentication check with failure/redirect
|
||||
- Renders SPA bootstrap layout
|
||||
- Referenced by all actions via @spa() decorator
|
||||
- One #[SPA] per feature/bundle (e.g., /app/frontend, /app/root, /app/login)
|
||||
- Bundles segregate code for security and performance:
|
||||
* Save bandwidth by loading only needed features
|
||||
* Reduce processing time by smaller bundle sizes
|
||||
* Protect confidential code (e.g., root admin from unauthorized users)
|
||||
- Typically one #[SPA] per feature at rsx/app/(feature)/(feature)_spa_controller::index
|
||||
|
||||
2. Feature Controllers (Ajax Endpoints Only)
|
||||
- Provide data via #[Ajax_Endpoint] methods
|
||||
- No #[SPA] or #[Route] methods
|
||||
- Called by actions to fetch/save data
|
||||
|
||||
3. JavaScript Actions (MANY per module)
|
||||
- Represent individual pages/routes
|
||||
- Define route patterns with @route()
|
||||
- Load data in on_load() lifecycle method
|
||||
- Access URL parameters via this.args
|
||||
|
||||
4. Action Templates (.jqhtml)
|
||||
- Render action content
|
||||
- Standard JQHTML component templates
|
||||
- Replace traditional Blade views
|
||||
|
||||
5. Layout Template (.jqhtml)
|
||||
- Persistent wrapper around actions
|
||||
- Must have element with $id="content"
|
||||
- Persists across action navigation
|
||||
|
||||
6. Layout Class (.js)
|
||||
- Extends Spa_Layout
|
||||
- Optional on_action() hook for navigation tracking
|
||||
|
||||
Execution Flow:
|
||||
First Request:
|
||||
1. User navigates to SPA route (e.g., /contacts)
|
||||
2. Dispatcher calls bootstrap controller
|
||||
3. Bootstrap returns rsx_view(SPA) with window.rsxapp.is_spa = true
|
||||
4. Client JavaScript discovers all actions via manifest
|
||||
5. Router matches URL to action class
|
||||
6. Creates layout on <body>
|
||||
7. Creates action inside layout $id="content" area
|
||||
|
||||
Subsequent Navigation:
|
||||
1. User clicks link or calls Spa.dispatch()
|
||||
2. Router matches URL to action class
|
||||
3. Destroys old action, creates new action
|
||||
4. Layout persists - no re-render
|
||||
5. No server request, no page reload
|
||||
|
||||
BOOTSTRAP CONTROLLER
|
||||
Creating SPA Entry Point:
|
||||
// /rsx/app/frontend/Frontend_Spa_Controller.php
|
||||
class Frontend_Spa_Controller extends Rsx_Controller_Abstract
|
||||
{
|
||||
#[SPA]
|
||||
#[Auth('Permission::authenticated()')]
|
||||
public static function index(Request $request, array $params = [])
|
||||
{
|
||||
return rsx_view(SPA);
|
||||
}
|
||||
}
|
||||
|
||||
Key Points:
|
||||
- One bootstrap controller per feature/bundle
|
||||
- #[SPA] attribute marks as SPA entry point
|
||||
- #[Auth] performs server-side authentication with failure/redirect
|
||||
- Must return rsx_view(SPA) - special constant
|
||||
- All actions in the same bundle reference this via @spa() decorator
|
||||
|
||||
Bundle Segregation:
|
||||
- Separate bundles for different features: /app/frontend, /app/root, /app/login
|
||||
- Each bundle has its own #[SPA] bootstrap
|
||||
- Benefits:
|
||||
* Bandwidth: Users only download code for authorized features
|
||||
* Performance: Smaller bundles = faster load times
|
||||
* Security: Root admin code never sent to unauthorized users
|
||||
- Example: Frontend users never receive root admin JavaScript/CSS
|
||||
|
||||
Location:
|
||||
- Place at feature root: /rsx/app/{feature}/
|
||||
- Not in subdirectories
|
||||
- One per feature/bundle
|
||||
- Naming: {Feature}_Spa_Controller::index
|
||||
|
||||
Multiple SPA Bootstraps:
|
||||
Different features can have separate SPA bootstraps:
|
||||
- /app/frontend/Frontend_Spa_Controller::index (regular users)
|
||||
- /app/root/Root_Spa_Controller::index (root admins)
|
||||
- /app/login/Login_Spa_Controller::index (authentication flow)
|
||||
|
||||
Each bootstrap controls access to its feature's actions via #[Auth].
|
||||
|
||||
FEATURE CONTROLLERS
|
||||
Ajax Endpoint Pattern:
|
||||
Feature controllers provide data endpoints only, no routes or SPA entry:
|
||||
|
||||
// /rsx/app/frontend/contacts/frontend_contacts_controller.php
|
||||
class Frontend_Contacts_Controller extends Rsx_Controller_Abstract
|
||||
{
|
||||
#[Ajax_Endpoint]
|
||||
public static function datagrid_fetch(Request $request, array $params = []): array
|
||||
{
|
||||
return Contacts_DataGrid::fetch($params);
|
||||
}
|
||||
|
||||
#[Ajax_Endpoint]
|
||||
public static function save(Request $request, array $params = [])
|
||||
{
|
||||
// Validation and save logic
|
||||
$contact->save();
|
||||
|
||||
Flash_Alert::success('Contact saved successfully');
|
||||
|
||||
return [
|
||||
'contact_id' => $contact->id,
|
||||
'redirect' => Rsx::Route('Contacts_View_Action', $contact->id),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- All methods are #[Ajax_Endpoint]
|
||||
- No #[Route] methods in SPA feature controllers
|
||||
- No #[SPA] methods (only in bootstrap controller)
|
||||
- Return arrays/data, not views
|
||||
- Use Flash_Alert with redirects for success messages
|
||||
|
||||
See Also:
|
||||
ajax_error_handling(3) for error patterns
|
||||
controller(3) for #[Ajax_Endpoint] details
|
||||
|
||||
JAVASCRIPT ACTIONS
|
||||
Creating Actions:
|
||||
// /rsx/app/frontend/contacts/Contacts_Index_Action.js
|
||||
@route('/contacts')
|
||||
@layout('Frontend_Layout')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
class Contacts_Index_Action extends Spa_Action {
|
||||
async on_load() {
|
||||
this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
|
||||
}
|
||||
|
||||
on_create() {
|
||||
// Component initialization
|
||||
}
|
||||
|
||||
on_ready() {
|
||||
// DOM is ready, setup event handlers
|
||||
this.$id('search').on('input', () => this.reload());
|
||||
}
|
||||
}
|
||||
|
||||
Decorator Syntax:
|
||||
@route(pattern)
|
||||
URL pattern with optional :param segments
|
||||
Example: '/contacts/:id', '/users/:id/posts/:post_id'
|
||||
|
||||
@layout(class_name)
|
||||
Layout class to render within
|
||||
Example: 'Frontend_Layout', 'Dashboard_Layout'
|
||||
|
||||
@spa(controller::method)
|
||||
References bootstrap controller
|
||||
Example: 'Frontend_Spa_Controller::index'
|
||||
|
||||
@title(page_title)
|
||||
Browser page title for this action (optional, clears if not present)
|
||||
Alternatively, set dynamically in on_ready() via document.title = "value"
|
||||
Example: @title('Contacts - RSX')
|
||||
|
||||
URL Parameters:
|
||||
Parameters from route pattern and query string available in this.args:
|
||||
|
||||
// URL: /contacts/123?tab=history
|
||||
@route('/contacts/:id')
|
||||
class Contacts_View_Action extends Spa_Action {
|
||||
on_create() {
|
||||
console.log(this.args.id); // "123"
|
||||
console.log(this.args.tab); // "history"
|
||||
}
|
||||
}
|
||||
|
||||
Lifecycle Methods:
|
||||
on_create() Component construction, setup this.state
|
||||
on_load() Fetch data, populate this.data (read-only this.args)
|
||||
on_render() Template rendering
|
||||
on_ready() DOM ready, setup event handlers
|
||||
|
||||
See Also:
|
||||
jqhtml(3) for complete lifecycle documentation
|
||||
|
||||
ACTION TEMPLATES
|
||||
Creating Templates:
|
||||
Action templates are standard JQHTML components:
|
||||
|
||||
<!-- /rsx/app/frontend/contacts/Contacts_Index_Action.jqhtml -->
|
||||
<Define:Contacts_Index_Action tag="main">
|
||||
<h1>Contacts</h1>
|
||||
|
||||
<div class="contacts-list">
|
||||
<% for (let contact of this.data.contacts) { %>
|
||||
<div class="contact-item">
|
||||
<a href="<%= Rsx.Route('Contacts_View_Action', {id: contact.id}) %>">
|
||||
<%= contact.name %>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</Define:Contacts_Index_Action>
|
||||
|
||||
Key Points:
|
||||
- Use Rsx.Route() for all URLs, never hardcode
|
||||
- Access data via this.data (loaded in on_load)
|
||||
- Use standard JQHTML template syntax
|
||||
- Can reference other components
|
||||
|
||||
LAYOUTS
|
||||
Layout Template:
|
||||
Layout provides persistent wrapper around actions:
|
||||
|
||||
<!-- /rsx/app/frontend/Frontend_Layout.jqhtml -->
|
||||
<Define:Frontend_Layout>
|
||||
<div class="frontend-layout">
|
||||
<header class="mb-3">
|
||||
<h1>My App</h1>
|
||||
<nav>
|
||||
<a href="<%= Rsx.Route('Contacts_Index_Action') %>">Contacts</a>
|
||||
<a href="<%= Rsx.Route('Projects_Index_Action') %>">Projects</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main $id="content">
|
||||
<!-- Actions render here -->
|
||||
</main>
|
||||
|
||||
<footer class="mt-3">
|
||||
<p>© 2024 My Company</p>
|
||||
</footer>
|
||||
</div>
|
||||
</Define:Frontend_Layout>
|
||||
|
||||
Requirements:
|
||||
- Must have element with $id="content"
|
||||
- Content area is where actions render
|
||||
- Layout persists across navigation
|
||||
|
||||
Layout Class:
|
||||
Optional JavaScript class for navigation tracking:
|
||||
|
||||
// /rsx/app/frontend/Frontend_Layout.js
|
||||
class Frontend_Layout extends Spa_Layout {
|
||||
on_action(url, action_name, args) {
|
||||
// Called AFTER action component created and booted
|
||||
// Called BEFORE action reaches on_ready()
|
||||
// Can immediately access this.action properties
|
||||
|
||||
// Update active nav highlighting
|
||||
this.$('.nav-link').removeClass('active');
|
||||
this.$(`[data-action="${action_name}"]`).addClass('active');
|
||||
|
||||
// If you need to wait for action's full lifecycle:
|
||||
// await this.action.ready();
|
||||
}
|
||||
}
|
||||
|
||||
on_action() Lifecycle Timing:
|
||||
- Called after action component is created and booted
|
||||
- Called before action reaches on_ready()
|
||||
- Can immediately access this.action properties
|
||||
- Use await this.action.ready() to wait for action's full loading
|
||||
|
||||
URL GENERATION
|
||||
CRITICAL: All URLs must use Rsx::Route() or Rsx.Route(). Raw URLs like
|
||||
"/contacts" will produce errors.
|
||||
|
||||
Signature:
|
||||
Rsx::Route($action, $params = null)
|
||||
Rsx.Route(action, params = null)
|
||||
|
||||
Where:
|
||||
- $action: Controller class, SPA action, or "Class::method"
|
||||
Defaults to 'index' method if no :: present
|
||||
- $params: Integer sets 'id', array/object provides named params
|
||||
|
||||
PHP Syntax:
|
||||
// Works for both controller routes and SPA actions
|
||||
Rsx::Route('Contacts_Index_Action') // /contacts
|
||||
Rsx::Route('Contacts_View_Action', 123) // /contacts/123
|
||||
|
||||
JavaScript Syntax:
|
||||
// Works for both controller routes and SPA actions
|
||||
Rsx.Route('Contacts_Index_Action') // /contacts
|
||||
Rsx.Route('Contacts_View_Action', 123) // /contacts/123
|
||||
|
||||
Automatic Detection:
|
||||
Rsx::Route() automatically detects whether the class is:
|
||||
- PHP controller with #[Route] attribute
|
||||
- JavaScript SPA action with @route() decorator
|
||||
|
||||
Query Parameters:
|
||||
Extra parameters become query string:
|
||||
|
||||
Rsx::Route('Contacts_Index_Action', ['filter' => 'active', 'sort' => 'name'])
|
||||
// Result: /contacts?filter=active&sort=name
|
||||
|
||||
NAVIGATION
|
||||
Automatic Browser Integration:
|
||||
- Clicking links to SPA routes triggers client-side navigation
|
||||
- Browser back/forward buttons handled without page reload
|
||||
- URL updates in address bar via pushState()
|
||||
- Links to external or non-SPA routes perform full page load
|
||||
|
||||
Programmatic Navigation:
|
||||
Spa.dispatch(url)
|
||||
Navigate to URL programmatically
|
||||
If URL is part of current SPA, uses client-side routing
|
||||
If URL is external or different SPA, performs full page load
|
||||
|
||||
Example:
|
||||
// Navigate within SPA
|
||||
Spa.dispatch('/contacts/123');
|
||||
|
||||
// Navigate to external URL (full page load)
|
||||
Spa.dispatch('https://example.com');
|
||||
|
||||
Use Spa.dispatch() instead of window.location for all navigation.
|
||||
|
||||
Accessing Active Components:
|
||||
Spa.layout Reference to current layout component instance
|
||||
Spa.action Reference to current action component instance
|
||||
|
||||
Example:
|
||||
// From anywhere in SPA context
|
||||
Spa.action.reload(); // Reload current action
|
||||
Spa.layout.update_nav(); // Call layout method
|
||||
|
||||
FILE ORGANIZATION
|
||||
Standard SPA Module Structure:
|
||||
/rsx/app/frontend/
|
||||
|-- Frontend_Spa_Controller.php # Bootstrap controller
|
||||
|-- Frontend_Layout.js # Layout class
|
||||
|-- Frontend_Layout.jqhtml # Layout template
|
||||
|-- frontend_bundle.php # Bundle definition
|
||||
|
|
||||
|-- contacts/
|
||||
| |-- frontend_contacts_controller.php # Ajax endpoints
|
||||
| |-- Contacts_Index_Action.js # /contacts
|
||||
| |-- Contacts_Index_Action.jqhtml
|
||||
| |-- Contacts_View_Action.js # /contacts/:id
|
||||
| `-- Contacts_View_Action.jqhtml
|
||||
|
|
||||
`-- projects/
|
||||
|-- frontend_projects_controller.php
|
||||
|-- Projects_Index_Action.js
|
||||
`-- Projects_Index_Action.jqhtml
|
||||
|
||||
Naming Conventions:
|
||||
- Bootstrap controller: {Module}_Spa_Controller.php
|
||||
- Feature controllers: {feature}_{module}_controller.php
|
||||
- Actions: {Feature}_{Action}_Action.js
|
||||
- Templates: {Feature}_{Action}_Action.jqhtml
|
||||
- Layout: {Module}_Layout.js and .jqhtml
|
||||
|
||||
EXAMPLES
|
||||
Complete Contacts Module:
|
||||
|
||||
1. Bootstrap Controller:
|
||||
// /rsx/app/frontend/Frontend_Spa_Controller.php
|
||||
class Frontend_Spa_Controller extends Rsx_Controller_Abstract
|
||||
{
|
||||
#[SPA]
|
||||
#[Auth('Permission::authenticated()')]
|
||||
public static function index(Request $request, array $params = [])
|
||||
{
|
||||
return rsx_view(SPA);
|
||||
}
|
||||
}
|
||||
|
||||
2. Feature Controller:
|
||||
// /rsx/app/frontend/contacts/frontend_contacts_controller.php
|
||||
class Frontend_Contacts_Controller extends Rsx_Controller_Abstract
|
||||
{
|
||||
#[Ajax_Endpoint]
|
||||
public static function fetch_all(Request $request, array $params = []): array
|
||||
{
|
||||
$contacts = Contact_Model::query()
|
||||
->where('is_active', 1)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return $contacts->toArray();
|
||||
}
|
||||
|
||||
#[Ajax_Endpoint]
|
||||
public static function save(Request $request, array $params = [])
|
||||
{
|
||||
$contact = Contact_Model::findOrNew($params['id'] ?? null);
|
||||
$contact->fill($params);
|
||||
$contact->save();
|
||||
|
||||
Flash_Alert::success('Contact saved');
|
||||
|
||||
return [
|
||||
'contact_id' => $contact->id,
|
||||
'redirect' => Rsx::Route('Contacts_View_Action', $contact->id),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
3. List Action:
|
||||
// /rsx/app/frontend/contacts/Contacts_Index_Action.js
|
||||
@route('/contacts')
|
||||
@layout('Frontend_Layout')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
class Contacts_Index_Action extends Spa_Action {
|
||||
async on_load() {
|
||||
this.data.contacts = await Frontend_Contacts_Controller.fetch_all();
|
||||
}
|
||||
}
|
||||
|
||||
4. List Template:
|
||||
<!-- /rsx/app/frontend/contacts/Contacts_Index_Action.jqhtml -->
|
||||
<Define:Contacts_Index_Action tag="main">
|
||||
<h1>Contacts</h1>
|
||||
<% for (let contact of this.data.contacts) { %>
|
||||
<div>
|
||||
<a href="<%= Rsx.Route('Contacts_View_Action', {id: contact.id}) %>">
|
||||
<%= contact.name %>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</Define:Contacts_Index_Action>
|
||||
|
||||
5. View Action with Parameters:
|
||||
// /rsx/app/frontend/contacts/Contacts_View_Action.js
|
||||
@route('/contacts/:id')
|
||||
@layout('Frontend_Layout')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
class Contacts_View_Action extends Spa_Action {
|
||||
async on_load() {
|
||||
const contact_id = this.args.id;
|
||||
this.data.contact = await Contact_Model.fetch(contact_id);
|
||||
}
|
||||
}
|
||||
|
||||
6. Layout:
|
||||
<!-- /rsx/app/frontend/Frontend_Layout.jqhtml -->
|
||||
<Define:Frontend_Layout>
|
||||
<div class="app">
|
||||
<header>
|
||||
<nav>
|
||||
<a href="<%= Rsx.Route('Contacts_Index_Action') %>">Contacts</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main $id="content"></main>
|
||||
</div>
|
||||
</Define:Frontend_Layout>
|
||||
|
||||
SPA VS TRADITIONAL ROUTES
|
||||
Architecture Comparison:
|
||||
|
||||
Traditional Route:
|
||||
Every navigation:
|
||||
1. Browser sends HTTP request to server
|
||||
2. Controller executes, queries database
|
||||
3. Blade template renders full HTML page
|
||||
4. Server sends complete HTML to browser
|
||||
5. Browser parses and renders new page
|
||||
6. JavaScript re-initializes
|
||||
7. User sees page flash/reload
|
||||
|
||||
SPA Route:
|
||||
First navigation:
|
||||
1. Browser sends HTTP request to server
|
||||
2. Bootstrap controller renders SPA shell
|
||||
3. JavaScript initializes, discovers actions
|
||||
4. Router matches URL to action
|
||||
5. Action loads data via Ajax
|
||||
6. Action renders in persistent layout
|
||||
|
||||
Subsequent navigation:
|
||||
1. Router matches URL to action (client-side)
|
||||
2. New action loads data via Ajax
|
||||
3. New action renders in existing layout
|
||||
4. No server request, no page reload
|
||||
5. Layout persists (header/nav/footer stay)
|
||||
|
||||
Code Comparison:
|
||||
|
||||
Traditional:
|
||||
// Controller
|
||||
#[Route('/contacts')]
|
||||
public static function index(Request $request, array $params = []) {
|
||||
$contacts = Contact_Model::all();
|
||||
return rsx_view('Contacts_List', ['contacts' => $contacts]);
|
||||
}
|
||||
|
||||
// Blade view
|
||||
@extends('layout')
|
||||
@section('content')
|
||||
<h1>Contacts</h1>
|
||||
@foreach($contacts as $contact)
|
||||
<div>{{ $contact->name }}</div>
|
||||
@endforeach
|
||||
@endsection
|
||||
|
||||
SPA:
|
||||
// Bootstrap (once)
|
||||
#[SPA]
|
||||
public static function index(Request $request, array $params = []) {
|
||||
return rsx_view(SPA);
|
||||
}
|
||||
|
||||
// Ajax endpoint
|
||||
#[Ajax_Endpoint]
|
||||
public static function fetch_all(Request $request, array $params = []): array {
|
||||
return Contact_Model::all()->toArray();
|
||||
}
|
||||
|
||||
// Action
|
||||
@route('/contacts')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
class Contacts_Index_Action extends Spa_Action {
|
||||
async on_load() {
|
||||
this.data.contacts = await Frontend_Contacts_Controller.fetch_all();
|
||||
}
|
||||
}
|
||||
|
||||
// Template
|
||||
<Define:Contacts_Index_Action>
|
||||
<h1>Contacts</h1>
|
||||
<% for (let contact of this.data.contacts) { %>
|
||||
<div><%= contact.name %></div>
|
||||
<% } %>
|
||||
</Define:Contacts_Index_Action>
|
||||
|
||||
TROUBLESHOOTING
|
||||
SPA Not Initializing:
|
||||
- Verify bootstrap controller has #[SPA] attribute
|
||||
- Check controller returns rsx_view(SPA)
|
||||
- Ensure bundle includes SPA directory
|
||||
- Verify window.rsxapp.is_spa is true in HTML
|
||||
|
||||
Action Not Found:
|
||||
- Check @route() decorator matches URL pattern
|
||||
- Verify action extends Spa_Action
|
||||
- Ensure action file included in bundle
|
||||
- Run: php artisan rsx:manifest:build
|
||||
|
||||
Navigation Not Working:
|
||||
- Verify using Rsx.Route() not hardcoded URLs
|
||||
- Check @spa() decorator references correct bootstrap controller
|
||||
- Ensure layout has $id="content" element
|
||||
- Test Spa.dispatch() directly
|
||||
|
||||
Layout Not Persisting:
|
||||
- Verify all actions in module use same @layout()
|
||||
- Check layout template has $id="content"
|
||||
- Ensure not mixing SPA and traditional routes
|
||||
|
||||
this.args Empty:
|
||||
- Check route pattern includes :param for dynamic segments
|
||||
- Verify URL matches route pattern exactly
|
||||
- Remember query params (?key=value) also in this.args
|
||||
|
||||
Data Not Loading:
|
||||
- Verify Ajax endpoint has #[Ajax_Endpoint]
|
||||
- Check endpoint returns array, not view
|
||||
- Ensure await used in on_load() for async calls
|
||||
- Verify controller included in bundle
|
||||
|
||||
COMMON PATTERNS
|
||||
Conditional Navigation:
|
||||
// Navigate based on condition
|
||||
if (user.is_admin) {
|
||||
Spa.dispatch(Rsx.Route('Admin_Dashboard_Action'));
|
||||
} else {
|
||||
Spa.dispatch(Rsx.Route('User_Dashboard_Action'));
|
||||
}
|
||||
|
||||
Reloading Current Action:
|
||||
// Refresh current action data
|
||||
Spa.action.reload();
|
||||
|
||||
Form Submission with Redirect:
|
||||
async on_submit() {
|
||||
const data = this.vals();
|
||||
const result = await Controller.save(data);
|
||||
|
||||
if (result.errors) {
|
||||
Form_Utils.apply_form_errors(this.$, result.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Flash alert set server-side, redirect to view
|
||||
Spa.dispatch(result.redirect);
|
||||
}
|
||||
|
||||
Multiple Layouts:
|
||||
// Different layouts for different areas
|
||||
@layout('Admin_Layout')
|
||||
class Admin_Users_Action extends Spa_Action { }
|
||||
|
||||
@layout('Frontend_Layout')
|
||||
class Frontend_Contacts_Action extends Spa_Action { }
|
||||
|
||||
SEE ALSO
|
||||
controller(3) - Controller patterns and Ajax endpoints
|
||||
jqhtml(3) - Component lifecycle and templates
|
||||
routing(3) - URL generation and route patterns
|
||||
modals(3) - Modal dialogs in SPA context
|
||||
ajax_error_handling(3) - Error handling patterns
|
||||
267
app/RSpade/man/storage.txt
Executable file
267
app/RSpade/man/storage.txt
Executable file
@@ -0,0 +1,267 @@
|
||||
RSX_STORAGE(1) RSpade Manual RSX_STORAGE(1)
|
||||
|
||||
NAME
|
||||
Rsx_Storage - Scoped browser storage helper with automatic fallback
|
||||
|
||||
SYNOPSIS
|
||||
// Session storage (cleared on tab close)
|
||||
Rsx_Storage.session_set(key, value)
|
||||
Rsx_Storage.session_get(key)
|
||||
Rsx_Storage.session_remove(key)
|
||||
|
||||
// Local storage (persists across sessions)
|
||||
Rsx_Storage.local_set(key, value)
|
||||
Rsx_Storage.local_get(key)
|
||||
Rsx_Storage.local_remove(key)
|
||||
|
||||
DESCRIPTION
|
||||
Rsx_Storage provides safe, scoped access to browser sessionStorage and
|
||||
localStorage with automatic handling of unavailable storage, quota exceeded
|
||||
errors, and scope invalidation.
|
||||
|
||||
Key features:
|
||||
- Automatic scoping by session, user, site, and build version
|
||||
- Graceful degradation when storage unavailable (returns null)
|
||||
- Automatic quota management (clears and retries when full)
|
||||
- Scope validation (clears stale data on scope change)
|
||||
- Developer-friendly key format for easy inspection
|
||||
|
||||
SCOPING SYSTEM
|
||||
All storage keys are automatically scoped to prevent data leakage between:
|
||||
- Different sessions (window.rsxapp.session_hash - hashed, non-reversible)
|
||||
- Different users (window.rsxapp.user.id)
|
||||
- Different sites (window.rsxapp.site.id)
|
||||
- Different builds (window.rsxapp.build_key)
|
||||
|
||||
The scope is calculated by combining these values into a suffix:
|
||||
session_hash_user_id_site_id_build_key
|
||||
|
||||
This scope is stored in the special key `_rsx_scope_key`. On page load, if
|
||||
this key doesn't exist or doesn't match the current scope, all RSpade keys
|
||||
are cleared and the new scope is stored.
|
||||
|
||||
The session_hash is a server-generated HMAC hash of the session cookie using
|
||||
the application's encryption key, making it non-reversible while maintaining
|
||||
consistency per session.
|
||||
|
||||
Example scope suffix:
|
||||
a1b2c3d4e5f6_42_1_v2.1.0
|
||||
└─ session ──┘ │ │ └─ build
|
||||
user │
|
||||
site
|
||||
|
||||
KEY FORMAT
|
||||
Keys are stored with an `rsx::` namespace prefix, followed by the developer
|
||||
key, followed by the scope suffix:
|
||||
|
||||
rsx::developer_key::scope_suffix
|
||||
|
||||
Example:
|
||||
rsx::flash_queue::abc123def456_42_1_v2.1.0
|
||||
|
||||
The `rsx::` prefix serves two purposes:
|
||||
1. Identifies RSpade keys for safe selective clearing (scope changes, quota)
|
||||
2. Prevents collisions with other JavaScript libraries
|
||||
|
||||
This format allows developers to easily identify keys in browser developer
|
||||
tools while maintaining proper scoping and coexistence with third-party
|
||||
libraries. When inspecting storage, you'll see the `rsx::` prefix, your
|
||||
original key name, and the scope suffix.
|
||||
|
||||
STORAGE AVAILABILITY
|
||||
Rsx_Storage automatically detects if sessionStorage or localStorage are
|
||||
available. Storage may be unavailable due to:
|
||||
|
||||
- Private browsing mode (some browsers block storage)
|
||||
- Browser security settings
|
||||
- Storage quota set to 0
|
||||
- Browser bugs or incompatibilities
|
||||
|
||||
When storage is unavailable:
|
||||
- set() operations are silently ignored (no error)
|
||||
- get() operations return null
|
||||
- remove() operations are silently ignored
|
||||
|
||||
This allows the application to continue functioning even when storage is
|
||||
unavailable, as long as the stored data is non-critical.
|
||||
|
||||
SIZE LIMIT
|
||||
Individual values larger than 1 MB are automatically rejected and not stored.
|
||||
|
||||
When attempting to store data > 1 MB:
|
||||
- Operation is silently skipped (no error thrown)
|
||||
- Console warning logged with actual size
|
||||
- get() will return null for that key
|
||||
|
||||
This prevents quota issues and ensures browser storage remains performant.
|
||||
If you need to store large data, consider:
|
||||
- Storing server-side in database
|
||||
- Using IndexedDB for large client-side data
|
||||
- Splitting data into smaller chunks
|
||||
|
||||
QUOTA EXCEEDED HANDLING
|
||||
When a set() operation fails due to quota exceeded (storage full), Rsx_Storage
|
||||
automatically:
|
||||
|
||||
1. Clears only RSpade keys (keys starting with `rsx::`)
|
||||
2. Preserves other libraries' data
|
||||
3. Restores the _rsx_scope_key
|
||||
4. Retries the set() operation once
|
||||
|
||||
If the retry also fails, the error is logged and the operation is abandoned.
|
||||
|
||||
This ensures the application continues functioning even when storage is full,
|
||||
and minimizes impact on other JavaScript libraries sharing the same storage.
|
||||
Only RSpade's previously stored data will be lost.
|
||||
|
||||
SCOPE INVALIDATION
|
||||
Storage is automatically validated before every write operation. When the scope
|
||||
changes, only RSpade keys (starting with `rsx::`) are cleared, preserving other
|
||||
libraries' data.
|
||||
|
||||
Scope changes occur when:
|
||||
|
||||
- User logs in/out (user ID changes)
|
||||
- User switches sites (site ID changes)
|
||||
- Application is updated (build key changes)
|
||||
- Session changes (rsx cookie changes)
|
||||
|
||||
This prevents stale data from one context bleeding into another context.
|
||||
|
||||
Example: User A logs in, stores preferences, logs out. User B logs in on the
|
||||
same browser. User B sees clean RSpade storage, not User A's data. Other
|
||||
libraries' data (e.g., analytics cookies, third-party preferences) remains
|
||||
intact.
|
||||
|
||||
VOLATILE STORAGE WARNING
|
||||
Browser storage is VOLATILE and can be cleared at any time. Never store data
|
||||
critical to application functionality.
|
||||
|
||||
Storage can be lost due to:
|
||||
- User clearing browser data
|
||||
- Private browsing mode restrictions
|
||||
- Quota exceeded errors (automatic clear + retry)
|
||||
- Scope changes (logout, build update, session change)
|
||||
- Browser storage unavailable
|
||||
- Storage corruption or browser bugs
|
||||
|
||||
ONLY store data that is:
|
||||
- Cached for performance (can be re-fetched)
|
||||
- UI convenience state (non-essential)
|
||||
- Transient messages (flash alerts, notifications)
|
||||
|
||||
If data is REQUIRED for the application to function correctly, store it
|
||||
server-side in the database or PHP session.
|
||||
|
||||
EXAMPLES
|
||||
Cache API response data (sessionStorage)
|
||||
// Store cached data
|
||||
const users = await fetch_users();
|
||||
Rsx_Storage.session_set('cached_users', users);
|
||||
|
||||
// Retrieve cached data
|
||||
let users = Rsx_Storage.session_get('cached_users');
|
||||
if (!users) {
|
||||
users = await fetch_users();
|
||||
Rsx_Storage.session_set('cached_users', users);
|
||||
}
|
||||
|
||||
Persist UI preferences (localStorage)
|
||||
// Save theme preference
|
||||
Rsx_Storage.local_set('theme', 'dark');
|
||||
|
||||
// Load theme preference
|
||||
const theme = Rsx_Storage.local_get('theme') || 'light';
|
||||
apply_theme(theme);
|
||||
|
||||
Store flash alert queue (sessionStorage)
|
||||
// Queue persists across page navigation
|
||||
Rsx_Storage.session_set('flash_queue', messages);
|
||||
|
||||
// Restore queue on next page
|
||||
const messages = Rsx_Storage.session_get('flash_queue') || [];
|
||||
|
||||
Complex data structures
|
||||
// Automatically JSON serialized
|
||||
Rsx_Storage.local_set('user_prefs', {
|
||||
theme: 'dark',
|
||||
sidebar_collapsed: true,
|
||||
recent_items: [1, 2, 3]
|
||||
});
|
||||
|
||||
// Automatically JSON parsed
|
||||
const prefs = Rsx_Storage.local_get('user_prefs');
|
||||
console.log(prefs.theme); // 'dark'
|
||||
|
||||
Handling unavailable storage
|
||||
// Always check for null (storage unavailable or key not found)
|
||||
const cached = Rsx_Storage.session_get('data');
|
||||
if (cached) {
|
||||
use_cached_data(cached);
|
||||
} else {
|
||||
fetch_fresh_data();
|
||||
}
|
||||
|
||||
Removing stale data
|
||||
// Clean up when no longer needed
|
||||
Rsx_Storage.session_remove('temp_data');
|
||||
|
||||
SESSIONSTORAGE VS LOCALSTORAGE
|
||||
sessionStorage:
|
||||
- Cleared when tab/window closes
|
||||
- Not shared across tabs/windows
|
||||
- Use for: temporary data, current session state, flash messages
|
||||
|
||||
localStorage:
|
||||
- Persists across browser sessions
|
||||
- Shared across all tabs/windows of same origin
|
||||
- Use for: user preferences, cached data, long-term UI state
|
||||
|
||||
Both are scoped identically by Rsx_Storage (cookie, user, site, build).
|
||||
|
||||
MIGRATION FROM NATIVE STORAGE
|
||||
If you're currently using sessionStorage/localStorage directly:
|
||||
|
||||
Before:
|
||||
sessionStorage.setItem('my_key', JSON.stringify(data));
|
||||
const data = JSON.parse(sessionStorage.getItem('my_key'));
|
||||
sessionStorage.removeItem('my_key');
|
||||
|
||||
After:
|
||||
Rsx_Storage.session_set('my_key', data);
|
||||
const data = Rsx_Storage.session_get('my_key');
|
||||
Rsx_Storage.session_remove('my_key');
|
||||
|
||||
Benefits:
|
||||
- No manual JSON.stringify/parse
|
||||
- Automatic scoping (prevents data leakage)
|
||||
- Graceful handling of unavailable storage
|
||||
- Automatic quota management
|
||||
- Scope validation
|
||||
|
||||
DEBUGGING
|
||||
Inspect storage in browser developer tools:
|
||||
|
||||
1. Open Developer Tools (F12)
|
||||
2. Navigate to Application tab (Chrome) or Storage tab (Firefox)
|
||||
3. Expand Session Storage or Local Storage
|
||||
4. Look for keys matching: rsx::your_key::scope_suffix
|
||||
|
||||
The _rsx_scope_key shows the current active scope.
|
||||
All RSpade keys are prefixed with `rsx::` for easy identification.
|
||||
|
||||
Console logging:
|
||||
- Scope changes: "[Rsx_Storage] Scope changed, clearing RSpade keys only"
|
||||
- First use: "[Rsx_Storage] Initializing scope (first use)"
|
||||
- Quota exceeded: "[Rsx_Storage] Quota exceeded, clearing RSpade keys"
|
||||
- Keys cleared: "[Rsx_Storage] Cleared X RSpade keys"
|
||||
- Errors: "[Rsx_Storage] Failed to..."
|
||||
|
||||
FILE LOCATION
|
||||
/system/app/RSpade/Core/Js/Rsx_Storage.js
|
||||
|
||||
SEE ALSO
|
||||
Flash Alert system (uses Rsx_Storage for queue persistence)
|
||||
php artisan rsx:man flash_alert
|
||||
|
||||
RSPADE January 2025 RSX_STORAGE(1)
|
||||
@@ -1,212 +1,663 @@
|
||||
RSX TASK SYSTEM
|
||||
================
|
||||
|
||||
The RSX Task system provides a structured way to define and execute background
|
||||
tasks, similar to how Controllers handle HTTP requests but for CLI/background
|
||||
execution.
|
||||
NAME
|
||||
Tasks - Unified background task execution with scheduling and queueing
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Tasks are static methods in Service classes that can be executed from:
|
||||
- Command line (via `rsx:task:run`)
|
||||
- Internal PHP code (via `Task::internal()`)
|
||||
- Future: Queue systems, cron scheduling, progress tracking
|
||||
|
||||
## CREATING SERVICES
|
||||
|
||||
Services are classes that extend `Rsx_Service_Abstract` and live in:
|
||||
/rsx/services/
|
||||
|
||||
Example service structure:
|
||||
|
||||
<?php
|
||||
use App\RSpade\Core\Service\Rsx_Service_Abstract;
|
||||
|
||||
class My_Service extends Rsx_Service_Abstract
|
||||
SYNOPSIS
|
||||
Immediate CLI Execution
|
||||
-----------------------
|
||||
#[Task('Generate report')]
|
||||
public static function generate_report(Task_Instance $task, array $params = [])
|
||||
{
|
||||
#[Task('Description of what this task does')]
|
||||
public static function my_task(array $params = [])
|
||||
return ['status' => 'complete'];
|
||||
}
|
||||
|
||||
Run via: php artisan rsx:task:run Service_Name generate_report --param=value
|
||||
|
||||
Scheduled Tasks (Recurring)
|
||||
---------------------------
|
||||
#[Task('Clean thumbnails daily')]
|
||||
#[Schedule('0 3 * * *')] // 3am daily, cron syntax
|
||||
public static function clean_thumbnails(Task_Instance $task, array $params = [])
|
||||
{
|
||||
$task->log("Starting cleanup...");
|
||||
// ... cleanup logic
|
||||
}
|
||||
|
||||
Queued Tasks (Async from Application)
|
||||
--------------------------------------
|
||||
#[Task('Transcode video', queue: 'video', timeout: 3600)]
|
||||
public static function transcode(Task_Instance $task, array $params = [])
|
||||
{
|
||||
$task->set_status('progress', 0);
|
||||
$temp_dir = $task->get_temp_directory();
|
||||
|
||||
// ... transcoding logic
|
||||
|
||||
$task->set_status('progress', 100);
|
||||
return ['output_path' => '/storage/tasks/123/output.mp4'];
|
||||
}
|
||||
|
||||
// Dispatch from controller:
|
||||
$task_id = Task::dispatch('Video_Service', 'transcode', ['video_id' => 123]);
|
||||
|
||||
// Poll for status:
|
||||
$status = Task::status($task_id);
|
||||
|
||||
DESCRIPTION
|
||||
RSX provides a unified task execution system with three execution modes:
|
||||
|
||||
1. Immediate CLI - Run tasks directly via artisan command
|
||||
2. Scheduled - Recurring tasks run automatically on cron schedule
|
||||
3. Queued - Async tasks dispatched from application code with status tracking
|
||||
|
||||
Philosophy: Visual Basic simplicity - define tasks with attributes, the
|
||||
framework handles workers, scheduling, queueing, and cleanup automatically.
|
||||
|
||||
All tasks are Service methods with #[Task] attribute. Execution mode is
|
||||
determined by additional attributes (#[Schedule]) or invocation method
|
||||
(Task::dispatch()).
|
||||
|
||||
TASK EXECUTION MODES
|
||||
|
||||
Mode 1: Immediate CLI (Interactive)
|
||||
------------------------------------
|
||||
Run task immediately from command line. No database tracking, no timeout,
|
||||
output printed to console.
|
||||
|
||||
Usage:
|
||||
php artisan rsx:task:run Service_Name task_name --param=value
|
||||
|
||||
Characteristics:
|
||||
- Synchronous execution
|
||||
- No timeout enforcement
|
||||
- No database tracking
|
||||
- Output to STDOUT
|
||||
- $task parameter provided but minimal functionality
|
||||
|
||||
Mode 2: Scheduled (Recurring)
|
||||
------------------------------
|
||||
Tasks run automatically on a cron schedule. Tracked in database, debounced
|
||||
(no parallel execution), run as soon as possible after scheduled time.
|
||||
|
||||
Define with #[Schedule] attribute using cron syntax:
|
||||
#[Schedule('0 3 * * *')] // Daily at 3am
|
||||
#[Schedule('*/15 * * * *')] // Every 15 minutes
|
||||
#[Schedule('0 */6 * * *')] // Every 6 hours
|
||||
#[Schedule('0 2 * * 1')] // Mondays at 2am
|
||||
|
||||
Characteristics:
|
||||
- Automatic execution when scheduled time reached
|
||||
- Database tracking (next_run_at, started_at, completed_at)
|
||||
- Debounced - if already running, skip until next schedule
|
||||
- If execution missed (server down), runs as soon as possible
|
||||
- Default queue: 'scheduled' with max_workers: 1
|
||||
- Optional custom queue via queue parameter in #[Task]
|
||||
|
||||
Mode 3: Queued (Async Application)
|
||||
-----------------------------------
|
||||
Tasks dispatched from application code, run asynchronously with full
|
||||
status tracking and progress updates.
|
||||
|
||||
Dispatch from code:
|
||||
$task_id = Task::dispatch('Service_Name', 'task_name', $params);
|
||||
|
||||
Poll for status:
|
||||
$status = Task::status($task_id);
|
||||
// Returns: status, progress, log, result
|
||||
|
||||
Characteristics:
|
||||
- Asynchronous execution
|
||||
- Full database tracking
|
||||
- Progress and status updates
|
||||
- Timeout enforcement (default 30 minutes)
|
||||
- Temporary file support with auto-cleanup
|
||||
- Queue management with configurable concurrency
|
||||
|
||||
CREATING TASKS
|
||||
|
||||
Task Services
|
||||
-------------
|
||||
Tasks are static methods in Service classes extending Rsx_Service_Abstract.
|
||||
|
||||
Location: /rsx/services/
|
||||
|
||||
Basic structure:
|
||||
<?php
|
||||
namespace Rsx\Services;
|
||||
|
||||
use App\RSpade\Core\Service\Rsx_Service_Abstract;
|
||||
use App\RSpade\Core\Task\Task_Instance;
|
||||
|
||||
class My_Service extends Rsx_Service_Abstract
|
||||
{
|
||||
// Task implementation
|
||||
#[Task('Description of task')]
|
||||
public static function my_task(Task_Instance $task, array $params = [])
|
||||
{
|
||||
// Task implementation
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
Task Method Requirements
|
||||
------------------------
|
||||
- Must be public static methods
|
||||
- Must have #[Task('description')] attribute
|
||||
- Must accept (Task_Instance $task, array $params = [])
|
||||
- Should return data (stored as JSON in database for queued/scheduled)
|
||||
|
||||
Task Attribute
|
||||
--------------
|
||||
#[Task('Description', queue: 'name', timeout: seconds)]
|
||||
|
||||
Parameters:
|
||||
- description (required) - Human-readable task description
|
||||
- queue (optional) - Queue name for async execution (default: 'default')
|
||||
- timeout (optional) - Timeout in seconds (default: 1800 = 30 min)
|
||||
|
||||
Queue must be defined in config/rsx.php or error thrown.
|
||||
Timeout only enforced for queued/scheduled tasks, not immediate CLI.
|
||||
|
||||
Schedule Attribute
|
||||
------------------
|
||||
#[Schedule('cron_expression')]
|
||||
|
||||
Cron syntax (5 fields):
|
||||
* * * * *
|
||||
| | | | |
|
||||
| | | | +-- Day of week (0-7, 0=Sunday, 7=Sunday)
|
||||
| | | +---- Month (1-12)
|
||||
| | +------ Day of month (1-31)
|
||||
| +-------- Hour (0-23)
|
||||
+---------- Minute (0-59)
|
||||
|
||||
Examples:
|
||||
'0 3 * * *' - Daily at 3am
|
||||
'*/15 * * * *' - Every 15 minutes
|
||||
'0 */6 * * *' - Every 6 hours
|
||||
'0 9 * * 1-5' - Weekdays at 9am
|
||||
'30 2 1 * *' - First day of month at 2:30am
|
||||
|
||||
Tasks with #[Schedule] are automatically registered and run by task processor.
|
||||
|
||||
TASK INSTANCE API
|
||||
|
||||
The Task_Instance object provides methods for interacting with the task
|
||||
execution context. Available in all execution modes.
|
||||
|
||||
Logging
|
||||
-------
|
||||
$task->log($message)
|
||||
|
||||
Appends text to task status log. Each call adds timestamped entry.
|
||||
Stored in database for queued/scheduled tasks.
|
||||
|
||||
Example:
|
||||
$task->log("Starting video processing...");
|
||||
$task->log("Frame 100/500 processed");
|
||||
|
||||
Status Updates (Key-Value Pairs)
|
||||
---------------------------------
|
||||
$task->set_status($key, $value)
|
||||
|
||||
Sets arbitrary status data. Stored as JSON in database.
|
||||
Use for progress tracking, time estimates, current step, etc.
|
||||
|
||||
Example:
|
||||
$task->set_status('progress', 25);
|
||||
$task->set_status('time_remaining', 120);
|
||||
$task->set_status('current_step', 'encoding');
|
||||
$task->set_status('frames_processed', 100);
|
||||
|
||||
Client can read via Task::status($task_id)->get('progress')
|
||||
|
||||
Temporary Directory
|
||||
-------------------
|
||||
$temp_dir = $task->get_temp_directory()
|
||||
|
||||
Returns: storage/tasks/{task_id}/
|
||||
|
||||
Creates temporary directory for task file operations. Automatically
|
||||
cleaned up 1 hour after task completion (configurable).
|
||||
|
||||
Example:
|
||||
$temp = $task->get_temp_directory();
|
||||
$output = "{$temp}/output.mp4";
|
||||
FFmpeg::transcode($input, $output);
|
||||
|
||||
Set Custom Cleanup Time
|
||||
-----------------------
|
||||
$task->set_temp_expiration($seconds)
|
||||
|
||||
Override default 1 hour cleanup. Call after get_temp_directory().
|
||||
|
||||
Example:
|
||||
$temp = $task->get_temp_directory();
|
||||
$task->set_temp_expiration(7200); // Keep for 2 hours
|
||||
|
||||
DISPATCHING TASKS
|
||||
|
||||
Dispatch from Application Code
|
||||
-------------------------------
|
||||
$task_id = Task::dispatch($service, $task, $params);
|
||||
|
||||
Parameters:
|
||||
- $service - Service class name (e.g., 'Video_Service')
|
||||
- $task - Task method name (e.g., 'transcode')
|
||||
- $params - Associative array of parameters
|
||||
|
||||
Returns: Task ID (integer) for status polling
|
||||
|
||||
Example:
|
||||
$task_id = Task::dispatch('Video_Service', 'transcode', [
|
||||
'video_id' => 123,
|
||||
'quality' => 'high'
|
||||
]);
|
||||
|
||||
Auto-spawns worker if needed based on queue concurrency settings.
|
||||
|
||||
Check Task Status
|
||||
-----------------
|
||||
$status = Task::status($task_id);
|
||||
|
||||
Returns Task_Status object with:
|
||||
- $status->status - 'pending', 'running', 'completed', 'failed', 'stuck'
|
||||
- $status->get($key, $default) - Get status value
|
||||
- $status->log - Full text log
|
||||
- $status->result - Task return value (when completed)
|
||||
- $status->started_at - Timestamp
|
||||
- $status->completed_at - Timestamp
|
||||
|
||||
Example:
|
||||
$status = Task::status($task_id);
|
||||
if ($status->status === 'completed') {
|
||||
$output = $status->result['output_path'];
|
||||
} else {
|
||||
$progress = $status->get('progress', 0);
|
||||
}
|
||||
|
||||
QUEUE CONFIGURATION
|
||||
|
||||
Define queues in config/rsx.php:
|
||||
|
||||
'tasks' => [
|
||||
'global_max_workers' => 1,
|
||||
'default_timeout' => 1800, // 30 minutes
|
||||
'cleanup_stuck_after' => 1800,
|
||||
'temp_directory_default_ttl' => 3600, // 1 hour
|
||||
|
||||
'queues' => [
|
||||
'default' => ['max_workers' => 1],
|
||||
'scheduled' => ['max_workers' => 1],
|
||||
'video' => ['max_workers' => 2],
|
||||
'export' => ['max_workers' => 1],
|
||||
'email' => ['max_workers' => 5],
|
||||
],
|
||||
],
|
||||
|
||||
Queue Settings
|
||||
--------------
|
||||
global_max_workers - Total concurrent workers across ALL queues
|
||||
default_timeout - Default task timeout in seconds
|
||||
cleanup_stuck_after - Consider task stuck after this many seconds
|
||||
temp_directory_default_ttl - Auto-delete temp files after this long
|
||||
|
||||
Per-Queue Settings
|
||||
------------------
|
||||
max_workers - Maximum concurrent workers for this queue
|
||||
|
||||
Worker spawning logic:
|
||||
1. When task dispatched, check running workers for that queue
|
||||
2. If running < max_workers AND global limit not reached, spawn worker
|
||||
3. Worker processes queue FIFO until empty, then exits
|
||||
|
||||
Undefined Queue Error
|
||||
---------------------
|
||||
Using queue name not defined in config throws error at dispatch time.
|
||||
|
||||
TASK PROCESSOR COMMAND
|
||||
|
||||
Start Task Processor
|
||||
--------------------
|
||||
php artisan rsx:task:process [--queue=name]
|
||||
|
||||
This command:
|
||||
1. Cleans up stuck tasks (check PIDs, mark as stuck)
|
||||
2. Processes scheduled tasks that are due
|
||||
3. Checks all queues for pending tasks
|
||||
4. Spawns workers up to max_workers per queue
|
||||
5. Becomes a worker itself if under quota
|
||||
6. Processes tasks until queue empty, then exits
|
||||
|
||||
Options:
|
||||
--queue=name Process only specific queue
|
||||
|
||||
Running via Cron
|
||||
----------------
|
||||
Add to system crontab to run every minute:
|
||||
|
||||
* * * * * cd /var/www/html && php artisan rsx:task:process
|
||||
|
||||
This single cron entry handles:
|
||||
- All scheduled tasks
|
||||
- All queued tasks
|
||||
- Worker management
|
||||
- Stuck task cleanup
|
||||
|
||||
TASK DATABASE SCHEMA
|
||||
|
||||
Table: _tasks
|
||||
-------------
|
||||
- id - Task instance ID
|
||||
- server_id - Server identifier (default 1, for future distributed tasks)
|
||||
- service_name - Service class name
|
||||
- task_name - Task method name
|
||||
- queue_name - Queue name (NULL for scheduled tasks)
|
||||
- execution_mode - 'scheduled' or 'queued'
|
||||
- status - 'pending', 'running', 'completed', 'failed', 'stuck'
|
||||
- params - JSON parameters
|
||||
- result - JSON task return value
|
||||
- status_data - JSON status key-value pairs
|
||||
- status_log - Text log (append-only)
|
||||
- scheduled_at - When task should run (scheduled tasks)
|
||||
- next_run_at - Next execution time (recurring scheduled)
|
||||
- started_at - When task started processing
|
||||
- completed_at - When task finished
|
||||
- timeout_seconds - Task timeout
|
||||
- worker_pid - Process ID of worker
|
||||
- has_temp_directory - Boolean flag
|
||||
- temp_expires_at - When to cleanup temp files
|
||||
- created_at, updated_at
|
||||
|
||||
Advisory Locking
|
||||
----------------
|
||||
Database advisory locks coordinate queue access:
|
||||
- Lock key: task_queue_{queue_name}
|
||||
- Prevents race conditions in worker spawning
|
||||
- Atomic task selection and status updates
|
||||
|
||||
TASK LIFECYCLE
|
||||
|
||||
Scheduled Task Lifecycle
|
||||
------------------------
|
||||
1. Task registered with #[Schedule] attribute
|
||||
2. Entry created in _tasks with next_run_at
|
||||
3. Task processor checks scheduled tasks every minute
|
||||
4. When next_run_at <= now, task status set to 'pending'
|
||||
5. Worker picks up task, sets status to 'running', records worker_pid
|
||||
6. Task executes, updates logged and status tracked
|
||||
7. On completion, status set to 'completed', result stored
|
||||
8. next_run_at calculated for next execution based on cron
|
||||
|
||||
Queued Task Lifecycle
|
||||
---------------------
|
||||
1. Task::dispatch() called from application code
|
||||
2. Entry created in _tasks with status 'pending'
|
||||
3. Worker spawned if needed based on concurrency settings
|
||||
4. Worker picks up task atomically via advisory lock
|
||||
5. Status set to 'running', worker_pid recorded
|
||||
6. Task executes with progress updates via $task API
|
||||
7. On completion, status set to 'completed', result stored
|
||||
8. Temp directory marked for cleanup if created
|
||||
|
||||
Stuck Task Detection
|
||||
--------------------
|
||||
Task considered stuck if:
|
||||
- Status is 'running'
|
||||
- started_at + timeout_seconds < now
|
||||
|
||||
Task processor handles stuck tasks:
|
||||
1. Check if worker_pid still running
|
||||
2. If PID dead, mark task 'stuck'
|
||||
3. If PID alive but timeout exceeded, kill PID and mark 'stuck'
|
||||
4. Log stuck task for manual review
|
||||
|
||||
EXAMPLES
|
||||
|
||||
Example 1: Scheduled Cleanup Task
|
||||
----------------------------------
|
||||
<?php
|
||||
namespace Rsx\Services;
|
||||
|
||||
use App\RSpade\Core\Service\Rsx_Service_Abstract;
|
||||
use App\RSpade\Core\Task\Task_Instance;
|
||||
|
||||
class Cleanup_Service extends Rsx_Service_Abstract
|
||||
{
|
||||
#[Task('Clean old thumbnails daily at 3am')]
|
||||
#[Schedule('0 3 * * *')]
|
||||
public static function clean_thumbnails(Task_Instance $task, array $params = [])
|
||||
{
|
||||
$task->log("Starting thumbnail cleanup");
|
||||
|
||||
$deleted = 0;
|
||||
// ... cleanup logic
|
||||
|
||||
$task->log("Cleanup complete. Deleted {$deleted} files.");
|
||||
return ['deleted_count' => $deleted];
|
||||
}
|
||||
}
|
||||
|
||||
Example 2: Video Transcoding (Queued)
|
||||
--------------------------------------
|
||||
<?php
|
||||
namespace Rsx\Services;
|
||||
|
||||
use App\RSpade\Core\Service\Rsx_Service_Abstract;
|
||||
use App\RSpade\Core\Task\Task_Instance;
|
||||
|
||||
class Video_Service extends Rsx_Service_Abstract
|
||||
{
|
||||
#[Task('Transcode uploaded video', queue: 'video', timeout: 7200)]
|
||||
public static function transcode(Task_Instance $task, array $params = [])
|
||||
{
|
||||
$video_id = $params['video_id'];
|
||||
$video = Video_Model::find($video_id);
|
||||
|
||||
$task->log("Loading video {$video_id}");
|
||||
$task->set_status('progress', 0);
|
||||
|
||||
$temp = $task->get_temp_directory();
|
||||
$task->set_temp_expiration(7200); // Keep for 2 hours
|
||||
|
||||
$input = $video->get_storage_path();
|
||||
$output = "{$temp}/output.mp4";
|
||||
|
||||
FFmpeg::transcode($input, $output, function($progress) use ($task) {
|
||||
$task->set_status('progress', $progress);
|
||||
$task->log("Progress: {$progress}%");
|
||||
});
|
||||
|
||||
$task->set_status('progress', 100);
|
||||
$task->log("Transcoding complete");
|
||||
|
||||
$attachment = File_Attachment_Model::create_from_path($output);
|
||||
|
||||
return [
|
||||
'message' => 'Task completed',
|
||||
'data' => 'result data'
|
||||
'attachment_id' => $attachment->id,
|
||||
'output_path' => $output,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
## TASK METHODS
|
||||
// Controller dispatch:
|
||||
#[Ajax_Endpoint]
|
||||
public static function upload_video(Request $request, array $params = [])
|
||||
{
|
||||
$video = Video_Model::create([...]);
|
||||
|
||||
Tasks must:
|
||||
- Be public static methods
|
||||
- Have the #[Task('description')] attribute
|
||||
- Accept array $params = [] parameter
|
||||
- Return data (will be JSON-encoded for CLI output)
|
||||
$task_id = Task::dispatch('Video_Service', 'transcode', [
|
||||
'video_id' => $video->id
|
||||
]);
|
||||
|
||||
Task signature:
|
||||
public static function task_name(array $params = [])
|
||||
|
||||
## PARAMETER HANDLING
|
||||
|
||||
Services inherit parameter helpers from Rsx_Service_Abstract:
|
||||
|
||||
protected static function __param($params, $key, $default = null)
|
||||
protected static function __has_param($params, $key)
|
||||
|
||||
Example usage:
|
||||
$name = static::__param($params, 'name', 'Guest');
|
||||
if (static::__has_param($params, 'force')) {
|
||||
// ...
|
||||
return ['video_id' => $video->id, 'task_id' => $task_id];
|
||||
}
|
||||
|
||||
## PRE-TASK HOOK
|
||||
|
||||
Override pre_task() to run validation/auth before any task executes:
|
||||
|
||||
public static function pre_task(array $params = [])
|
||||
// Status polling:
|
||||
#[Ajax_Endpoint]
|
||||
public static function check_transcode(Request $request, array $params = [])
|
||||
{
|
||||
if (!RsxAuth::check()) {
|
||||
throw new \Exception("Authentication required");
|
||||
}
|
||||
return null; // Continue to task
|
||||
}
|
||||
|
||||
If pre_task() returns non-null, task execution halts and returns that value.
|
||||
|
||||
## LISTING TASKS
|
||||
|
||||
View all available tasks:
|
||||
php artisan rsx:task:list
|
||||
|
||||
Output shows services and their tasks with descriptions:
|
||||
Service_Test
|
||||
hello_world - Test task with no arguments
|
||||
greet - Test task with optional name parameter
|
||||
calculate - Test task with multiple parameters
|
||||
|
||||
## RUNNING TASKS
|
||||
|
||||
Execute a task from command line:
|
||||
php artisan rsx:task:run Service task_name
|
||||
|
||||
With parameters:
|
||||
php artisan rsx:task:run Service task_name --param=value
|
||||
php artisan rsx:task:run Service greet --name=John
|
||||
|
||||
Boolean flags:
|
||||
php artisan rsx:task:run Service task_name --force
|
||||
|
||||
JSON values (auto-parsed):
|
||||
php artisan rsx:task:run Service task_name --data='{"key":"value"}'
|
||||
|
||||
Debug mode (wrapped response):
|
||||
php artisan rsx:task:run Service task_name --debug
|
||||
|
||||
## OUTPUT MODES
|
||||
|
||||
Default mode - Raw JSON response (just the return value):
|
||||
{
|
||||
"message": "Task completed",
|
||||
"count": 42
|
||||
}
|
||||
|
||||
Debug mode - Wrapped response with success indicator:
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"message": "Task completed",
|
||||
"count": 42
|
||||
}
|
||||
}
|
||||
|
||||
## INTERNAL TASK CALLS
|
||||
|
||||
Call tasks from PHP code using Task::internal():
|
||||
|
||||
use App\RSpade\Core\Task\Task;
|
||||
|
||||
$result = Task::internal('Service_Name', 'task_name', [
|
||||
'param1' => 'value1',
|
||||
'param2' => 'value2'
|
||||
]);
|
||||
|
||||
This is useful for:
|
||||
- Composing complex tasks from simpler ones
|
||||
- Calling tasks from controllers
|
||||
- Background job processing
|
||||
|
||||
Example composition:
|
||||
#[Task('Run all seeders')]
|
||||
public static function seed_all(array $params = [])
|
||||
{
|
||||
$clients = Task::internal('Seeder_Service', 'seed_clients', $params);
|
||||
$contacts = Task::internal('Seeder_Service', 'seed_contacts', $params);
|
||||
$status = Task::status($params['task_id']);
|
||||
|
||||
return [
|
||||
'clients' => $clients,
|
||||
'contacts' => $contacts
|
||||
'status' => $status->status,
|
||||
'progress' => $status->get('progress', 0),
|
||||
'log' => $status->log,
|
||||
'result' => $status->result,
|
||||
];
|
||||
}
|
||||
|
||||
## ERROR HANDLING
|
||||
|
||||
All errors return JSON (never throws to stderr):
|
||||
Example 3: Export Generation (Queued)
|
||||
--------------------------------------
|
||||
#[Task('Generate CSV export', queue: 'export', timeout: 1800)]
|
||||
public static function generate_export(Task_Instance $task, array $params = [])
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message",
|
||||
"error_type": "Exception",
|
||||
"trace": "..."
|
||||
$task->log("Starting export generation");
|
||||
|
||||
$temp = $task->get_temp_directory();
|
||||
$csv_path = "{$temp}/export.csv";
|
||||
|
||||
$total = Client_Model::count();
|
||||
$processed = 0;
|
||||
|
||||
$fp = fopen($csv_path, 'w');
|
||||
fputcsv($fp, ['ID', 'Name', 'Email', 'Created']);
|
||||
|
||||
Client_Model::chunk(100, function($clients) use ($fp, &$processed, $total, $task) {
|
||||
foreach ($clients as $client) {
|
||||
fputcsv($fp, [
|
||||
$client->id,
|
||||
$client->name,
|
||||
$client->email,
|
||||
$client->created_at,
|
||||
]);
|
||||
$processed++;
|
||||
}
|
||||
|
||||
$progress = round(($processed / $total) * 100);
|
||||
$task->set_status('progress', $progress);
|
||||
$task->log("Processed {$processed}/{$total} clients");
|
||||
});
|
||||
|
||||
fclose($fp);
|
||||
|
||||
$attachment = File_Attachment_Model::create_from_path($csv_path);
|
||||
|
||||
return ['attachment_id' => $attachment->id];
|
||||
}
|
||||
|
||||
Exit codes:
|
||||
- 0: Success
|
||||
- 1: Error
|
||||
BUILT-IN SCHEDULED TASKS
|
||||
|
||||
## ATTRIBUTE CONFLICTS
|
||||
The framework includes built-in scheduled tasks in app/RSpade/Core/Tasks/:
|
||||
|
||||
A method cannot have multiple execution type attributes. These conflict:
|
||||
- #[Route] (HTTP routes)
|
||||
- #[Ajax_Endpoint] (Ajax endpoints)
|
||||
- #[Task] (CLI tasks)
|
||||
Cleanup_Service::cleanup_old_tasks
|
||||
-----------------------------------
|
||||
Removes completed/failed tasks older than configured retention period.
|
||||
Runs daily at 2am.
|
||||
|
||||
The manifest build will fail if these are mixed on the same method.
|
||||
Cleanup_Service::cleanup_temp_directories
|
||||
------------------------------------------
|
||||
Deletes temporary task directories past their expiration time.
|
||||
Runs hourly.
|
||||
|
||||
## USE CASES
|
||||
These tasks are automatically registered and require no user configuration.
|
||||
|
||||
Tasks are ideal for:
|
||||
- Database seeders
|
||||
- Data migrations
|
||||
- Report generation
|
||||
- Batch processing
|
||||
- Maintenance operations
|
||||
- Background jobs
|
||||
- Scheduled operations
|
||||
TROUBLESHOOTING
|
||||
|
||||
Example services:
|
||||
Seeder_Service - Database seeding
|
||||
Report_Service - Generate reports
|
||||
Cleanup_Service - Maintenance tasks
|
||||
Import_Service - Data imports
|
||||
Export_Service - Data exports
|
||||
Task Not Running
|
||||
----------------
|
||||
Problem: Scheduled task not executing
|
||||
|
||||
## FUTURE FEATURES (NOT YET IMPLEMENTED)
|
||||
Checks:
|
||||
- Is cron configured? (* * * * * php artisan rsx:task:process)
|
||||
- Check _tasks table for next_run_at timestamp
|
||||
- Run manually: php artisan rsx:task:process
|
||||
- Check Laravel logs for errors
|
||||
|
||||
The Task system is designed to support future enhancements:
|
||||
- Queue integration (dispatch tasks to Redis/database queue)
|
||||
- Cron scheduling (#[Schedule] attribute)
|
||||
- Progress tracking (long-running tasks report progress)
|
||||
- Task history (log of all task executions)
|
||||
- Task dependencies (ensure X runs before Y)
|
||||
- Parallel execution (run multiple tasks concurrently)
|
||||
Worker Not Spawning
|
||||
--------------------
|
||||
Problem: Dispatched task stays 'pending'
|
||||
|
||||
## EXAMPLES
|
||||
Checks:
|
||||
- Is global_max_workers reached?
|
||||
- Is queue max_workers reached?
|
||||
- Is task processor running? (cron or manual)
|
||||
- Check _tasks table for worker_pid
|
||||
- Try: php artisan rsx:task:process --queue=queuename
|
||||
|
||||
See example services:
|
||||
/rsx/services/service_test.php - Basic task examples
|
||||
/rsx/services/seeder_service.php - Database seeding examples
|
||||
Task Stuck
|
||||
----------
|
||||
Problem: Task shows 'stuck' status
|
||||
|
||||
Test tasks:
|
||||
php artisan rsx:task:list
|
||||
php artisan rsx:task:run Service_Test hello_world
|
||||
php artisan rsx:task:run Service_Test greet --name=Brian
|
||||
php artisan rsx:task:run Service_Test calculate --a=10 --b=5 --op=multiply
|
||||
Causes:
|
||||
- Worker process killed/crashed
|
||||
- Timeout exceeded
|
||||
- Server restarted
|
||||
|
||||
Recovery:
|
||||
- Task processor automatically detects stuck tasks
|
||||
- Stuck tasks marked for manual review
|
||||
- Check status_log for clues
|
||||
- Re-dispatch if needed
|
||||
|
||||
Undefined Queue Error
|
||||
---------------------
|
||||
Problem: Error when dispatching task
|
||||
|
||||
Solution:
|
||||
Define queue in config/rsx.php:
|
||||
'queues' => [
|
||||
'myqueue' => ['max_workers' => 1],
|
||||
],
|
||||
|
||||
BEST PRACTICES
|
||||
|
||||
1. Choose Right Execution Mode
|
||||
- Immediate CLI: Development, testing, one-off operations
|
||||
- Scheduled: Recurring maintenance, cleanup, reports
|
||||
- Queued: User-triggered async work, long-running processes
|
||||
|
||||
2. Set Appropriate Timeouts
|
||||
- Default 30 minutes is generous
|
||||
- Video transcoding: 2+ hours
|
||||
- Exports: 30 minutes
|
||||
- Email sending: 5 minutes
|
||||
|
||||
3. Use Descriptive Logging
|
||||
- Log major steps, not every iteration
|
||||
- Include useful context (IDs, counts, progress)
|
||||
- Helps debugging stuck/failed tasks
|
||||
|
||||
4. Queue Naming
|
||||
- Use semantic names: 'video', 'export', 'email'
|
||||
- Group similar workloads
|
||||
- Set concurrency based on resource usage
|
||||
|
||||
5. Temp Directory Cleanup
|
||||
- Only create if needed
|
||||
- Set expiration based on use case
|
||||
- Return attachment_id instead of path when possible
|
||||
|
||||
6. Status Updates
|
||||
- Update progress regularly (every 5-10%)
|
||||
- Include time estimates when possible
|
||||
- Use consistent key names across tasks
|
||||
|
||||
7. Error Handling
|
||||
- Throw exceptions for failures
|
||||
- Task status automatically set to 'failed'
|
||||
- Exception message stored in result
|
||||
- Don't catch and swallow errors
|
||||
|
||||
FUTURE FEATURES (NOT YET IMPLEMENTED)
|
||||
|
||||
The task system is designed to support future enhancements:
|
||||
|
||||
- Distributed tasks across multiple servers (server_id support)
|
||||
- Metrics and monitoring (task duration, failure rates, queue depth)
|
||||
|
||||
SEE ALSO
|
||||
service.txt - Service class documentation
|
||||
ajax.txt - Ajax endpoint system
|
||||
config_rsx.txt - RSX configuration reference
|
||||
|
||||
504
app/RSpade/man/thumbnails.txt
Executable file
504
app/RSpade/man/thumbnails.txt
Executable file
@@ -0,0 +1,504 @@
|
||||
THUMBNAILS
|
||||
==========
|
||||
|
||||
NAME
|
||||
Thumbnails - Two-tier cached thumbnail system with named presets and dynamic sizes
|
||||
|
||||
SYNOPSIS
|
||||
Named Preset Thumbnails (Recommended)
|
||||
-------------------------------------
|
||||
// Define presets in config/rsx.php
|
||||
'thumbnails' => [
|
||||
'presets' => [
|
||||
'profile' => ['type' => 'cover', 'width' => 200, 'height' => 200],
|
||||
'gallery' => ['type' => 'fit', 'width' => 400, 'height' => 300],
|
||||
],
|
||||
],
|
||||
|
||||
// Use in templates
|
||||
<img src="<?= $attachment->get_thumbnail_url_preset('profile') ?>" />
|
||||
|
||||
Dynamic Thumbnails (Ad-hoc Sizes)
|
||||
----------------------------------
|
||||
<img src="<?= $attachment->get_thumbnail_url('cover', 200, 200) ?>" />
|
||||
<img src="<?= $attachment->get_thumbnail_url('fit', 400) ?>" />
|
||||
|
||||
Artisan Commands
|
||||
----------------
|
||||
php artisan rsx:thumbnails:stats
|
||||
php artisan rsx:thumbnails:clean [--preset] [--dynamic]
|
||||
php artisan rsx:thumbnails:generate [--preset=profile]
|
||||
|
||||
DESCRIPTION
|
||||
RSX provides a two-tier thumbnail caching system designed to prevent cache
|
||||
pollution from arbitrary thumbnail sizes while maintaining flexibility for
|
||||
developers.
|
||||
|
||||
IMPORTANT: Thumbnails automatically scale to 2x requested dimensions for
|
||||
HiDPI/Retina displays, but cap at 66% of source image dimensions to avoid
|
||||
excessive upscaling. The output aspect ratio always matches requested
|
||||
dimensions, but actual resolution may be lower than requested.
|
||||
|
||||
Unlike traditional approaches that generate thumbnails on-demand without
|
||||
limits, RSX distinguishes between:
|
||||
|
||||
1. Preset Thumbnails - Developer-defined named sizes for application use
|
||||
- Managed via scheduled cleanup tasks
|
||||
- Protected from spam/abuse
|
||||
- Optimized for cache warming
|
||||
|
||||
2. Dynamic Thumbnails - Ad-hoc sizes requested via URL parameters
|
||||
- Synchronous quota enforcement after each generation
|
||||
- LRU eviction (oldest files deleted first)
|
||||
- For edge cases and development
|
||||
|
||||
Philosophy: Define thumbnail sizes once in config, reference by name
|
||||
throughout your application. This prevents users from generating thousands
|
||||
of arbitrary-sized thumbnails and polluting your cache.
|
||||
|
||||
Storage Location: storage/rsx-thumbnails/preset/ and .../dynamic/
|
||||
These directories survive rsx:clean and are managed separately via
|
||||
rsx:thumbnails:clean command (schedule this for periodic cleanup).
|
||||
|
||||
PRESET THUMBNAILS
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
Define named presets in /system/config/rsx.php:
|
||||
|
||||
'thumbnails' => [
|
||||
'presets' => [
|
||||
'profile' => ['type' => 'cover', 'width' => 200, 'height' => 200],
|
||||
'gallery' => ['type' => 'fit', 'width' => 400, 'height' => 300],
|
||||
'icon_small' => ['type' => 'cover', 'width' => 32, 'height' => 32],
|
||||
'icon_large' => ['type' => 'cover', 'width' => 64, 'height' => 64],
|
||||
],
|
||||
'quotas' => [
|
||||
'preset_max_bytes' => 100 * 1024 * 1024, // 100MB
|
||||
'dynamic_max_bytes' => 50 * 1024 * 1024, // 50MB
|
||||
],
|
||||
'touch_on_read' => true,
|
||||
'touch_interval' => 600, // 10 minutes
|
||||
],
|
||||
|
||||
Thumbnail Types
|
||||
---------------
|
||||
cover - Fills dimensions completely, cropping excess (like background-size: cover)
|
||||
fit - Maintains aspect ratio within dimensions, transparent background
|
||||
|
||||
Usage in Code
|
||||
-------------
|
||||
// Get preset thumbnail URL
|
||||
$url = $attachment->get_thumbnail_url_preset('profile');
|
||||
|
||||
// Use in Blade templates
|
||||
<img src="<?= $user->get_attachment('profile_photo')->get_thumbnail_url_preset('profile') ?>" />
|
||||
|
||||
// Use in jqhtml templates
|
||||
<img src="<%= attachment.get_thumbnail_url_preset('gallery') %>" />
|
||||
|
||||
Quota Management
|
||||
----------------
|
||||
Preset thumbnails are enforced via scheduled task (not synchronous):
|
||||
|
||||
Schedule this command to run periodically (e.g., daily):
|
||||
php artisan rsx:thumbnails:clean --preset
|
||||
|
||||
This deletes oldest preset thumbnails until under quota limit.
|
||||
|
||||
Cache Warming
|
||||
-------------
|
||||
Pre-generate preset thumbnails for all attachments:
|
||||
|
||||
php artisan rsx:thumbnails:generate
|
||||
php artisan rsx:thumbnails:generate --preset=profile
|
||||
php artisan rsx:thumbnails:generate --key=abc123def456
|
||||
|
||||
Useful after deployment or when adding new presets.
|
||||
|
||||
DYNAMIC THUMBNAILS
|
||||
|
||||
Usage
|
||||
-----
|
||||
For edge cases where preset sizes don't fit:
|
||||
|
||||
$url = $attachment->get_thumbnail_url('cover', 200, 200);
|
||||
$url = $attachment->get_thumbnail_url('fit', 400); // Height optional
|
||||
|
||||
<img src="<?= $attachment->get_thumbnail_url('cover', 150, 150) ?>" />
|
||||
|
||||
Quota Enforcement
|
||||
-----------------
|
||||
Dynamic thumbnails are enforced SYNCHRONOUSLY after each new thumbnail:
|
||||
- Check total directory size
|
||||
- If over quota, delete oldest files (by mtime) until under limit
|
||||
- This happens automatically, no scheduled task needed
|
||||
|
||||
When to Use
|
||||
-----------
|
||||
- Development/testing with non-standard sizes
|
||||
- Edge cases where preset sizes don't fit
|
||||
- One-off thumbnail requirements
|
||||
|
||||
When NOT to Use
|
||||
---------------
|
||||
- Regular application thumbnails (use presets instead)
|
||||
- User-facing features (prevents cache pollution)
|
||||
|
||||
CACHING BEHAVIOR
|
||||
|
||||
Cache Storage
|
||||
-------------
|
||||
Preset: storage/rsx-thumbnails/preset/{preset_name}_{hash}_{ext}.webp
|
||||
Dynamic: storage/rsx-thumbnails/dynamic/{type}_{w}x{h}_{hash}_{ext}.webp
|
||||
|
||||
All thumbnails are WebP format for optimal size/quality balance.
|
||||
|
||||
Cache Hits
|
||||
----------
|
||||
When thumbnail exists:
|
||||
1. Attempt to open file (handles race condition if deleted)
|
||||
2. Touch mtime if enabled and >10 minutes old (LRU tracking)
|
||||
3. Serve from disk with 1-year cache headers
|
||||
|
||||
Cache Misses
|
||||
------------
|
||||
When thumbnail doesn't exist:
|
||||
1. Generate via Imagick (images) or icon-based (non-images)
|
||||
2. Save to cache directory
|
||||
3. Enforce quota (dynamic only)
|
||||
4. Serve from memory (don't re-read from disk)
|
||||
|
||||
LRU Eviction
|
||||
------------
|
||||
Both preset and dynamic thumbnails use LRU (Least Recently Used):
|
||||
- mtime touched on cache hit (configurable interval)
|
||||
- Oldest files (by mtime) deleted first when over quota
|
||||
- Prevents deleting frequently-accessed thumbnails
|
||||
|
||||
Race Condition Handling
|
||||
-----------------------
|
||||
Between file existence check and open, file might be deleted by quota
|
||||
enforcement. The system handles this gracefully:
|
||||
- fopen() failure triggers regeneration
|
||||
- No error shown to user
|
||||
- Thumbnail generated and served immediately
|
||||
|
||||
ARTISAN COMMANDS
|
||||
|
||||
rsx:thumbnails:stats
|
||||
--------------------
|
||||
Display usage statistics for both preset and dynamic caches.
|
||||
|
||||
Output includes:
|
||||
- File count and total size
|
||||
- Quota usage percentage
|
||||
- Oldest and newest thumbnail ages
|
||||
- Breakdown by preset name (for preset thumbnails)
|
||||
|
||||
Example:
|
||||
$ php artisan rsx:thumbnails:stats
|
||||
|
||||
Preset Thumbnails:
|
||||
Files: 1,234
|
||||
Total Size: 45.2 MB / 100 MB (45.2%)
|
||||
Oldest: 30 days ago
|
||||
Newest: 2 minutes ago
|
||||
|
||||
Dynamic Thumbnails:
|
||||
Files: 567
|
||||
Total Size: 38.7 MB / 50 MB (77.4%)
|
||||
Oldest: 7 days ago
|
||||
Newest: 5 seconds ago
|
||||
|
||||
Breakdown by preset:
|
||||
profile: 234 files, 12.3 MB
|
||||
gallery: 567 files, 28.9 MB
|
||||
|
||||
rsx:thumbnails:clean
|
||||
--------------------
|
||||
Enforce quotas by deleting oldest files until under limit.
|
||||
|
||||
Options:
|
||||
--preset Clean only preset thumbnails
|
||||
--dynamic Clean only dynamic thumbnails
|
||||
--all Clean both (default if no options specified)
|
||||
|
||||
Examples:
|
||||
php artisan rsx:thumbnails:clean
|
||||
php artisan rsx:thumbnails:clean --preset
|
||||
php artisan rsx:thumbnails:clean --dynamic
|
||||
|
||||
Schedule this for preset thumbnails:
|
||||
// In your scheduler (future feature)
|
||||
$schedule->command('rsx:thumbnails:clean --preset')->daily();
|
||||
|
||||
rsx:thumbnails:generate
|
||||
-----------------------
|
||||
Pre-generate preset thumbnails for attachments.
|
||||
|
||||
Options:
|
||||
--preset=name Generate specific preset for all attachments
|
||||
--key=xyz Generate thumbnails for specific attachment
|
||||
--all Generate all presets for all attachments (default)
|
||||
|
||||
Examples:
|
||||
php artisan rsx:thumbnails:generate
|
||||
php artisan rsx:thumbnails:generate --preset=profile
|
||||
php artisan rsx:thumbnails:generate --key=abc123def456
|
||||
|
||||
Use cases:
|
||||
- Cache warming after deployment
|
||||
- Regenerating after changing preset dimensions
|
||||
- Background generation for new uploads
|
||||
|
||||
CONFIGURATION OPTIONS
|
||||
|
||||
thumbnails.presets
|
||||
------------------
|
||||
Array of named presets with type, width, and height.
|
||||
|
||||
Format: ['name' => ['type' => 'cover'|'fit', 'width' => int, 'height' => int]]
|
||||
|
||||
Example:
|
||||
'presets' => [
|
||||
'profile' => ['type' => 'cover', 'width' => 200, 'height' => 200],
|
||||
'gallery' => ['type' => 'fit', 'width' => 400, 'height' => 300],
|
||||
],
|
||||
|
||||
thumbnails.quotas.preset_max_bytes
|
||||
----------------------------------
|
||||
Maximum bytes for preset thumbnail directory.
|
||||
Enforced via scheduled rsx:thumbnails:clean --preset.
|
||||
|
||||
Default: 100 * 1024 * 1024 (100MB)
|
||||
|
||||
thumbnails.quotas.dynamic_max_bytes
|
||||
-----------------------------------
|
||||
Maximum bytes for dynamic thumbnail directory.
|
||||
Enforced synchronously after each new dynamic thumbnail.
|
||||
|
||||
Default: 50 * 1024 * 1024 (50MB)
|
||||
|
||||
thumbnails.max_dynamic_size
|
||||
---------------------------
|
||||
Maximum dimension limit for dynamic thumbnails (base resolution).
|
||||
This value represents the pre-2x-scaling dimension limit. The actual
|
||||
generated thumbnail will be double this value.
|
||||
|
||||
Example: 800 allows requests up to 800x800, generates 1600x1600
|
||||
|
||||
Preset thumbnails have no enforced maximum (developer-controlled).
|
||||
|
||||
Default: 800
|
||||
NOTE: Application configuration - set in config/rsx.php, not .env
|
||||
|
||||
thumbnails.touch_on_read
|
||||
------------------------
|
||||
Whether to touch file mtime on cache hit (for LRU tracking).
|
||||
|
||||
Default: true
|
||||
Environment: THUMBNAILS_TOUCH_ON_READ
|
||||
|
||||
thumbnails.touch_interval
|
||||
-------------------------
|
||||
Only touch mtime if file is older than this many seconds.
|
||||
Prevents excessive filesystem writes while maintaining LRU accuracy.
|
||||
|
||||
Default: 600 (10 minutes)
|
||||
Environment: THUMBNAILS_TOUCH_INTERVAL
|
||||
|
||||
DIMENSION LIMITS AND RESOLUTION SCALING
|
||||
|
||||
HiDPI/Retina Display Support
|
||||
----------------------------
|
||||
All thumbnails automatically scale to 2x requested dimensions for sharp
|
||||
display on high-DPI screens. However, to prevent excessive upscaling:
|
||||
|
||||
- Requested dimensions are multiplied by 2x
|
||||
- If 2x dimensions exceed 66% of source image size on either axis,
|
||||
the source image dimensions are used instead
|
||||
- Aspect ratio is preserved by cropping to requested proportions
|
||||
|
||||
Examples:
|
||||
Request: 100x100 from 500x500 source
|
||||
Generated: 200x200 (2x scaling applied)
|
||||
|
||||
Request: 200x200 from 500x500 source
|
||||
66% threshold: 330x330 (500 * 0.66)
|
||||
2x target: 400x400 (exceeds 330)
|
||||
Generated: 500x500 (uses source, crops to 1:1 aspect ratio)
|
||||
|
||||
Request: 100x100 from 150x150 source
|
||||
66% threshold: 99x99
|
||||
2x target: 200x200 (exceeds 99)
|
||||
Generated: 150x150 (uses source dimensions)
|
||||
|
||||
This ensures thumbnails are always sharp on HiDPI displays while avoiding
|
||||
quality loss from excessive upscaling of small source images.
|
||||
|
||||
Dynamic Thumbnail Constraints
|
||||
------------------------------
|
||||
Dynamic thumbnails enforce additional dimension limits to prevent abuse:
|
||||
- Minimum: 10x10 pixels (before 2x scaling)
|
||||
- Maximum: Configurable via max_dynamic_size (default 800x800)
|
||||
- After 2x scaling: becomes 1600x1600 with default setting
|
||||
|
||||
Preset thumbnails have no enforced limits (developer-controlled).
|
||||
|
||||
To change the dynamic maximum:
|
||||
// In config/rsx.php
|
||||
'thumbnails' => [
|
||||
'max_dynamic_size' => 1200, // 1200 base → 2400 after 2x
|
||||
]
|
||||
|
||||
Output Resolution Not Guaranteed
|
||||
---------------------------------
|
||||
The actual output resolution may differ from requested dimensions due to:
|
||||
- 2x scaling for HiDPI displays
|
||||
- 66% source dimension threshold
|
||||
- Maximum dimension constraints (default 1600x1600 for dynamic)
|
||||
|
||||
However, the aspect ratio ALWAYS matches requested dimensions. Plan your
|
||||
layouts accordingly - use CSS to constrain display size if needed.
|
||||
|
||||
NON-IMAGE FILES
|
||||
|
||||
Thumbnails for non-image files (PDFs, documents, etc.) use icon-based
|
||||
rendering. The system generates a WebP thumbnail containing the
|
||||
appropriate file type icon.
|
||||
|
||||
Icons are provided by File_Attachment_Icons class and include:
|
||||
- Brand-specific icons (PDF, Photoshop, Illustrator)
|
||||
- Generic category icons (document, video, audio, archive)
|
||||
|
||||
FILESYSTEM STRUCTURE
|
||||
|
||||
storage/rsx-thumbnails/
|
||||
├── preset/
|
||||
│ ├── profile_abc123_jpg.webp
|
||||
│ ├── profile_def456_png.webp
|
||||
│ └── gallery_abc123_jpg.webp
|
||||
└── dynamic/
|
||||
├── cover_200x200_abc123_jpg.webp
|
||||
└── fit_400x300_def456_png.webp
|
||||
|
||||
Filename Format:
|
||||
- Preset: {preset_name}_{storage_hash}_{extension}.webp
|
||||
- Dynamic: {type}_{width}x{height}_{storage_hash}_{extension}.webp
|
||||
|
||||
Why include extension in filename?
|
||||
Handles edge case where identical file content uploaded with different
|
||||
extensions (e.g., zip vs docx) should render different icons.
|
||||
|
||||
BEST PRACTICES
|
||||
|
||||
1. Use Preset Thumbnails Everywhere
|
||||
Define all thumbnail sizes your application needs as presets.
|
||||
This prevents cache pollution and enables quota management.
|
||||
|
||||
2. Name Presets Semantically
|
||||
Use names like 'profile', 'gallery', 'thumbnail' instead of dimensions.
|
||||
This makes code more maintainable when you change sizes.
|
||||
|
||||
3. Schedule Preset Cleanup
|
||||
Add rsx:thumbnails:clean --preset to your scheduler (daily/weekly).
|
||||
This prevents preset cache from growing unbounded.
|
||||
|
||||
4. Monitor Cache Usage
|
||||
Run rsx:thumbnails:stats periodically to monitor quota usage.
|
||||
Adjust quotas if needed.
|
||||
|
||||
5. Warm Cache After Deployment
|
||||
Run rsx:thumbnails:generate after deploying to pre-generate thumbnails.
|
||||
This prevents first-visitor slowness.
|
||||
|
||||
6. Use Dynamic Sparingly
|
||||
Reserve dynamic thumbnails for edge cases and development.
|
||||
Production features should use presets.
|
||||
|
||||
TROUBLESHOOTING
|
||||
|
||||
Preset Not Found Error
|
||||
----------------------
|
||||
Error: "Thumbnail preset 'xyz' not defined"
|
||||
|
||||
Solution: Add preset to config/rsx.php:
|
||||
|
||||
'thumbnails' => [
|
||||
'presets' => [
|
||||
'xyz' => ['type' => 'cover', 'width' => 200, 'height' => 200],
|
||||
],
|
||||
],
|
||||
|
||||
Cache Growing Too Large
|
||||
-----------------------
|
||||
Problem: Thumbnail cache exceeds quota
|
||||
|
||||
Solutions:
|
||||
- Lower quota in config/rsx.php
|
||||
- Run rsx:thumbnails:clean manually
|
||||
- Schedule rsx:thumbnails:clean for automatic cleanup
|
||||
- Reduce number of presets
|
||||
- Decrease touch_interval to improve LRU accuracy
|
||||
|
||||
Thumbnails Not Generating
|
||||
--------------------------
|
||||
Problem: Blank images or 404 errors
|
||||
|
||||
Checks:
|
||||
- ImageMagick installed? (php -m | grep imagick)
|
||||
- File exists on disk? (check attachment->file_storage->get_full_path())
|
||||
- Directory writable? (storage/rsx-thumbnails/ permissions)
|
||||
- Check Laravel logs for exceptions
|
||||
|
||||
Slow Initial Load
|
||||
-----------------
|
||||
Problem: First thumbnail request is slow
|
||||
|
||||
Solution: Pre-generate thumbnails
|
||||
php artisan rsx:thumbnails:generate
|
||||
|
||||
Thumbnails Deleted Unexpectedly
|
||||
--------------------------------
|
||||
Problem: Dynamic thumbnails disappear
|
||||
|
||||
Explanation: Dynamic quota enforcement deletes oldest files when over limit.
|
||||
This is intentional to prevent abuse.
|
||||
|
||||
Solutions:
|
||||
- Convert to preset thumbnail if regularly needed
|
||||
- Increase dynamic_max_bytes quota
|
||||
- Reduce dynamic thumbnail usage
|
||||
|
||||
FUTURE ENHANCEMENTS
|
||||
|
||||
Client-Side DPR Detection
|
||||
-------------------------
|
||||
Current limitation: Server always generates at 2x resolution regardless of
|
||||
actual client display capabilities.
|
||||
|
||||
Planned enhancement: JavaScript helpers that detect window.devicePixelRatio
|
||||
and automatically calculate display-appropriate dimensions.
|
||||
|
||||
Proposed API:
|
||||
// JavaScript - automatically adjusts for devicePixelRatio
|
||||
const url = attachment.get_thumbnail_url_for_display('cover', 200, 200);
|
||||
|
||||
// On 1x display: requests 200x200 (server generates 400x400)
|
||||
// On 2x display: requests 200x200 (server generates 400x400)
|
||||
// On 3x display: requests 300x300 (server generates 600x600)
|
||||
|
||||
Benefits:
|
||||
- Optimized bandwidth usage per device
|
||||
- Better cache utilization
|
||||
- Sharper images on 3x displays
|
||||
- Smaller images for 1x displays
|
||||
|
||||
This would require client-side JavaScript helpers integrated with the
|
||||
File_Attachment_Model fetch system.
|
||||
|
||||
SEE ALSO
|
||||
file_upload.txt - File attachment and upload system
|
||||
storage.txt - Storage directory organization
|
||||
config_rsx.txt - RSX configuration reference
|
||||
Reference in New Issue
Block a user