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); console.log(product.status_label); // Enum properties populated console.log(Product_Model.STATUS_ACTIVE); // Static enum constants Returns instantiated JS model class with enum properties and optional custom methods. 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) TODO - Real-time notifications: Broadcast model changes to connected clients - Revision tracking: Auto-increment revision column via database trigger on update 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