Standardize settings file naming and relocate documentation files Fix code quality violations from rsx:check Reorganize user_management directory into logical subdirectories Move Quill Bundle to core and align with Tom Select pattern Simplify Site Settings page to focus on core site information Complete Phase 5: Multi-tenant authentication with login flow and site selection Add route query parameter rule and synchronize filename validation logic Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs Implement filename convention rule and resolve VS Code auto-rename conflict Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns Implement RPC server architecture for JavaScript parsing WIP: Add RPC server infrastructure for JS parsing (partial implementation) Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation Add JQHTML-CLASS-01 rule and fix redundant class names Improve code quality rules and resolve violations Remove legacy fatal error format in favor of unified 'fatal' error type Filter internal keys from window.rsxapp output Update button styling and comprehensive form/modal documentation Add conditional fly-in animation for modals Fix non-deterministic bundle compilation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
358 lines
12 KiB
Plaintext
Executable File
358 lines
12 KiB
Plaintext
Executable File
RSpade Models (Rsx_Model_Abstract)
|
|
|
|
OVERVIEW
|
|
|
|
All RSX models must extend Rsx_Model_Abstract, which provides:
|
|
- Enum system with magic properties and methods
|
|
- Mass assignment prevention (explicit field assignment only)
|
|
- Eager loading prevention (explicit queries only)
|
|
- Ajax ORM fetch() system
|
|
- Automatic boolean casting for TINYINT(1) columns
|
|
- Relationship management via #[Relationship] attribute
|
|
|
|
ENUM SYSTEM
|
|
|
|
Define enums using public static $enums property:
|
|
|
|
public static $enums = [
|
|
'status_id' => [
|
|
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'order' => 1],
|
|
2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive', 'order' => 2]
|
|
],
|
|
'role_id' => [
|
|
1 => ['constant' => 'ROLE_ADMIN', 'label' => 'Administrator'],
|
|
2 => ['constant' => 'ROLE_USER', 'label' => 'User']
|
|
]
|
|
];
|
|
|
|
Magic Properties (Instance):
|
|
- $model->status_id_label - Get label for current enum value
|
|
- $model->status_id_constant - Get constant name for current value
|
|
- $model->status_id_order - Get sort order for current value
|
|
- $model->status_id_enum_val - Get all properties for current value
|
|
|
|
Magic Methods (Static):
|
|
- Model::status_id_enum() - Get all enum definitions (sorted by 'order')
|
|
- Model::status_id_enum_select() - Get key/label pairs for dropdowns
|
|
- Model::status_id_enum_ids() - Get all possible enum values
|
|
|
|
Enum properties in $enums can include any custom keys (label, order, constant,
|
|
color, icon, etc.). All properties are exported to JavaScript via toArray().
|
|
|
|
Optional 'selectable' property (default true) controls dropdown visibility:
|
|
2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false]
|
|
|
|
MASS ASSIGNMENT PREVENTION
|
|
|
|
Mass assignment is explicitly prohibited. Use explicit field assignment:
|
|
|
|
// CORRECT
|
|
$model = new User_Model();
|
|
$model->name = $request->input('name');
|
|
$model->email = $request->input('email');
|
|
$model->save();
|
|
|
|
// WRONG - throws MassAssignmentException
|
|
$model = User_Model::create(['name' => 'John', 'email' => 'john@example.com']);
|
|
$model->fill(['name' => 'John']);
|
|
$model->update(['name' => 'John']);
|
|
|
|
Blocked methods: fill(), forceFill(), create(), firstOrCreate(), firstOrNew(),
|
|
updateOrCreate(), update()
|
|
|
|
EAGER LOADING PREVENTION
|
|
|
|
All forms of eager loading throw exceptions. Use explicit queries:
|
|
|
|
// WRONG - throws exception
|
|
$users = User_Model::with('posts')->get();
|
|
$user->load('posts');
|
|
|
|
// CORRECT - explicit queries
|
|
$users = User_Model::all();
|
|
foreach ($users as $user) {
|
|
$posts = Post_Model::where('user_id', $user->id)->get();
|
|
}
|
|
|
|
Rationale: Prevents recursive loading chains that accidentally cascade across
|
|
the entire database. Future optimization will use caching strategies instead.
|
|
|
|
See: php artisan rsx:man rspade (DATABASE PHILOSOPHY section)
|
|
|
|
AJAX ORM (Model Fetch System)
|
|
|
|
Models can opt-in to client-side fetching by implementing fetch() with
|
|
#[Ajax_Endpoint_Model_Fetch] attribute:
|
|
|
|
#[Ajax_Endpoint_Model_Fetch]
|
|
public static function fetch($id)
|
|
{
|
|
// Check authorization
|
|
if (!RsxAuth::check()) {
|
|
return false;
|
|
}
|
|
|
|
// Fetch single record
|
|
$model = static::find($id);
|
|
return $model ?: false;
|
|
}
|
|
|
|
JavaScript usage:
|
|
const product = await Product_Model.fetch(1);
|
|
const products = await Product_Model.fetch([1, 2, 3]);
|
|
|
|
Framework automatically splits array IDs into individual fetch() calls for
|
|
security (no mass fetching).
|
|
|
|
See: php artisan rsx:man model_fetch
|
|
|
|
RELATIONSHIPS
|
|
|
|
Define relationships using #[Relationship] attribute to prevent Laravel from
|
|
auto-detecting non-relationship methods:
|
|
|
|
#[Relationship]
|
|
public function posts()
|
|
{
|
|
return $this->hasMany(Post_Model::class);
|
|
}
|
|
|
|
Only methods with #[Relationship] are considered relationships. This prevents
|
|
methods like is_active() from being mistaken for relationships.
|
|
|
|
Note: Eager loading is still blocked. Relationships are supported but must be
|
|
loaded with explicit queries.
|
|
|
|
AUTOMATIC BOOLEAN CASTING
|
|
|
|
TINYINT(1) columns are automatically cast to boolean without manual $casts
|
|
definition. Framework consults manifest database metadata to detect boolean
|
|
columns at runtime.
|
|
|
|
Manual $casts entries take precedence over automatic detection.
|
|
|
|
NEVER EXPORT FIELDS
|
|
|
|
Exclude sensitive fields from toArray() and Ajax responses:
|
|
|
|
protected $neverExport = ['password', 'api_token', 'remember_token'];
|
|
|
|
Fields in $neverExport are automatically removed before JavaScript export.
|
|
|
|
UTILITY METHODS
|
|
|
|
Static Methods:
|
|
- get_table_static() - Get table name without instantiating model
|
|
- getColumns() - Get array of column names for model's table
|
|
- hasColumn($column) - Check if column exists
|
|
- get_relationships() - Get array of relationship method names
|
|
- clear_table_cache() - Clear query cache for table
|
|
- clear_all_caches() - Clear all query caches
|
|
|
|
Instance Methods:
|
|
- make_new() - Create new model instance (recommended over new Model())
|
|
- get_mass_assignment_example() - Get helpful example code for this model
|
|
|
|
MODEL ORGANIZATION WITH TRAITS
|
|
|
|
Large models can be split into domain-specific trait files. This organization
|
|
strategy is primarily designed for models, though traits can be used elsewhere
|
|
in RSX applications.
|
|
|
|
Naming Convention:
|
|
|
|
Trait files should be prefixed with the model name they elaborate on:
|
|
Users_Model -> Users_Model_Authentication (trait for auth methods)
|
|
Users_Model -> Users_Model_Relationships (trait for relationship methods)
|
|
Users_Model -> Users_Model_Permissions (trait for permission logic)
|
|
|
|
This makes it immediately clear which model a trait belongs to.
|
|
|
|
Organization Strategy:
|
|
|
|
Group methods by problem domain within separate trait files:
|
|
- Authentication/authorization logic
|
|
- Query methods for specific use cases
|
|
- Relationship definitions
|
|
- Business logic for specific features
|
|
- Computed properties and accessors
|
|
|
|
Example Implementation:
|
|
|
|
// rsx/models/traits/users_model_authentication.php
|
|
trait Users_Model_Authentication
|
|
{
|
|
public static function find_by_email(string $email)
|
|
{
|
|
return static::where('email', $email)->first();
|
|
}
|
|
|
|
public static function find_by_api_token(string $token)
|
|
{
|
|
return static::where('api_token', $token)->first();
|
|
}
|
|
|
|
public function verify_password(string $password): bool
|
|
{
|
|
return password_verify($password, $this->password);
|
|
}
|
|
}
|
|
|
|
// rsx/models/traits/users_model_relationships.php
|
|
trait Users_Model_Relationships
|
|
{
|
|
#[Relationship]
|
|
public function posts()
|
|
{
|
|
return $this->hasMany(Posts_Model::class);
|
|
}
|
|
|
|
#[Relationship]
|
|
public function profile()
|
|
{
|
|
return $this->hasOne(User_Profiles_Model::class);
|
|
}
|
|
}
|
|
|
|
// rsx/models/users_model.php
|
|
class Users_Model extends Rsx_Model_Abstract
|
|
{
|
|
use Users_Model_Authentication;
|
|
use Users_Model_Relationships;
|
|
|
|
protected $table = 'users';
|
|
public static $enums;
|
|
protected $neverExport = ['password', 'api_token'];
|
|
}
|
|
|
|
Manifest Integration:
|
|
|
|
The manifest system automatically discovers and indexes methods from traits:
|
|
- Trait files are loaded before classes during manifest build
|
|
- Methods from traits appear in the model's metadata
|
|
- IDE helpers correctly locate trait method definitions
|
|
- Code quality rules validate trait methods same as class methods
|
|
|
|
Benefits:
|
|
|
|
- Keep models focused and maintainable
|
|
- Group related functionality by problem domain
|
|
- Easier to navigate large models with hundreds of methods
|
|
- Clear separation between different aspects of model behavior
|
|
- Traits are discovered and validated automatically by manifest
|
|
|
|
SITE-BASED MULTI-TENANCY
|
|
|
|
RSpade provides automatic multi-tenant data isolation through model inheritance.
|
|
|
|
Model Types:
|
|
|
|
Rsx_Model_Abstract
|
|
- Base model for global/system-wide data
|
|
- No automatic site scoping
|
|
- Examples: countries, regions, system settings
|
|
|
|
Rsx_Site_Model_Abstract
|
|
- Base model for tenant-specific data
|
|
- Automatic site_id scoping on all operations
|
|
- Examples: users, clients, projects, tasks
|
|
|
|
How It Works:
|
|
|
|
Models extending Rsx_Site_Model_Abstract automatically:
|
|
- Filter all queries by current session site_id
|
|
- Set site_id on all new records from session
|
|
- Prevent changing site_id on existing records
|
|
- Prevent cross-site data access (throws fatal error)
|
|
- Require site_id column in database table
|
|
|
|
Database Schema:
|
|
CREATE TABLE projects (
|
|
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
site_id BIGINT NOT NULL, -- Added by migration normalization
|
|
name VARCHAR(255) NOT NULL,
|
|
description LONGTEXT,
|
|
INDEX idx_site_id (site_id) -- Added automatically
|
|
)
|
|
|
|
The site_id column is added automatically during migration normalization
|
|
if the model extends Rsx_Site_Model_Abstract.
|
|
|
|
Usage:
|
|
|
|
class Project_Model extends Rsx_Site_Model_Abstract
|
|
{
|
|
protected $table = 'projects';
|
|
}
|
|
|
|
// In controller with session site_id = 1
|
|
$projects = Project_Model::all();
|
|
// SELECT * FROM projects WHERE site_id = 1
|
|
|
|
$project = new Project_Model();
|
|
$project->name = 'New Project';
|
|
$project->save();
|
|
// INSERT INTO projects (site_id, name) VALUES (1, 'New Project')
|
|
|
|
Security Enforcement:
|
|
|
|
// Attempt to change site_id - FATAL ERROR
|
|
$project = Project_Model::find(1); // site_id = 1
|
|
$project->site_id = 2;
|
|
$project->save();
|
|
// FATAL: Attempted to change site_id from 1 to 2. Changing site_id is not allowed.
|
|
|
|
// Attempt to save wrong site record - FATAL ERROR
|
|
// (If somehow loaded a record from site 2 while session is site 1)
|
|
$project->save();
|
|
// FATAL: Cross-site saves are not allowed.
|
|
|
|
Admin Operations (Bypass Site Scoping):
|
|
|
|
// Access all sites (admin operations only)
|
|
$all_projects = Project_Model::without_site_scope(function () {
|
|
return Project_Model::all();
|
|
});
|
|
|
|
// Query specific site
|
|
$site_2_projects = Project_Model::without_site_scope(function () {
|
|
return Project_Model::where('site_id', 2)->get();
|
|
});
|
|
|
|
Single-Site Applications:
|
|
|
|
Even single-site applications use site_id = 1 for all records.
|
|
The architecture is identical - just happens to use only one site.
|
|
Easy upgrade path if multi-tenancy needed later.
|
|
|
|
How site_id is Determined:
|
|
|
|
site_id comes from the session (Session::get_site_id()):
|
|
- User logs in → login_user_id set in session
|
|
- User has site memberships → site_id set in session
|
|
- All queries for site models filtered by this site_id
|
|
- Changing session site_id changes which data is accessible
|
|
|
|
See: php artisan rsx:man auth (for complete authentication/site selection flow)
|
|
|
|
NAMING CONVENTIONS
|
|
|
|
Tables:
|
|
- Plural snake_case (users, user_logins, product_categories)
|
|
- Always include 'id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY'
|
|
|
|
Columns:
|
|
- snake_case for all column names
|
|
- Foreign keys must suffix with '_id' (user_id, site_id)
|
|
- Booleans must prefix with 'is_' (is_active, is_published)
|
|
- Timestamps must suffix with '_at' (created_at, updated_at, deleted_at)
|
|
- Use BIGINT for all integers, TINYINT(1) for booleans only
|
|
- All text columns use UTF-8 (utf8mb4_unicode_ci collation)
|
|
|
|
SEE ALSO
|
|
|
|
php artisan rsx:man model_fetch - Ajax ORM fetch system
|
|
php artisan rsx:man model_normalization - Database normalization process
|
|
php artisan rsx:man enums - Complete enum system documentation
|
|
php artisan rsx:man migrations - Migration system and safety
|