Files
rspade_system/app/RSpade/man/polymorphic.txt
root eb3ccd722d Add Polymorphic_Field_Helper for JSON-encoded polymorphic form fields
Fix incorrect data-sid selector in route-debug help example
Fix Form_Utils to use component.$sid() instead of data-sid selector
Add response helper functions and use _message as reserved metadata key

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-23 23:26:55 +00:00

257 lines
8.1 KiB
Plaintext
Executable File

POLYMORPHIC(7) RSX Framework Manual POLYMORPHIC(7)
NAME
polymorphic - JSON-encoded polymorphic field handling
SYNOPSIS
Server-side:
use App\RSpade\Core\Polymorphic_Field_Helper;
$field = Polymorphic_Field_Helper::parse($params['fieldname'], [
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.
The Problem
Traditional form handling passes polymorphic data as separate fields:
eventable_type=Contact_Model
eventable_id=123
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
The Solution
Polymorphic fields are submitted as a single JSON-encoded value:
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
SERVER-SIDE USAGE
Polymorphic_Field_Helper Class
Location: App\RSpade\Core\Polymorphic_Field_Helper
Parse a field value:
use App\RSpade\Core\Polymorphic_Field_Helper;
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
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:
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:
$model->eventable_type = $eventable->model; // "Contact_Model"
$model->eventable_id = $eventable->id; // 123
For optional fields, id will be null if not provided:
$model->parent_id = $parent->id; // null or integer
State Checking Methods
$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
To create a picker for polymorphic fields, build a component that:
1. Maintains a hidden input with the JSON-encoded value
2. Implements val() to get/set as {model: 'Model_Name', id: number}
3. Syncs the hidden input on every change
4. Handles pre-load value setting (value set before options loaded)
Hidden Input Pattern
The component template should include a hidden input:
<input type="hidden" $sid="hidden_value" name="<%= this.args.name %>" />
On value change, sync to hidden input as JSON:
_sync_hidden_value() {
const $hidden = this.$sid('hidden_value');
if (this._value && this._value.model && this._value.id) {
$hidden.val(JSON.stringify(this._value));
} else {
$hidden.val('');
}
}
val() Implementation
The val() method should work before and after component loads:
val(value) {
if (arguments.length === 0) {
// Getter
return this._value || this._pending_value || null;
}
// Setter
const parsed = value ? {model: value.model, id: parseInt(value.id)} : null;
if (this.data.loading) {
this._pending_value = parsed;
this._value = parsed;
this._sync_hidden_value();
} else {
this._value = parsed;
this._apply_to_ui();
this._sync_hidden_value();
}
}
Using Tom Select
For searchable dropdowns, use Tom Select with a compound value format:
// Compound value: "Model_Name:id"
valueField: 'compound_value',
onChange: function(compound_value) {
if (!compound_value) {
that._value = null;
} else {
const parts = compound_value.split(':');
that._value = {model: parts[0], id: parseInt(parts[1])};
}
that._sync_hidden_value();
}
Add options with the compound format:
all_options.push({
compound_value: 'Contact_Model:' + contact.id,
model: 'Contact_Model',
id: contact.id,
name: contact.name,
});
COMPLETE EXAMPLE
Controller
use App\RSpade\Core\Polymorphic_Field_Helper;
#[Ajax_Endpoint]
public static function save(Request $request, array $params = [])
{
$eventable = Polymorphic_Field_Helper::parse($params['eventable'] ?? null, [
Contact_Model::class,
Project_Model::class,
]);
$errors = [];
if ($error = $eventable->validate('Please select an entity')) {
$errors['eventable'] = $error;
}
if (!empty($errors)) {
return response_form_error('Please correct errors', $errors);
}
$activity = new Activity_Model();
$activity->eventable_type = $eventable->model;
$activity->eventable_id = $eventable->id;
$activity->save();
return ['success' => true];
}
DATABASE SCHEMA
Polymorphic relationships use two columns:
$table->string('eventable_type');
$table->unsignedBigInteger('eventable_id');
The type column stores the model class basename (e.g., "Contact_Model").
SECURITY CONSIDERATIONS
Model Type Validation
CRITICAL: Always validate that submitted model types are in the allowed
list. Attackers could submit arbitrary model names to exploit the
polymorphic relationship.
The Polymorphic_Field_Helper enforces this automatically:
// Only Contact_Model and Project_Model are accepted
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
Project_Model::class,
]);
// Submitting {"model":"User_Model","id":1} will fail validation
$eventable->is_valid(); // false
Use Model::class, Not Strings
Always use the ::class constant to specify allowed models:
// CORRECT - uses class constant
Polymorphic_Field_Helper::parse($value, [Contact_Model::class]);
// WRONG - hardcoded string
Polymorphic_Field_Helper::parse($value, ['Contact_Model']);
Using ::class ensures:
- IDE autocompletion and refactoring support
- Compile-time error if class doesn't exist
- Consistent naming with actual class names
SEE ALSO
form_conventions(7), ajax_error_handling(7)
RSX Framework 2025-12-23 POLYMORPHIC(7)