Add polymorphic type references system for efficient integer-based storage

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-27 23:10:30 +00:00
parent c1485ccbdb
commit 4db772b132
14 changed files with 1071 additions and 87 deletions

View File

@@ -407,6 +407,24 @@ abstract class Rsx_Model_Abstract extends Model
*/
public static $enums;
/**
* Polymorphic type reference columns
*
* Columns listed here will automatically be cast using Rsx_Type_Ref_Cast,
* which transparently converts between class name strings (PHP) and
* integer IDs (database).
*
* Example:
* protected static $type_ref_columns = ['fileable_type'];
*
* With this:
* $attachment->fileable_type = 'Contact_Model'; // Stored as integer
* echo $attachment->fileable_type; // Returns 'Contact_Model'
*
* @var array
*/
protected static $type_ref_columns = [];
/**
* Get the casts array with automatic type casting based on database schema
*
@@ -470,6 +488,14 @@ abstract class Rsx_Model_Abstract extends Model
}
}
// Apply type ref casts for polymorphic type columns
// These columns store integer IDs but expose class name strings
foreach (static::$type_ref_columns as $column) {
if (!isset($casts[$column])) {
$casts[$column] = \App\RSpade\Core\Database\TypeRefs\Rsx_Type_Ref_Cast::class;
}
}
return $casts;
}
@@ -1130,7 +1156,8 @@ EXAMPLE;
*/
public function get_attachment(string $category)
{
return \App\RSpade\Core\Files\File_Attachment_Model::where('fileable_type', get_class($this))
// Use simple class name (type_ref system stores simple names, not FQCNs)
return \App\RSpade\Core\Files\File_Attachment_Model::where('fileable_type', class_basename($this))
->where('fileable_id', $this->id)
->where('fileable_category', $category)
->first();
@@ -1147,7 +1174,8 @@ EXAMPLE;
*/
public function get_attachments(string $category)
{
return \App\RSpade\Core\Files\File_Attachment_Model::where('fileable_type', get_class($this))
// Use simple class name (type_ref system stores simple names, not FQCNs)
return \App\RSpade\Core\Files\File_Attachment_Model::where('fileable_type', class_basename($this))
->where('fileable_id', $this->id)
->where('fileable_category', $category)
->get();

View File

@@ -0,0 +1,102 @@
# Type References System
## Overview
The Type Refs system provides transparent mapping between class name strings and integer IDs for polymorphic relationship columns. This allows efficient BIGINT storage in the database while developers work with human-readable class names.
## How It Works
1. **Database Storage**: Polymorphic `*_type` columns store BIGINT integers
2. **PHP Interface**: Developers work with class name strings (`'Contact_Model'`)
3. **Automatic Mapping**: `Rsx_Type_Ref_Cast` converts between the two
4. **Auto-Registration**: New class names are registered on first use
## Components
### Type_Ref_Registry
Static registry that manages the class name → ID mapping:
```php
// Get ID for class (auto-creates if new)
$id = Type_Ref_Registry::class_to_id('Contact_Model');
// Get class name for ID
$class = Type_Ref_Registry::id_to_class($id);
// Check existence
Type_Ref_Registry::has_class('Contact_Model');
Type_Ref_Registry::has_id(42);
// Refresh cache after manual DB changes
Type_Ref_Registry::refresh();
```
### Rsx_Type_Ref_Cast
Eloquent cast that handles transparent conversion:
```php
// In model, declare type_ref_columns
protected static $type_ref_columns = ['fileable_type'];
// Then use normally - cast handles conversion
$attachment->fileable_type = 'Contact_Model'; // Stored as integer
echo $attachment->fileable_type; // Reads as 'Contact_Model'
```
### Type_Ref_Model
Simple Eloquent model for the `_type_refs` table. Rarely used directly.
## Database Schema
```sql
_type_refs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
class_name VARCHAR(255) UNIQUE,
table_name VARCHAR(255) NULL,
created_at TIMESTAMP(3),
updated_at TIMESTAMP(3)
)
```
## Caching Strategy
1. **Redis Cache**: Build-scoped, invalidates on manifest rebuild
2. **Memory Cache**: Static properties survive within a request
3. **Database**: Source of truth, queried on cache miss
## Laravel Integration
The `Type_Ref_Registry::register_morph_map()` method is called during framework boot to register all type refs with Laravel's `Relation::morphMap()`. This enables Laravel's `morphTo()` relationships to work correctly.
## Adding New Polymorphic Models
1. Add `$type_ref_columns` to the model:
```php
protected static $type_ref_columns = ['attachable_type'];
```
2. Create migration to convert column:
```php
DB::statement("UPDATE my_table SET attachable_type = NULL");
DB::statement("ALTER TABLE my_table MODIFY attachable_type BIGINT NULL");
```
3. Use simple class names in code:
```php
$model->attachable_type = class_basename($related);
```
## Important Notes
- **Simple Names Only**: Always use simple class names (`Contact_Model`), never FQCNs
- **Auto-Registration**: New classes are auto-registered when first used
- **Transparent**: After setup, code works identically to VARCHAR storage
- **Laravel Compatible**: Works with `morphTo()`, `morphMany()`, etc.
## Reference
- `php artisan rsx:man polymorphic` - Full documentation
- `/system/app/RSpade/upstream_changes/type_refs_12_27.txt` - Migration guide

View File

@@ -0,0 +1,72 @@
<?php
namespace App\RSpade\Core\Database\TypeRefs;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use App\RSpade\Core\Database\TypeRefs\Type_Ref_Registry;
/**
* Rsx_Type_Ref_Cast - Eloquent cast for polymorphic type reference columns
*
* This cast provides transparent conversion between class name strings (PHP)
* and integer IDs (database) for polymorphic type columns.
*
* When reading from database:
* - Integer ID is converted to class name string
* - Null values remain null
* - Invalid IDs throw an exception
*
* When writing to database:
* - Class name string is converted to integer ID
* - New classes are auto-registered in _type_refs
* - Null values remain null
* - Invalid class names throw an exception
*
* Usage in model:
* protected static $type_ref_columns = ['fileable_type'];
*
* The cast is automatically applied by Rsx_Model_Abstract based on the
* $type_ref_columns declaration.
*/
#[Instantiatable]
class Rsx_Type_Ref_Cast implements CastsAttributes
{
/**
* Transform the attribute from the underlying model values.
*
* @param Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return string|null
*/
public function get(Model $model, string $key, mixed $value, array $attributes): ?string
{
if ($value === null || $value === '') {
return null;
}
// Convert integer ID to class name
return Type_Ref_Registry::id_to_class((int) $value);
}
/**
* Transform the attribute to its underlying model values.
*
* @param Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return int|null
*/
public function set(Model $model, string $key, mixed $value, array $attributes): ?int
{
if ($value === null || $value === '') {
return null;
}
// Convert class name to integer ID (auto-creates if needed)
return Type_Ref_Registry::class_to_id((string) $value);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\RSpade\Core\Database\TypeRefs;
use Illuminate\Database\Eloquent\Model;
/**
* Type_Ref_Model - Maps class names to integer IDs for polymorphic relationships
*
* This model represents entries in the _type_refs table, which provides a
* registry of class name to integer ID mappings used for efficient polymorphic
* column storage.
*
* The table is managed automatically by Type_Ref_Registry - you should not
* typically interact with this model directly.
*
* @property int $id
* @property string $class_name
* @property string|null $table_name
* @property string $created_at
* @property string $updated_at
*/
class Type_Ref_Model extends Model
{
protected $table = '_type_refs';
protected $guarded = [];
public $timestamps = true;
}

View File

@@ -0,0 +1,312 @@
<?php
namespace App\RSpade\Core\Database\TypeRefs;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use App\RSpade\Core\Cache\RsxCache;
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
use App\RSpade\Core\Manifest\Manifest;
/**
* Type_Ref_Registry - Manages polymorphic type reference mappings
*
* This registry provides a transparent mapping between class names (strings)
* and integer IDs for efficient polymorphic column storage. It handles:
*
* - Loading the type refs map from database/cache
* - Auto-creating new type ref entries when new classes are used
* - Lookups in both directions (class ID, ID class)
* - Registering the morph map with Laravel for relationship compatibility
*
* The registry uses a two-tier caching strategy:
* 1. Redis cache (if available) - shared across requests
* 2. PHP memory - loaded once per request
*
* Usage:
* $id = Type_Ref_Registry::class_to_id('Contact_Model');
* $class = Type_Ref_Registry::id_to_class($id);
*/
class Type_Ref_Registry
{
/**
* In-memory cache of the type refs map (class_name => id)
* @var array|null
*/
protected static $map_class_to_id = null;
/**
* In-memory cache of reverse map (id => class_name)
* @var array|null
*/
protected static $map_id_to_class = null;
/**
* Cache key for Redis storage (build-scoped - invalidates on manifest rebuild)
*/
protected const CACHE_KEY = 'type_refs_map';
/**
* Get the integer ID for a class name
*
* If the class doesn't exist in the registry, it will be auto-created
* after validating that it's a valid Rsx_Model subclass.
*
* @param string $class_name Simple class name (e.g., "Contact_Model")
* @return int The type ref ID
* @throws RuntimeException If class doesn't exist or isn't a valid model
*/
public static function class_to_id(string $class_name): int
{
static::_ensure_loaded();
// Check if already in map
if (isset(static::$map_class_to_id[$class_name])) {
return static::$map_class_to_id[$class_name];
}
// Auto-create new entry
return static::_create_type_ref($class_name);
}
/**
* Get the class name for an integer ID
*
* @param int $id The type ref ID
* @return string The simple class name
* @throws RuntimeException If ID doesn't exist in the registry
*/
public static function id_to_class(int $id): string
{
static::_ensure_loaded();
if (!isset(static::$map_id_to_class[$id])) {
throw new RuntimeException(
"Type ref ID {$id} not found in registry. " .
"The referenced model class may have been removed from the codebase."
);
}
return static::$map_id_to_class[$id];
}
/**
* Check if a class name exists in the registry
*
* @param string $class_name
* @return bool
*/
public static function has_class(string $class_name): bool
{
static::_ensure_loaded();
return isset(static::$map_class_to_id[$class_name]);
}
/**
* Check if an ID exists in the registry
*
* @param int $id
* @return bool
*/
public static function has_id(int $id): bool
{
static::_ensure_loaded();
return isset(static::$map_id_to_class[$id]);
}
/**
* Get the full type refs map (class_name => id)
*
* @return array
*/
public static function get_map(): array
{
static::_ensure_loaded();
return static::$map_class_to_id;
}
/**
* Register all type refs with Laravel's morph map
*
* This should be called during framework boot to enable Laravel's
* polymorphic relationship methods (morphTo, morphMany, etc.)
*/
public static function register_morph_map(): void
{
static::_ensure_loaded();
// Laravel's morph map expects class_name => FQCN
// But we use simple class names throughout, so we need to resolve them
$morph_map = [];
foreach (static::$map_class_to_id as $class_name => $id) {
// Try to resolve the fully qualified class name
$fqcn = Manifest::php_resolve_class($class_name);
if ($fqcn) {
$morph_map[$class_name] = $fqcn;
}
}
if (!empty($morph_map)) {
Relation::morphMap($morph_map);
}
}
/**
* Clear all caches and reload from database
*
* Call this after manually modifying the _type_refs table
*/
public static function refresh(): void
{
// Clear Redis cache
RsxCache::delete(static::CACHE_KEY);
static::$map_class_to_id = null;
static::$map_id_to_class = null;
static::_ensure_loaded();
}
/**
* Ensure the type refs map is loaded into memory
*/
protected static function _ensure_loaded(): void
{
if (static::$map_class_to_id !== null) {
return;
}
// Check if _type_refs table exists (might not during initial migration)
if (!Schema::hasTable('_type_refs')) {
static::$map_class_to_id = [];
static::$map_id_to_class = [];
return;
}
// Try to load from Redis cache first (build-scoped)
$cached = RsxCache::get(static::CACHE_KEY);
if ($cached !== null && is_array($cached)) {
static::$map_class_to_id = $cached;
static::$map_id_to_class = array_flip($cached);
return;
}
// Load from database
static::_load_from_database();
}
/**
* Load the type refs map from the database
*/
protected static function _load_from_database(): void
{
$rows = DB::table('_type_refs')->get(['id', 'class_name']);
static::$map_class_to_id = [];
static::$map_id_to_class = [];
foreach ($rows as $row) {
static::$map_class_to_id[$row->class_name] = (int) $row->id;
static::$map_id_to_class[(int) $row->id] = $row->class_name;
}
// Cache the map (build-scoped)
RsxCache::set(static::CACHE_KEY, static::$map_class_to_id, RsxCache::HOUR);
}
/**
* Create a new type ref entry for a class
*
* @param string $class_name
* @return int The new ID
* @throws RuntimeException If class is invalid
*/
protected static function _create_type_ref(string $class_name): int
{
// Validate that the class exists and is a valid model
static::_validate_class($class_name);
// Get the table name for the model
$table_name = static::_get_table_name($class_name);
// Insert with duplicate key handling (race condition protection)
// Use INSERT IGNORE to handle concurrent requests
DB::statement(
"INSERT IGNORE INTO _type_refs (class_name, table_name, created_at, updated_at) VALUES (?, ?, NOW(3), NOW(3))",
[$class_name, $table_name]
);
// Fetch the ID (whether we just inserted or it already existed)
$row = DB::table('_type_refs')->where('class_name', $class_name)->first(['id']);
if (!$row) {
throw new RuntimeException("Failed to create type ref for class: {$class_name}");
}
$id = (int) $row->id;
// Update in-memory cache
static::$map_class_to_id[$class_name] = $id;
static::$map_id_to_class[$id] = $class_name;
// Update Redis cache
RsxCache::set(static::CACHE_KEY, static::$map_class_to_id, RsxCache::HOUR);
// Update Laravel morph map
$fqcn = Manifest::php_resolve_class($class_name);
if ($fqcn) {
Relation::morphMap([$class_name => $fqcn]);
}
return $id;
}
/**
* Validate that a class name is a valid Rsx model
*
* @param string $class_name
* @throws RuntimeException If invalid
*/
protected static function _validate_class(string $class_name): void
{
// Resolve the fully qualified class name
$fqcn = Manifest::php_resolve_class($class_name);
if (!$fqcn) {
throw new RuntimeException(
"Cannot create type ref for '{$class_name}': Class not found in manifest. " .
"Ensure the class exists and extends Rsx_Model_Abstract."
);
}
// Check if it extends Rsx_Model_Abstract
if (!is_subclass_of($fqcn, Rsx_Model_Abstract::class)) {
throw new RuntimeException(
"Cannot create type ref for '{$class_name}': Class must extend Rsx_Model_Abstract. " .
"Only model classes can be used in polymorphic type references."
);
}
}
/**
* Get the table name for a model class
*
* @param string $class_name
* @return string|null
*/
protected static function _get_table_name(string $class_name): ?string
{
$fqcn = Manifest::php_resolve_class($class_name);
if (!$fqcn) {
return null;
}
try {
// Create a reflection to get the table name without instantiating
$instance = new $fqcn();
return $instance->getTable();
} catch (\Throwable $e) {
return null;
}
}
}

View File

@@ -162,6 +162,14 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
*/
protected $table = '_file_attachments';
/**
* Polymorphic type reference columns
*
* fileable_type stores integer ID in database but exposes class name string
* @var array
*/
protected static $type_ref_columns = ['fileable_type'];
/**
* Get the physical file storage record
*
@@ -912,7 +920,9 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
}
// Replace any existing attachment with this category
static::where('fileable_type', get_class($model))
// Use simple class name (type_ref system stores simple names, not FQCNs)
$class_name = class_basename($model);
static::where('fileable_type', $class_name)
->where('fileable_id', $model->id)
->where('fileable_category', $category)
->update([
@@ -922,7 +932,7 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
]);
// Assign this attachment
$this->fileable_type = get_class($model);
$this->fileable_type = $class_name;
$this->fileable_id = $model->id;
$this->fileable_category = $category;
$this->save();
@@ -947,7 +957,8 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
}
// Assign this attachment (without replacing others)
$this->fileable_type = get_class($model);
// Use simple class name (type_ref system stores simple names, not FQCNs)
$this->fileable_type = class_basename($model);
$this->fileable_id = $model->id;
$this->fileable_category = $category;
$this->save();

View File

@@ -243,6 +243,10 @@ class Rsx_Framework_Provider extends ServiceProvider
// This also acquires application locks via RsxBootstrap
Manifest::init();
// Register polymorphic type references with Laravel's morph map
// This enables Laravel's morphTo() relationships to work with integer type refs
\App\RSpade\Core\Database\TypeRefs\Type_Ref_Registry::register_morph_map();
// Register RSX autoloader
\App\RSpade\Core\Autoloader::register();

View File

@@ -50,22 +50,12 @@ class Search_Index_Model extends Rsx_Site_Model_Abstract
protected $table = '_search_indexes';
/**
* The attributes that should be cast to native types
* Polymorphic type reference columns
*
* indexable_type stores integer ID in database but exposes class name string
* @var array
*/
protected $casts = [
'indexed_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Column metadata for special handling
*
* @var array
*/
protected $columnMeta = [];
protected static $type_ref_columns = ['indexable_type'];
/**
* Get the indexed model (polymorphic relationship)

View File

@@ -1,64 +1,148 @@
POLYMORPHIC(7) RSX Framework Manual POLYMORPHIC(7)
NAME
polymorphic - JSON-encoded polymorphic field handling
polymorphic - Polymorphic relationships with type references
SYNOPSIS
Server-side:
use App\RSpade\Core\Polymorphic_Field_Helper;
Model definition:
$field = Polymorphic_Field_Helper::parse($params['fieldname'], [
class Activity_Model extends Rsx_Model_Abstract
{
protected static $type_ref_columns = ['eventable_type'];
public function eventable()
{
return $this->morphTo();
}
}
Usage:
$activity->eventable_type = 'Contact_Model'; // Stores integer in DB
$activity->eventable_id = 123;
$activity->save();
echo $activity->eventable_type; // Returns "Contact_Model"
Form handling:
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
Project_Model::class,
]);
if ($error = $field->validate('Please select an entity')) {
$errors['fieldname'] = $error;
}
$model->poly_type = $field->model;
$model->poly_id = $field->id;
Client-side (form input value format):
{"model":"Contact_Model","id":123}
DESCRIPTION
Polymorphic fields allow a single database relationship to reference
multiple model types. For example, an Activity can be related to either
a Contact or a Project. This document describes the standard pattern for
handling polymorphic fields in RSX applications.
Polymorphic relationships allow a single database column pair to reference
multiple model types. For example, an Activity can be related to either a
Contact or a Project using eventable_type and eventable_id columns.
The Problem
RSX uses a type reference system that stores integers in the database but
transparently converts to/from class name strings in PHP. This provides:
Traditional form handling passes polymorphic data as separate fields:
- Efficient integer storage instead of VARCHAR class names
- Automatic type discovery - no manual mapping required
- Transparent conversion - you work with class names, never see integers
- Laravel morphTo() compatibility through auto-registered morph maps
eventable_type=Contact_Model
eventable_id=123
TYPE REFERENCE SYSTEM
How It Works
This approach has issues:
- Requires custom form submission handlers to inject hidden fields
- Field naming is inconsistent (type vs _type, id vs _id)
- No standard validation pattern
- Security validation (allowed model types) is ad-hoc
RSX maintains a _type_refs table that maps class names to integer IDs:
The Solution
id | class_name | table_name
1 | Contact_Model | contacts
2 | Project_Model | projects
3 | Task_Model | tasks
Polymorphic fields are submitted as a single JSON-encoded value:
When you set a type_ref column to a class name, RSX:
1. Checks if the class exists in _type_refs
2. If not, validates it's a valid Rsx_Model subclass and creates entry
3. Stores the integer ID in the database
When you read a type_ref column, RSX:
1. Looks up the integer ID in _type_refs
2. Returns the class name string
Defining Type Reference Columns
Declare which columns are type references in your model:
class File_Attachment_Model extends Rsx_Model_Abstract
{
protected static $type_ref_columns = ['fileable_type'];
public function fileable()
{
return $this->morphTo();
}
}
The cast is automatically applied - no manual $casts definition needed.
Database Schema
Type reference columns must be BIGINT, not VARCHAR:
fileable_type BIGINT NULL,
fileable_id BIGINT NULL,
The _type_refs table is created automatically by framework migrations.
Auto-Discovery
When storing a new class name that isn't in _type_refs yet:
$attachment->fileable_type = 'Custom_Model';
$attachment->save();
RSX will:
1. Verify Custom_Model exists and extends Rsx_Model_Abstract
2. Create a new _type_refs entry with the next available ID
3. Store that ID in the fileable_type column
This means any model can be used in polymorphic relationships without
pre-registration. The system is fully automatic.
Caching
The type refs map is cached:
1. In Redis (if available) - refreshed when entries are added
2. In PHP memory - loaded once per request
The map is typically small (< 100 entries), so full-map caching is used.
Validation
On write: Throws exception if value is not a valid Rsx_Model subclass
On read: Throws exception if ID doesn't exist in _type_refs table
Null values: Allowed if the database column is nullable
LARAVEL MORPH MAP INTEGRATION
RSX automatically registers all type refs with Laravel's morph map during
framework initialization. This means standard Laravel polymorphic methods
work transparently:
// These all work as expected
$attachment->fileable; // Returns the related model
$model->attachments; // morphMany relationship
Activity_Model::whereHasMorph(); // Polymorphic queries
The morph map uses simple class names (e.g., "Contact_Model") not fully
qualified names, matching how RSX models work throughout the framework.
FORM HANDLING
Client-Side Format
Polymorphic fields are submitted as JSON-encoded values:
eventable={"model":"Contact_Model","id":123}
Benefits:
- Single field to validate
- Standard val() getter/setter pattern on client
- Polymorphic_Field_Helper handles parsing and security validation
- Clean, reusable code on both client and server
This single-field approach simplifies form handling compared to
separate type and id fields.
SERVER-SIDE USAGE
Polymorphic_Field_Helper Class
Polymorphic_Field_Helper
Location: App\RSpade\Core\Polymorphic_Field_Helper
Parse a field value:
Server-side parsing with security validation:
use App\RSpade\Core\Polymorphic_Field_Helper;
@@ -67,39 +151,34 @@ SERVER-SIDE USAGE
Project_Model::class,
]);
The second argument is the whitelist of allowed model classes. Always
use Model::class syntax - never hardcode model name strings.
Validation Methods
Required field validation:
// Validation
if ($error = $eventable->validate('Please select an entity')) {
$errors['eventable'] = $error;
}
Optional field validation:
if ($error = $parent->validate_optional('Invalid parent type')) {
$errors['parent'] = $error;
}
Accessing Values
After validation:
// Access values
$model->eventable_type = $eventable->model; // "Contact_Model"
$model->eventable_id = $eventable->id; // 123
For optional fields, id will be null if not provided:
The whitelist ensures only allowed model types are accepted.
$model->parent_id = $parent->id; // null or integer
Validation Methods
State Checking Methods
Required field:
if ($error = $field->validate('Please select')) {
$errors['field'] = $error;
}
$field->is_empty() // No value provided
$field->is_valid() // Value provided and model type allowed
$field->is_invalid() // Value provided but model type not allowed
Optional field:
if ($error = $field->validate_optional('Invalid type')) {
$errors['field'] = $error;
}
State Checking
$field->is_empty() // No value provided
$field->is_valid() // Value provided and model type allowed
$field->is_invalid() // Value provided but model type not allowed
CLIENT-SIDE IMPLEMENTATION
Building a Polymorphic Picker Component
@@ -179,6 +258,23 @@ CLIENT-SIDE IMPLEMENTATION
});
COMPLETE EXAMPLE
Model Definition
class Activity_Model extends Rsx_Model_Abstract
{
protected static $type_ref_columns = ['eventable_type'];
public static $enums = [];
public static $rel = [];
protected $table = 'activities';
public function eventable()
{
return $this->morphTo();
}
}
Controller
use App\RSpade\Core\Polymorphic_Field_Helper;
@@ -209,13 +305,69 @@ COMPLETE EXAMPLE
return ['success' => true];
}
DATABASE SCHEMA
Polymorphic relationships use two columns:
Migration
$table->string('eventable_type');
$table->unsignedBigInteger('eventable_id');
DB::statement("
CREATE TABLE activities (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
eventable_type BIGINT NULL,
eventable_id BIGINT NULL,
description TEXT NULL,
site_id BIGINT NOT NULL,
created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
created_by BIGINT NULL,
updated_by BIGINT NULL,
INDEX idx_eventable (eventable_type, eventable_id),
FOREIGN KEY (site_id) REFERENCES sites(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
The type column stores the model class basename (e.g., "Contact_Model").
RSX VS LARAVEL
Laravel Default
- Stores full class name as VARCHAR: "App\\Models\\Contact"
- Requires manual morph map configuration for aliases
- No automatic discovery of new types
RSX Approach
- Stores integer ID in BIGINT column
- Automatic class name to ID mapping via _type_refs table
- Auto-discovery creates new entries on first use
- Morph map auto-registered for Laravel relationship compatibility
- Simple class names: "Contact_Model" not "App\\Models\\Contact"
Benefits
- Smaller database storage (BIGINT vs VARCHAR)
- Faster queries (integer comparison vs string comparison)
- Indexable without concerns about string length
- Consistent with RSX simple class name conventions
- Zero configuration - just declare $type_ref_columns
CONVERTING EXISTING COLUMNS
To convert a VARCHAR polymorphic column to use type_refs:
1. Create migration to add temporary column and convert data
2. Update model to declare $type_ref_columns
3. Drop old VARCHAR column
Example migration (if you have existing data to preserve):
// Add new integer column
DB::statement("ALTER TABLE my_table ADD COLUMN entity_type_new BIGINT NULL AFTER entity_type");
// Note: Data migration would go here if needed
// For new installations, just drop and recreate the column
// Drop old column and rename
DB::statement("ALTER TABLE my_table DROP COLUMN entity_type");
DB::statement("ALTER TABLE my_table CHANGE entity_type_new entity_type BIGINT NULL");
For fresh installations with no data to preserve, simply:
DB::statement("ALTER TABLE my_table MODIFY entity_type BIGINT NULL");
SECURITY CONSIDERATIONS
Model Type Validation
@@ -235,6 +387,12 @@ SECURITY CONSIDERATIONS
// Submitting {"model":"User_Model","id":1} will fail validation
$eventable->is_valid(); // false
Type Ref Auto-Creation
The type_refs system validates that any new class name is a valid
Rsx_Model subclass before creating an entry. This prevents attackers
from polluting the _type_refs table with invalid class names.
Use Model::class, Not Strings
Always use the ::class constant to specify allowed models:
@@ -251,6 +409,6 @@ SECURITY CONSIDERATIONS
- Consistent naming with actual class names
SEE ALSO
form_conventions(7), ajax_error_handling(7)
model(7), form_conventions(7), enum(7)
RSX Framework 2025-12-23 POLYMORPHIC(7)
RSX Framework 2025-12-27 POLYMORPHIC(7)

View File

@@ -0,0 +1,146 @@
TYPE REFS SYSTEM - MIGRATION GUIDE
Date: 2025-12-27
SUMMARY
Polymorphic relationship columns (like fileable_type, taskable_type) now store
integer IDs in the database instead of VARCHAR class name strings. This change
improves storage efficiency and query performance. A new _type_refs table maps
between integer IDs and class names. The framework handles conversion transparently
via Eloquent casts - your PHP code continues to work with class name strings.
If your application has custom polymorphic relationships beyond file_attachments,
_search_indexes, and tasks, you must add the $type_ref_columns property to those
models and run migrations to convert the column types.
AFFECTED FILES
Models with polymorphic columns need $type_ref_columns property:
- Any model with *_type columns for morphTo/morphMany relationships
Files that query polymorphic columns by class name:
- Must use simple class names (Contact_Model), not FQCNs
CHANGES REQUIRED
1. Add $type_ref_columns to Models with Polymorphic Columns
Any model with polymorphic type columns needs to declare them:
BEFORE: (no declaration, column stored as VARCHAR)
class My_Attachment_Model extends Rsx_Site_Model_Abstract
{
protected $table = 'my_attachments';
// fileable_type column stored class names as VARCHAR
}
AFTER:
class My_Attachment_Model extends Rsx_Site_Model_Abstract
{
protected $table = 'my_attachments';
/**
* Polymorphic type reference columns
* @var array
*/
protected static $type_ref_columns = ['fileable_type'];
}
2. Create Migration to Convert Column Type
For each table with polymorphic columns, create a migration:
php artisan make:migration:safe convert_my_table_polymorphic_to_type_refs
Migration content:
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
// Clear existing data (type refs will auto-populate on first use)
DB::statement("UPDATE my_attachments SET fileable_type = NULL");
// Convert column from VARCHAR to BIGINT
DB::statement("ALTER TABLE my_attachments MODIFY fileable_type BIGINT NULL");
}
};
3. Use Simple Class Names in Queries
When querying by polymorphic type, use simple class names:
BEFORE:
$attachments = File_Attachment_Model::where('fileable_type', 'App\Models\Contact_Model')
->where('fileable_id', $id)
->get();
AFTER:
$attachments = File_Attachment_Model::where('fileable_type', 'Contact_Model')
->where('fileable_id', $id)
->get();
When setting polymorphic types from model instances:
BEFORE:
$attachment->fileable_type = get_class($model); // Returns FQCN
AFTER:
$attachment->fileable_type = class_basename($model); // Simple name
4. Using Type Refs Programmatically (Advanced)
The Type_Ref_Registry provides direct access to mappings:
use App\RSpade\Core\Database\TypeRefs\Type_Ref_Registry;
// Get ID for a class name
$id = Type_Ref_Registry::class_to_id('Contact_Model');
// Get class name for an ID
$class = Type_Ref_Registry::id_to_class($id);
// Check if registered
$exists = Type_Ref_Registry::has_class('Contact_Model');
// Refresh cache after manual database changes
Type_Ref_Registry::refresh();
CONFIGURATION
No configuration changes required.
The _type_refs table is created automatically by framework migrations.
Type refs are auto-registered when first used.
VERIFICATION
1. Run migrations:
php artisan migrate
2. Verify type refs table exists:
SELECT * FROM _type_refs;
3. Test polymorphic relationships:
$model = My_Model::find(1);
$attachment = File_Attachment_Model::create_from_upload($file, [
'site_id' => Session::get_site_id(),
]);
$attachment->attach_to($model, 'documents');
// Verify type was stored correctly
$stored = File_Attachment_Model::find($attachment->id);
echo $stored->fileable_type; // Should print 'My_Model'
4. Check database column type:
SHOW COLUMNS FROM file_attachments WHERE Field = 'fileable_type';
-- Type should be bigint(20)
REFERENCE
php artisan rsx:man polymorphic
/system/app/RSpade/Core/Database/TypeRefs/CLAUDE.md

View File

@@ -361,6 +361,21 @@
"created_at": "2025-12-26T02:35:44+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe convert_client_status_to_enum"
},
"2025_12_27_225200_create_type_refs_table.php": {
"created_at": "2025-12-27T22:52:00+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe create_type_refs_table"
},
"2025_12_27_225224_convert_polymorphic_columns_to_type_refs.php": {
"created_at": "2025-12-27T22:52:24+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe convert_polymorphic_columns_to_type_refs"
},
"2025_12_27_225305_convert_tasks_polymorphic_to_type_refs.php": {
"created_at": "2025-12-27T22:53:05+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe convert_tasks_polymorphic_to_type_refs"
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* IMPORTANT: Use raw MySQL queries for clarity and auditability
* DB::statement() with raw SQL
* Schema::create() with Blueprint
*
* REQUIRED: ALL tables MUST have: id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
* No exceptions - every table needs this exact ID column (SIGNED for easier migrations)
*
* Integer types: Use BIGINT for all integers, TINYINT(1) for booleans only
* Never use unsigned - all integers should be signed
*
* Migrations must be self-contained - no Model/Service references
*
* @return void
*/
public function up()
{
// Create the _type_refs table for polymorphic type reference mapping
// Maps class names to integer IDs for efficient polymorphic column storage
DB::statement("
CREATE TABLE _type_refs (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
class_name VARCHAR(255) NOT NULL,
table_name VARCHAR(255) NULL,
created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY unique_class_name (class_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
/**
* down() method is prohibited in RSpade framework
* Migrations should only move forward, never backward
* You may remove this comment as soon as you see it and understand.
*/
};

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* IMPORTANT: Use raw MySQL queries for clarity and auditability
* DB::statement("ALTER TABLE type_refs ADD COLUMN new_field VARCHAR(255)")
* Schema::table() with Blueprint
*
* Migrations must be self-contained - no Model/Service references
*
* @return void
*/
public function up()
{
// Convert polymorphic type columns from VARCHAR to BIGINT
// These columns will now store type_ref IDs instead of class name strings
// Existing data is cleared since there's no production data to migrate
// _file_attachments: Convert fileable_type from VARCHAR to BIGINT
// First drop the existing index that uses the VARCHAR column
DB::statement("ALTER TABLE _file_attachments DROP INDEX IF EXISTS idx_fileable");
// Clear existing data and modify column type
DB::statement("UPDATE _file_attachments SET fileable_type = NULL");
DB::statement("ALTER TABLE _file_attachments MODIFY fileable_type BIGINT NULL");
// Recreate index with BIGINT column
DB::statement("ALTER TABLE _file_attachments ADD INDEX idx_fileable (fileable_type, fileable_id)");
// _search_indexes: Convert indexable_type from VARCHAR to BIGINT
// First drop the existing indexes that use the VARCHAR column
DB::statement("ALTER TABLE _search_indexes DROP INDEX IF EXISTS idx_indexable");
DB::statement("ALTER TABLE _search_indexes DROP INDEX IF EXISTS unique_indexable");
// Clear existing data and modify column type
DB::statement("TRUNCATE TABLE _search_indexes");
DB::statement("ALTER TABLE _search_indexes MODIFY indexable_type BIGINT NOT NULL");
// Recreate indexes with BIGINT column
DB::statement("ALTER TABLE _search_indexes ADD INDEX idx_indexable (indexable_type, indexable_id)");
DB::statement("ALTER TABLE _search_indexes ADD UNIQUE KEY unique_indexable (indexable_type, indexable_id)");
}
/**
* down() method is prohibited in RSpade framework
* Migrations should only move forward, never backward
* You may remove this comment as soon as you see it and understand.
*/
};

View File

@@ -1054,6 +1054,25 @@ Details: `php artisan rsx:man file_upload`
---
## POLYMORPHIC TYPE REFERENCES
Polymorphic `*_type` columns (fileable_type, taskable_type, etc.) store BIGINT integers in the database for efficiency. The framework maps between integer IDs and class names transparently.
```php
// Model: Declare type ref columns
protected static $type_ref_columns = ['fileable_type'];
// Usage: Code works with class name strings - conversion is automatic
$attachment->fileable_type = 'Contact_Model'; // Stored as integer in DB
echo $attachment->fileable_type; // Reads as 'Contact_Model'
```
**Simple Names Only**: Always use `class_basename($model)`, never `get_class($model)` (FQCNs)
Details: `php artisan rsx:man polymorphic`
---
## AJAX ENDPOINTS
```php