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>
257 lines
8.1 KiB
Plaintext
Executable File
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)
|