Move small tasks from wishlist to todo, update npm packages Replace #[Auth] attributes with manual auth checks and code quality rule Remove on_jqhtml_ready lifecycle method from framework Complete ACL system with 100-based role indexing and /dev/acl tester WIP: ACL system implementation with debug instrumentation Convert rsx:check JS linting to RPC socket server Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature Reorganize wishlists: priority order, mark sublayouts complete, add email Update model_fetch docs: mark MVP complete, fix enum docs, reorganize Comprehensive documentation overhaul: clarity, compression, and critical rules Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null() Add JS ORM relationship lazy-loading and fetch array handling Add JS ORM relationship fetching and CRUD documentation Fix ORM hydration and add IDE resolution for Base_* model stubs Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework Enhance JS ORM infrastructure and add Json_Tree class name badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
362 lines
12 KiB
Plaintext
Executable File
362 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);
|
|
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
|