Implement BEM-style enum naming and fetch() anti-aliasing policy

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-26 02:17:31 +00:00
parent a289eecf0f
commit 7d379b2402
50 changed files with 1041 additions and 577 deletions

View File

@@ -487,6 +487,7 @@ Only the following rules are approved for manifest-time execution:
- **JQHTML-INLINE-01** (JqhtmlInlineScriptRule): Prevents inline scripts/styles in Jqhtml template files (critical architecture violation)
- **PHP-SPA-01** (SpaAttributeMisuseRule): Prevents combining #[SPA] with #[Route] attributes (critical architecture misunderstanding)
- **MANIFEST-INST-01** (InstanceMethodsRule): Enforces static-only classes unless Instantiatable (framework convention)
- **PHP-ALIAS-01** (FieldAliasingRule): Prevents field name shortenings in fetch() and Ajax endpoints (anti-aliasing policy)
All other rules should return `false` from `is_called_during_manifest_scan()`.

View File

@@ -5,33 +5,34 @@ namespace App\RSpade\CodeQuality\Rules\PHP;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* FieldAliasingRule - Detects confusing field name shortenings
* FieldAliasingRule - Enforces fetch() anti-aliasing policy
*
* This rule catches cases where a field name is SHORTENED by dropping parts,
* which creates confusion about what the field actually represents.
* fetch() exists for SECURITY (removing private data), not aliasing.
*
* VIOLATION - Dropping parts from a name (confusing):
* 'type_label' => $contact->type_id_label, // Dropped "id" - what happened to it?
* 'client_label' => $record->client_id_label, // Dropped "id" - confusing
* 'name' => $user->display_name, // Dropped "display" - loses context
* VALID PATTERNS:
* 1. Model method with MATCHING name:
* 'full_name' => $model->full_name()
* 'unread_count' => $this->unread_count()
*
* ALLOWED - Renaming to a completely different concept:
* 'value' => $client->id, // "value" is a UI concept, not a shortened "id"
* 'label' => $client->name, // "label" is a UI concept, not a shortened "name"
* 'client_id' => $client->id, // Adding context, not dropping it
* 'id' => $client->id, // Same name - no aliasing
* 2. Conditional with matching property/method or literals:
* 'foo' => $condition ? $model->foo : null
* 'secret' => $user->is_admin ? $model->secret : '[REDACTED]'
*
* ALLOWED - Transformations:
* 'type_id_label_upper' => strtoupper($contact->type_id_label),
* INVALID PATTERNS:
* 1. Any property alias (key != property):
* 'type_label' => $model->type_id__label // BAD
*
* The detection logic: Flag when the key's underscore-separated parts are a
* PROPER SUBSET of the source property's parts (all key parts exist in source,
* but source has additional parts). This catches "dropping parts" without
* flagging legitimate renames to different concepts.
* 2. Method call with mismatched name:
* 'addr' => $model->formatted_address() // BAD - name must match
*
* 3. Redundant explicit assignments (unnecessary):
* 'id' => $model->id // Already in toArray()
*
* Applies to:
* - Controller methods with #[Ajax_Endpoint] attribute
* - Model fetch() methods with #[Ajax_Endpoint_Model_Fetch] attribute
*
* NOT checked (controllers are an escape hatch for custom responses):
* - Controller methods with #[Ajax_Endpoint] attribute
*/
class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
{
@@ -48,7 +49,7 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
*/
public function get_name(): string
{
return 'Field Aliasing Prohibition';
return 'Fetch Anti-Aliasing Policy';
}
/**
@@ -56,7 +57,7 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
*/
public function get_description(): string
{
return 'Prohibits renaming fields during serialization - field names must be consistent across all application layers';
return 'Enforces fetch() anti-aliasing policy - fetch() is for security, not aliasing';
}
/**
@@ -72,7 +73,7 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
*/
public function is_called_during_manifest_scan(): bool
{
return false; // Only run during rsx:check
return true; // Immediate feedback on aliasing violations
}
/**
@@ -101,21 +102,16 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
return;
}
// Determine file type and get relevant methods
// Only check models - controllers are an escape hatch for custom responses
$extends = $metadata['extends'] ?? null;
$methods_to_check = [];
if ($extends === 'Rsx_Controller_Abstract') {
// Controller - check methods with #[Ajax_Endpoint]
$methods_to_check = $this->get_ajax_endpoint_methods($metadata);
} elseif ($extends === 'Rsx_Model_Abstract') {
// Model - check fetch() method with #[Ajax_Endpoint_Model_Fetch]
$methods_to_check = $this->get_model_fetch_methods($metadata);
} else {
// Not a controller or model we care about
if ($extends !== 'Rsx_Model_Abstract') {
return;
}
// Check fetch() method with #[Ajax_Endpoint_Model_Fetch]
$methods_to_check = $this->get_model_fetch_methods($metadata);
if (empty($methods_to_check)) {
return;
}
@@ -138,29 +134,6 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
}
}
/**
* Get methods with #[Ajax_Endpoint] attribute from controller
*/
private function get_ajax_endpoint_methods(array $metadata): array
{
$methods = $metadata['public_static_methods'] ?? [];
$result = [];
foreach ($methods as $method_name => $method_info) {
$attributes = $method_info['attributes'] ?? [];
foreach ($attributes as $attr_name => $attr_data) {
$short_name = basename(str_replace('\\', '/', $attr_name));
if ($short_name === 'Ajax_Endpoint') {
$result[$method_name] = $method_info;
break;
}
}
}
return $result;
}
/**
* Get fetch() methods with #[Ajax_Endpoint_Model_Fetch] attribute from model
*/
@@ -196,54 +169,82 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
$original_lines = explode("\n", $original_contents);
foreach ($lines as $offset => $line) {
// Pattern: 'key' => $var->property or 'key' => $var['property']
// We need to detect when key != property (with no transformation)
$actual_line_num = $method_start_line + $offset;
// Match: 'key_name' => $something->property_name
// or: 'key_name' => $something['property_name']
// Without function wrapping
// Check for line-level exception
if ($this->line_has_exception($original_lines, $actual_line_num)) {
continue;
}
// Pattern for object property access: 'key' => $var->prop
if (preg_match("/['\"]([a-zA-Z_][a-zA-Z0-9_]*)['\"]\\s*=>\\s*\\$[a-zA-Z_][a-zA-Z0-9_]*->([a-zA-Z_][a-zA-Z0-9_]*)\\s*[,\\]\\)]/", $line, $matches)) {
$key = $matches[1];
$property = $matches[2];
// Pattern: 'key' => ...
// We need to analyze what's on the right side
if (!preg_match("/['\"]([a-zA-Z_][a-zA-Z0-9_]*)['\"]\\s*=>/", $line, $key_match)) {
continue;
}
if ($this->is_problematic_alias($key, $property) && !$this->has_transformation($line, $matches[0])) {
// Check for line-level exception
$actual_line_num = $method_start_line + $offset;
if ($this->line_has_exception($original_lines, $actual_line_num)) {
continue;
}
$key = $key_match[1];
// Get the value part (everything after =>)
$arrow_pos = strpos($line, '=>');
if ($arrow_pos === false) {
continue;
}
$value_part = trim(substr($line, $arrow_pos + 2));
// Check for ternary operator
if ($this->is_ternary_expression($value_part)) {
$this->check_ternary($file_path, $actual_line_num, $line, $key, $value_part);
continue;
}
// Check for method call: $var->method() or $this->method()
if (preg_match('/\$([a-zA-Z_][a-zA-Z0-9_]*)->([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $value_part, $method_match)) {
$method_called = $method_match[2];
if ($key !== $method_called) {
$this->add_violation(
$file_path,
$actual_line_num,
"Field name shortened by dropping parts: '{$key}' is missing parts from '{$property}'",
"Method call key must match method name: '{$key}' != '{$method_called}()'",
trim($line),
$this->build_suggestion($key, $property),
$this->build_method_mismatch_suggestion($key, $method_called),
'high'
);
}
continue;
}
// Pattern for array access: 'key' => $var['prop']
if (preg_match("/['\"]([a-zA-Z_][a-zA-Z0-9_]*)['\"]\\s*=>\\s*\\$[a-zA-Z_][a-zA-Z0-9_]*\\[['\"]([a-zA-Z_][a-zA-Z0-9_]*)['\"]\\]\\s*[,\\]\\)]/", $line, $matches)) {
$key = $matches[1];
$property = $matches[2];
// Check for property access: $var->property or $var['property']
$property = null;
if ($this->is_problematic_alias($key, $property) && !$this->has_transformation($line, $matches[0])) {
// Check for line-level exception
$actual_line_num = $method_start_line + $offset;
if ($this->line_has_exception($original_lines, $actual_line_num)) {
continue;
}
// Object property: $var->prop
if (preg_match('/\$([a-zA-Z_][a-zA-Z0-9_]*)->([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*[,;\]\)]|$)/', $value_part, $prop_match)) {
$property = $prop_match[2];
}
// Array access: $var['prop']
elseif (preg_match('/\$([a-zA-Z_][a-zA-Z0-9_]*)\[[\'"]([a-zA-Z_][a-zA-Z0-9_]*)[\'"]\]/', $value_part, $arr_match)) {
$property = $arr_match[2];
}
if ($property !== null) {
if ($key === $property) {
// Redundant assignment - already in toArray()
$this->add_violation(
$file_path,
$actual_line_num,
"Field name shortened by dropping parts: '{$key}' is missing parts from '{$property}'",
"Redundant assignment: '{$key}' is already included by toArray()",
trim($line),
$this->build_suggestion($key, $property),
$this->build_redundant_suggestion($key),
'medium'
);
} else {
// Aliasing - key != property
$this->add_violation(
$file_path,
$actual_line_num,
"Field aliasing prohibited: '{$key}' != '{$property}'",
trim($line),
$this->build_alias_suggestion($key, $property),
'high'
);
}
@@ -252,85 +253,112 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
}
/**
* Check if the key is a problematic alias of the property
*
* Returns true if the key is a PROPER SUBSET of the property's parts,
* meaning all parts of the key exist in the property, but the property
* has additional parts that were dropped.
*
* Examples:
* 'type_label', 'type_id_label' -> true (dropped "id")
* 'name', 'display_name' -> true (dropped "display")
* 'value', 'id' -> false (different concept)
* 'client_id', 'id' -> false (adding context)
* 'id', 'id' -> false (same name)
* Check if expression contains a ternary operator (not inside a string)
*/
private function is_problematic_alias(string $key, string $property): bool
private function is_ternary_expression(string $value): bool
{
// Same name is never a violation
if ($key === $property) {
return false;
}
// Split by underscores
$key_parts = explode('_', strtolower($key));
$property_parts = explode('_', strtolower($property));
// Check if ALL key parts exist in the property parts
foreach ($key_parts as $key_part) {
if (!in_array($key_part, $property_parts)) {
// Key has a part that doesn't exist in property
// This means it's a rename to a different concept, not a shortening
return false;
}
}
// At this point, all key parts exist in property parts
// Check if property has additional parts (making key a proper subset)
if (count($property_parts) > count($key_parts)) {
// Property has more parts than key - parts were dropped
return true;
}
// Same number of parts (just reordered?) - not a violation
return false;
// Remove string contents to avoid false positives
$no_strings = preg_replace('/(["\'])(?:[^\\\\]|\\\\.)*?\1/', '', $value);
return str_contains($no_strings, '?') && str_contains($no_strings, ':');
}
/**
* Check if the value has a transformation applied (function call wrapping it)
* Check ternary expression for valid patterns
*/
private function has_transformation(string $line, string $matched_portion): bool
private function check_ternary(string $file_path, int $line_num, string $line, string $key, string $value_part): void
{
// Find where the matched portion starts in the line
$pos = strpos($line, $matched_portion);
if ($pos === false) {
return false;
// Extract the true and false branches
// This is simplified - a full parser would be needed for nested ternaries
$no_strings = preg_replace('/(["\'])(?:[^\\\\]|\\\\.)*?\1/', '""', $value_part);
// Find the ? and : positions
$q_pos = strpos($no_strings, '?');
$c_pos = strpos($no_strings, ':');
if ($q_pos === false || $c_pos === false || $c_pos < $q_pos) {
return; // Can't parse
}
// Get everything after '=>' and before the matched value
if (preg_match("/=>\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(/", $line, $fn_match)) {
// There's a function call before the value
$true_branch = trim(substr($value_part, $q_pos + 1, $c_pos - $q_pos - 1));
$false_branch = trim(substr($value_part, $c_pos + 1));
// Remove trailing punctuation from false branch
$false_branch = rtrim($false_branch, ',;)');
// Check each branch - must be either:
// 1. A literal (string, number, null, true, false)
// 2. A property/method access with matching key name
$true_valid = $this->is_valid_ternary_branch($key, $true_branch);
$false_valid = $this->is_valid_ternary_branch($key, $false_branch);
if (!$true_valid || !$false_valid) {
$this->add_violation(
$file_path,
$line_num,
"Ternary branches must use matching property/method name or literals",
trim($line),
$this->build_ternary_suggestion($key),
'high'
);
}
}
/**
* Check if a ternary branch is valid
*/
private function is_valid_ternary_branch(string $key, string $branch): bool
{
$branch = trim($branch);
// Literal values are always valid
if ($this->is_literal($branch)) {
return true;
}
// Check for method chaining or casting
if (preg_match("/=>\\s*\\([^)]+\\)\\s*\\$/", $line)) {
// Cast like (string)$var->prop
// Method call: $var->method() - method must match key
if (preg_match('/\$[a-zA-Z_][a-zA-Z0-9_]*->([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $branch, $m)) {
return $m[1] === $key;
}
// Property access: $var->prop - prop must match key
if (preg_match('/\$[a-zA-Z_][a-zA-Z0-9_]*->([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*$|[^(])/', $branch, $m)) {
return $m[1] === $key;
}
// Array access: $var['prop'] - prop must match key
if (preg_match('/\$[a-zA-Z_][a-zA-Z0-9_]*\[[\'"]([a-zA-Z_][a-zA-Z0-9_]*)[\'"]\]/', $branch, $m)) {
return $m[1] === $key;
}
// Other expressions (function calls, etc.) - can't validate easily, allow
return true;
}
/**
* Check if value is a literal
*/
private function is_literal(string $value): bool
{
$value = trim($value);
// null, true, false
if (in_array(strtolower($value), ['null', 'true', 'false'])) {
return true;
}
// Check for string concatenation
if (preg_match("/=>\\s*['\"].*['\"]\\s*\\.\\s*\\$/", $line) || preg_match("/\\$[^,]+\\.\\s*['\"]/", $line)) {
// Number
if (is_numeric($value)) {
return true;
}
// Check for ternary operator
if (strpos($line, '?') !== false && strpos($line, ':') !== false) {
// String literal
if (preg_match('/^(["\']).*\1$/', $value)) {
return true;
}
// Check for null coalescing
if (strpos($line, '??') !== false) {
// Empty array
if ($value === '[]') {
return true;
}
@@ -413,33 +441,100 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
}
/**
* Build suggestion for fixing the violation
* Build suggestion for method name mismatch
*/
private function build_suggestion(string $key, string $property): string
private function build_method_mismatch_suggestion(string $key, string $method): string
{
$suggestions = [];
$suggestions[] = "PROBLEM: Field name shortened by dropping parts.";
$suggestions[] = "";
$suggestions[] = "The key '{$key}' contains only some parts of '{$property}'.";
$suggestions[] = "This is confusing because it obscures what was removed.";
$suggestions[] = "";
$suggestions[] = "FIX: Use the full property name:";
$suggestions[] = "";
$suggestions[] = " // WRONG - parts dropped, confusing";
$suggestions[] = " '{$key}' => \$model->{$property},";
$suggestions[] = "";
$suggestions[] = " // CORRECT - full name preserved";
$suggestions[] = " '{$property}' => \$model->{$property},";
$suggestions[] = "";
$suggestions[] = "NOTE: Renaming to a DIFFERENT concept is allowed:";
$suggestions[] = "";
$suggestions[] = " // OK - 'value'/'label' are UI concepts, not shortenings";
$suggestions[] = " 'value' => \$model->id,";
$suggestions[] = " 'label' => \$model->name,";
$suggestions[] = "";
$suggestions[] = " // OK - adding context, not dropping it";
$suggestions[] = " 'client_id' => \$client->id,";
return implode("\n", [
"PROBLEM: Method call key doesn't match method name.",
"",
"fetch() anti-aliasing policy requires method keys to match method names.",
"This ensures a single source of truth and consistent naming across PHP/JS.",
"",
"FIX: Use the method name as the key:",
"",
" // WRONG",
" '{$key}' => \$model->{$method}(),",
"",
" // CORRECT",
" '{$method}' => \$model->{$method}(),",
"",
"See: php artisan rsx:man model_fetch",
]);
}
return implode("\n", $suggestions);
/**
* Build suggestion for redundant assignment
*/
private function build_redundant_suggestion(string $key): string
{
return implode("\n", [
"PROBLEM: Redundant explicit assignment.",
"",
"This field is already included automatically by toArray().",
"Explicit assignment is unnecessary and adds maintenance burden.",
"",
"FIX: Remove this line - the field is already in the output.",
"",
" // UNNECESSARY - remove this line",
" '{$key}' => \$model->{$key},",
"",
"toArray() automatically includes all model fields, enum properties,",
"and the __MODEL marker for JavaScript hydration.",
"",
"See: php artisan rsx:man model_fetch",
]);
}
/**
* Build suggestion for property aliasing
*/
private function build_alias_suggestion(string $key, string $property): string
{
return implode("\n", [
"PROBLEM: Field aliasing is prohibited.",
"",
"fetch() exists for SECURITY (removing private data), not aliasing.",
"Aliasing breaks grep searches and obscures data sources.",
"",
"OPTIONS:",
"",
"1. Use the original property name:",
" '{$property}' => \$model->{$property},",
"",
"2. If this is a computed value, create a model method:",
" // In model:",
" public function {$key}() { return ...; }",
"",
" // In fetch:",
" '{$key}' => \$model->{$key}(),",
"",
"3. If this is an enum property, use the full BEM-style name:",
" // Instead of 'type_label', use 'type_id__label'",
"",
"See: php artisan rsx:man model_fetch",
]);
}
/**
* Build suggestion for ternary violations
*/
private function build_ternary_suggestion(string $key): string
{
return implode("\n", [
"PROBLEM: Ternary branches must use matching names or literals.",
"",
"Conditional assignments in fetch() are allowed, but both branches",
"must use the same property/method name as the key, or be literals.",
"",
"VALID patterns:",
" '{$key}' => \$condition ? \$model->{$key} : null,",
" '{$key}' => \$model->can_see() ? \$model->{$key} : '[HIDDEN]',",
"",
"INVALID patterns:",
" '{$key}' => \$condition ? \$model->other_field : null,",
"",
"See: php artisan rsx:man model_fetch",
]);
}
}

View File

@@ -167,16 +167,15 @@ class Document_Models_Command extends FrameworkDeveloperCommand
if (property_exists($className, 'enums') && !empty($className::$enums)) {
foreach ($className::$enums as $columnName => $enumDefinitions) {
// Add enum accessor properties (instance properties for current value)
$enumProperties[] = " * @property-read string \${$columnName}_label";
$enumProperties[] = " * @property-read string \${$columnName}_constant";
$enumProperties[] = " * @property-read array \${$columnName}_enum_val";
// Add enum accessor properties (BEM-style: field__property)
$enumProperties[] = " * @property-read string \${$columnName}__label";
$enumProperties[] = " * @property-read string \${$columnName}__constant";
// Add enum static methods (mirrored in JavaScript stubs)
$enumMethods[] = " * @method static array {$columnName}_enum_val() Get all enum definitions with full metadata";
$enumMethods[] = " * @method static array {$columnName}_enum_select() Get selectable items for dropdowns";
$enumMethods[] = " * @method static array {$columnName}_enum_labels() Get simple id => label map";
$enumMethods[] = " * @method static array {$columnName}_enum_ids() Get array of all valid enum IDs";
// Add enum static methods (BEM-style, mirrored in JavaScript stubs)
$enumMethods[] = " * @method static array {$columnName}__enum() Get all enum definitions with full metadata";
$enumMethods[] = " * @method static array {$columnName}__enum_select() Get selectable items for dropdowns";
$enumMethods[] = " * @method static array {$columnName}__enum_labels() Get simple id => label map";
$enumMethods[] = " * @method static array {$columnName}__enum_ids() Get array of all valid enum IDs";
// Generate constants for each enum value
foreach ($enumDefinitions as $value => $definition) {

View File

@@ -33,7 +33,7 @@ use App\RSpade\Core\Models\User_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:53
* Generated on: 2025-12-26 01:29:30
* Table: _api_keys
*
* @property int $id
@@ -53,7 +53,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class Api_Key_Model extends Rsx_System_Model_Abstract
{
{
protected $table = '_api_keys';
public static $enums = [];

View File

@@ -402,10 +402,18 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= "\n";
}
// Generate enum value getter with Proxy for maintaining order
$content .= " static __{$column}_enum_val = null;\n";
$content .= " static {$column}_enum_val(enum_value) {\n";
$content .= " if (!this.__{$column}_enum_val) {\n";
// Generate enum getter with Proxy for maintaining order (BEM-style: field__enum)
$content .= " /**\n";
$content .= " * Get enum metadata for {$column}.\n";
$content .= " * @param {number} [enum_value] - If provided, returns metadata for that ID (or null + console.error if invalid)\n";
$content .= " * @returns {Object} All enum definitions keyed by ID, or single enum's metadata if enum_value provided\n";
$content .= " * @example\n";
$content .= " * // Get all: Model.{$column}__enum()\n";
$content .= " * // Get one: Model.{$column}__enum(Model.CONSTANT_NAME).property\n";
$content .= " */\n";
$content .= " static __{$column}__enum = null;\n";
$content .= " static {$column}__enum(enum_value) {\n";
$content .= " if (!this.__{$column}__enum) {\n";
$content .= " const data = {};\n";
$content .= " const order = [];\n";
@@ -418,7 +426,7 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
}
$content .= " // Cache Proxy that maintains sort order for enumeration\n";
$content .= " this.__{$column}_enum_val = new Proxy(data, {\n";
$content .= " this.__{$column}__enum = new Proxy(data, {\n";
$content .= " ownKeys() {\n";
$content .= " return order.map(String);\n";
$content .= " },\n";
@@ -434,19 +442,23 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " });\n";
$content .= " }\n";
$content .= " if (enum_value !== undefined) {\n";
$content .= " const result = this.__{$column}_enum_val[enum_value];\n";
$content .= " const result = this.__{$column}__enum[enum_value];\n";
$content .= " if (!result) {\n";
$content .= " console.error(`Invalid enum value '\${enum_value}' for {$column}`);\n";
$content .= " return null;\n";
$content .= " }\n";
$content .= " return result;\n";
$content .= " }\n";
$content .= " return this.__{$column}_enum_val;\n";
$content .= " return this.__{$column}__enum;\n";
$content .= " }\n\n";
// Generate enum_select() - Selectable items for dropdowns (respects selectable: false)
$content .= " static {$column}_enum_select() {\n";
$content .= " const fullData = this.{$column}_enum_val();\n";
$content .= " /**\n";
$content .= " * Get selectable options for {$column} dropdowns (excludes selectable:false items).\n";
$content .= " * @returns {Object} {id: label} pairs for dropdown options, sorted by 'order' property\n";
$content .= " */\n";
$content .= " static {$column}__enum_select() {\n";
$content .= " const fullData = this.{$column}__enum();\n";
$content .= " const data = {};\n";
$content .= " const order = [];\n";
$content .= " \n";
@@ -477,7 +489,11 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " }\n\n";
// Generate enum_labels() - Simple id => label map (all items, ignores selectable)
$content .= " static {$column}_enum_labels() {\n";
$content .= " /**\n";
$content .= " * Get all {$column} labels (includes non-selectable items).\n";
$content .= " * @returns {Object} {id: label} pairs for all enum values\n";
$content .= " */\n";
$content .= " static {$column}__enum_labels() {\n";
$content .= " const values = {};\n";
foreach ($enum_values as $value => $props) {
if (isset($props['label'])) {
@@ -490,7 +506,11 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " }\n\n";
// Generate enum_ids() - Array of all valid enum IDs
$content .= " static {$column}_enum_ids() {\n";
$content .= " /**\n";
$content .= " * Get all valid {$column} IDs.\n";
$content .= " * @returns {number[]} Array of all enum IDs\n";
$content .= " */\n";
$content .= " static {$column}__enum_ids() {\n";
$content .= " return [";
$ids = array_keys($enum_values);
$content .= implode(', ', array_map('json_encode', $ids));

View File

@@ -37,16 +37,16 @@ use RuntimeException;
* ]
* ];
*
* This provides magic properties and methods:
* - $model->status_label - Get label for current enum value
* - $model->status_constant - Get constant name for current value
* - $model->status_enum_val - Get all properties for current value
* This provides magic properties and methods (BEM-style double underscore):
* - $model->status__label - Get label for current enum value
* - $model->status__constant - Get constant name for current value
* - $model->status__badge - Get any custom property for current value
*
* Static methods (available in both PHP and JavaScript):
* - Model::status_enum_val() - Get all enum definitions with full metadata
* - Model::status_enum_select() - Get selectable items for dropdowns (respects selectable: false)
* - Model::status_enum_labels() - Get simple id => label lookup map
* - Model::status_enum_ids() - Get array of all valid enum IDs
* - Model::status__enum() - Get all enum definitions with full metadata
* - Model::status__enum_select() - Get selectable items for dropdowns (respects selectable: false)
* - Model::status__enum_labels() - Get simple id => label lookup map
* - Model::status__enum_ids() - Get array of all valid enum IDs
*/
#[Monoprogenic]
#[Instantiatable]
@@ -86,11 +86,11 @@ abstract class Rsx_Model_Abstract extends Model
/**
* Private helper to resolve enum magic properties and methods
*
* Handles (these are mirrored in JavaScript stubs):
* - field_enum_val() - Returns all enum definitions with full metadata
* - field_enum_select() - Returns selectable items for dropdowns
* - field_enum_labels() - Returns simple id => label map
* - field_enum_ids() - Returns array of all valid enum IDs
* Handles (these are mirrored in JavaScript stubs, BEM-style double underscore):
* - field__enum() - Returns all enum definitions with full metadata
* - field__enum_select() - Returns selectable items for dropdowns
* - field__enum_labels() - Returns simple id => label map
* - field__enum_ids() - Returns array of all valid enum IDs
*
* @param string $key The property/method being accessed
* @param mixed $value Optional value for filtering selectable items
@@ -119,13 +119,13 @@ abstract class Rsx_Model_Abstract extends Model
return $keyA <=> $keyB;
});
// field_enum_val() - All enum definitions with full metadata
if ($key == $column . '_enum_val') {
// field__enum() - All enum definitions with full metadata
if ($key == $column . '__enum') {
return $sorted_config;
}
// field_enum_select() - Selectable items for dropdowns (respects selectable: false)
if ($key == $column . '_enum_select') {
// field__enum_select() - Selectable items for dropdowns (respects selectable: false)
if ($key == $column . '__enum_select') {
$return = [];
foreach ($sorted_config as $k => $v) {
@@ -140,8 +140,8 @@ abstract class Rsx_Model_Abstract extends Model
return $return;
}
// field_enum_labels() - Simple id => label map (all items, ignores selectable)
if ($key == $column . '_enum_labels') {
// field__enum_labels() - Simple id => label map (all items, ignores selectable)
if ($key == $column . '__enum_labels') {
$return = [];
foreach ($sorted_config as $k => $v) {
if (isset($v['label'])) {
@@ -151,8 +151,8 @@ abstract class Rsx_Model_Abstract extends Model
return $return;
}
// field_enum_ids() - Array of all valid enum IDs
if ($key == $column . '_enum_ids') {
// field__enum_ids() - Array of all valid enum IDs
if ($key == $column . '__enum_ids') {
return array_keys($sorted_config);
}
}
@@ -164,39 +164,30 @@ abstract class Rsx_Model_Abstract extends Model
/**
* Magic getter for enum properties
*
* Provides access to:
* - field_label - Label for current enum value
* - field_constant - Constant name for current value
* - field_enum_val - All properties for current value
* - field_enum, field_enum_select, field_enum_ids - Via _get_static_magic
* Uses BEM-style double underscore to separate field from property:
* - field__label - Label for current enum value
* - field__constant - Constant name for current value
* - field__badge - Any custom property for current value
* - field__enum(), field__enum_select(), etc. - Via _get_static_magic
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
// Check for enum lookup functions: _enum, _enum_select, _enum_ids
// Check for enum lookup functions: __enum, __enum_select, __enum_ids
$static_call = self::_get_static_magic($key);
if ($static_call !== null) {
return $static_call;
}
// Look up enum properties related to current column value
// Look up enum properties related to current column value (BEM-style: field__property)
if (!empty(static::$enums)) {
foreach (static::$enums as $column => $enum_config) {
// $object->field_enum_val returns all properties for current value
if ($key == $column . '_enum_val') {
$current_value = $this->$column;
return isset(static::$enums[$column][$current_value])
? static::$enums[$column][$current_value]
: null;
}
// Look for specific enum property (e.g., field_label, field_constant)
// Look for specific enum property (e.g., field__label, field__constant)
foreach ($enum_config as $enum_val => $enum_properties) {
foreach ($enum_properties as $prop_name => $prop_value) {
if ($key == $column . '_' . $prop_name && $this->$column == $enum_val) {
if ($key == $column . '__' . $prop_name && $this->$column == $enum_val) {
return $prop_value;
}
}
@@ -219,18 +210,13 @@ abstract class Rsx_Model_Abstract extends Model
*/
public function __isset($key)
{
// Check for enum magic properties
// Check for enum magic properties (BEM-style: field__property)
if (!empty(static::$enums)) {
foreach (static::$enums as $column => $enum_config) {
// field_enum_val
if ($key == $column . '_enum_val') {
return true;
}
// field_label, field_constant, field_* (any custom enum property)
// field__label, field__constant, field__* (any custom enum property)
foreach ($enum_config as $enum_val => $enum_properties) {
foreach ($enum_properties as $prop_name => $prop_value) {
if ($key == $column . '_' . $prop_name) {
if ($key == $column . '__' . $prop_name) {
// Property exists if current column value matches this enum value
if ($this->$column == $enum_val) {
return true;
@@ -247,11 +233,11 @@ abstract class Rsx_Model_Abstract extends Model
/**
* Magic static method handler for enum methods
*
* Provides static access to (mirrored in JavaScript stubs):
* - Model::field_enum_val() - All enum definitions with full metadata
* - Model::field_enum_select() - Selectable items for dropdowns
* - Model::field_enum_labels() - Simple id => label map
* - Model::field_enum_ids() - Array of all valid enum IDs
* Provides static access to (mirrored in JavaScript stubs, BEM-style):
* - Model::field__enum() - All enum definitions with full metadata
* - Model::field__enum_select() - Selectable items for dropdowns
* - Model::field__enum_labels() - Simple id => label map
* - Model::field__enum_ids() - Array of all valid enum IDs
*
* @param string $key
* @param array $args
@@ -318,12 +304,12 @@ abstract class Rsx_Model_Abstract extends Model
}
}
// Add enum field extra data - ALL properties, not just label and constant
// Add enum field extra data - ALL properties (BEM-style: field__property)
foreach (static::$enums as $column => $definitions) {
if (isset($this->$column) && isset($definitions[$this->$column])) {
foreach ($definitions[$this->$column] as $prop => $value) {
// Add all enum properties to the export
$array[$column . '_' . $prop] = $value;
$array[$column . '__' . $prop] = $value;
}
}
}

View File

@@ -32,7 +32,7 @@ use App\RSpade\Core\Files\File_Storage_Model;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:54
* Generated on: 2025-12-26 01:29:30
* Table: _file_attachments
*
* @property int $id
@@ -59,19 +59,18 @@ use App\RSpade\Core\Files\File_Storage_Model;
* @property int $created_by
* @property int $updated_by
*
* @property-read string $file_type_id_label
* @property-read string $file_type_id_constant
* @property-read array $file_type_id_enum_val
* @property-read string $file_type_id__label
* @property-read string $file_type_id__constant
*
* @method static array file_type_id_enum_val() Get all enum definitions with full metadata
* @method static array file_type_id_enum_select() Get selectable items for dropdowns
* @method static array file_type_id_enum_labels() Get simple id => label map
* @method static array file_type_id_enum_ids() Get array of all valid enum IDs
* @method static array file_type_id__enum() Get all enum definitions with full metadata
* @method static array file_type_id__enum_select() Get selectable items for dropdowns
* @method static array file_type_id__enum_labels() Get simple id => label map
* @method static array file_type_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class File_Attachment_Model extends Rsx_Site_Model_Abstract
{
{
/**
* _AUTO_GENERATED_ Enum constants
*/
@@ -83,6 +82,7 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
const FILE_TYPE_DOCUMENT = 6;
const FILE_TYPE_OTHER = 7;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */

View File

@@ -16,7 +16,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:54
* Generated on: 2025-12-26 01:29:30
* Table: _file_storage
*
* @property int $id
@@ -30,7 +30,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @mixin \Eloquent
*/
class File_Storage_Model extends Rsx_Model_Abstract
{
{
// Required static properties from parent abstract class
public static $enums = [];
public static $rel = [];

View File

@@ -14,7 +14,7 @@ use App\RSpade\Core\Models\Region_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:54
* Generated on: 2025-12-26 01:29:30
* Table: countries
*
* @property int $id
@@ -32,7 +32,7 @@ use App\RSpade\Core\Models\Region_Model;
* @mixin \Eloquent
*/
class Country_Model extends Rsx_Model_Abstract
{
{
public static $enums = [];
protected $table = 'countries';

View File

@@ -12,7 +12,7 @@ use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:54
* Generated on: 2025-12-26 01:29:30
* Table: ip_addresses
*
* @property int $id
@@ -30,7 +30,7 @@ use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
* @mixin \Eloquent
*/
class Ip_Address_Model extends Rsx_System_Model_Abstract
{
{
/**
* Enum field definitions
* @var array

View File

@@ -24,7 +24,7 @@ use App\RSpade\Core\Session\Session;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:54
* Generated on: 2025-12-26 01:29:30
* Table: login_users
*
* @property int $id
@@ -40,21 +40,19 @@ use App\RSpade\Core\Session\Session;
* @property int $created_by
* @property int $updated_by
*
* @property-read string $status_id_label
* @property-read string $status_id_constant
* @property-read array $status_id_enum_val
* @property-read string $is_verified_label
* @property-read string $is_verified_constant
* @property-read array $is_verified_enum_val
* @property-read string $status_id__label
* @property-read string $status_id__constant
* @property-read string $is_verified__label
* @property-read string $is_verified__constant
*
* @method static array status_id_enum_val() Get all enum definitions with full metadata
* @method static array status_id_enum_select() Get selectable items for dropdowns
* @method static array status_id_enum_labels() Get simple id => label map
* @method static array status_id_enum_ids() Get array of all valid enum IDs
* @method static array is_verified_enum_val() Get all enum definitions with full metadata
* @method static array is_verified_enum_select() Get selectable items for dropdowns
* @method static array is_verified_enum_labels() Get simple id => label map
* @method static array is_verified_enum_ids() Get array of all valid enum IDs
* @method static array status_id__enum() Get all enum definitions with full metadata
* @method static array status_id__enum_select() Get selectable items for dropdowns
* @method static array status_id__enum_labels() Get simple id => label map
* @method static array status_id__enum_ids() Get array of all valid enum IDs
* @method static array is_verified__enum() Get all enum definitions with full metadata
* @method static array is_verified__enum_select() Get selectable items for dropdowns
* @method static array is_verified__enum_labels() Get simple id => label map
* @method static array is_verified__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
@@ -62,7 +60,7 @@ class Login_User_Model extends Rsx_Model_Abstract implements
\Illuminate\Contracts\Auth\Authenticatable,
\Illuminate\Contracts\Auth\Access\Authorizable,
\Illuminate\Contracts\Auth\CanResetPassword
{
{
/**
* _AUTO_GENERATED_ Enum constants
*/

View File

@@ -14,7 +14,7 @@ use App\RSpade\Core\Models\Country_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:54
* Generated on: 2025-12-26 01:29:30
* Table: regions
*
* @property int $id
@@ -31,7 +31,7 @@ use App\RSpade\Core\Models\Country_Model;
* @mixin \Eloquent
*/
class Region_Model extends Rsx_Model_Abstract
{
{
public static $enums = [];
protected $table = 'regions';

View File

@@ -14,7 +14,7 @@ use App\RSpade\Core\Models\User_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: sites
*
* @property int $id
@@ -31,7 +31,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class Site_Model extends Rsx_Model_Abstract
{
{
use SoftDeletes;
/**

View File

@@ -12,7 +12,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: user_invites
*
* @property int $id
@@ -28,7 +28,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
* @mixin \Eloquent
*/
class User_Invite_Model extends Rsx_Site_Model_Abstract
{
{
/**
* Enum field definitions
* @var array

View File

@@ -25,7 +25,7 @@ use App\RSpade\Core\Models\User_Profile_Model;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: users
*
* @property int $id
@@ -48,19 +48,18 @@ use App\RSpade\Core\Models\User_Profile_Model;
* @property string $invite_accepted_at
* @property string $invite_expires_at
*
* @property-read string $role_id_label
* @property-read string $role_id_constant
* @property-read array $role_id_enum_val
* @property-read string $role_id__label
* @property-read string $role_id__constant
*
* @method static array role_id_enum_val() Get all enum definitions with full metadata
* @method static array role_id_enum_select() Get selectable items for dropdowns
* @method static array role_id_enum_labels() Get simple id => label map
* @method static array role_id_enum_ids() Get array of all valid enum IDs
* @method static array role_id__enum() Get all enum definitions with full metadata
* @method static array role_id__enum_select() Get selectable items for dropdowns
* @method static array role_id__enum_labels() Get simple id => label map
* @method static array role_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class User_Model extends Rsx_Site_Model_Abstract
{
{
/**
* _AUTO_GENERATED_ Enum constants
*/
@@ -73,6 +72,7 @@ class User_Model extends Rsx_Site_Model_Abstract
const ROLE_VIEWER = 700;
const ROLE_DISABLED = 800;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */

View File

@@ -7,7 +7,7 @@ use App\RSpade\Core\Models\User_Model;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: user_permissions
*
* @property int $id
@@ -22,7 +22,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class User_Permission_Model extends Rsx_Model_Abstract
{
{
protected $table = 'user_permissions';
protected $fillable = []; // No mass assignment - always explicit

View File

@@ -35,7 +35,7 @@ use App\RSpade\Core\Models\User_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: user_profiles
*
* @property int $id
@@ -51,7 +51,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class User_Profile_Model extends Rsx_Model_Abstract
{
{
/**
* The table associated with the model
*

View File

@@ -13,7 +13,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: user_verifications
*
* @property int $id
@@ -27,19 +27,18 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @property int $created_by
* @property int $updated_by
*
* @property-read string $verification_type_id_label
* @property-read string $verification_type_id_constant
* @property-read array $verification_type_id_enum_val
* @property-read string $verification_type_id__label
* @property-read string $verification_type_id__constant
*
* @method static array verification_type_id_enum_val() Get all enum definitions with full metadata
* @method static array verification_type_id_enum_select() Get selectable items for dropdowns
* @method static array verification_type_id_enum_labels() Get simple id => label map
* @method static array verification_type_id_enum_ids() Get array of all valid enum IDs
* @method static array verification_type_id__enum() Get all enum definitions with full metadata
* @method static array verification_type_id__enum_select() Get selectable items for dropdowns
* @method static array verification_type_id__enum_labels() Get simple id => label map
* @method static array verification_type_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class User_Verification_Model extends Rsx_Model_Abstract
{
{
/**
* _AUTO_GENERATED_ Enum constants
*/
@@ -48,6 +47,7 @@ class User_Verification_Model extends Rsx_Model_Abstract
const VERIFICATION_TYPE_EMAIL_RECOVERY = 3;
const VERIFICATION_TYPE_SMS_RECOVERY = 4;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */

View File

@@ -17,7 +17,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: _search_indexes
*
* @property int $id
@@ -37,7 +37,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
* @mixin \Eloquent
*/
class Search_Index_Model extends Rsx_Site_Model_Abstract
{
{
// Required static properties from parent abstract class
public static $enums = [];
public static $rel = [];

View File

@@ -41,7 +41,7 @@ use App\RSpade\Core\Session\User_Agent;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: _sessions
*
* @property int $id
@@ -63,7 +63,7 @@ use App\RSpade\Core\Session\User_Agent;
* @mixin \Eloquent
*/
class Session extends Rsx_System_Model_Abstract
{
{
// Enum definitions (required by abstract parent)
public static $enums = [];

View File

@@ -6,7 +6,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-25 20:57:55
* Generated on: 2025-12-26 01:29:30
* Table: _flash_alerts
*
* @property int $id
@@ -18,19 +18,18 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @property int $updated_by
* @property string $updated_at
*
* @property-read string $type_id_label
* @property-read string $type_id_constant
* @property-read array $type_id_enum_val
* @property-read string $type_id__label
* @property-read string $type_id__constant
*
* @method static array type_id_enum_val() Get all enum definitions with full metadata
* @method static array type_id_enum_select() Get selectable items for dropdowns
* @method static array type_id_enum_labels() Get simple id => label map
* @method static array type_id_enum_ids() Get array of all valid enum IDs
* @method static array type_id__enum() Get all enum definitions with full metadata
* @method static array type_id__enum_select() Get selectable items for dropdowns
* @method static array type_id__enum_labels() Get simple id => label map
* @method static array type_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class Flash_Alert_Model extends Rsx_Model_Abstract
{
{
/**
* _AUTO_GENERATED_ Enum constants
*/
@@ -39,6 +38,7 @@ class Flash_Alert_Model extends Rsx_Model_Abstract
const TYPE_INFO = 3;
const TYPE_WARNING = 4;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */

View File

@@ -13,8 +13,16 @@ SYNOPSIS
DESCRIPTION
The enum system provides a powerful way to define predefined values for database
fields with associated metadata. It automatically generates constants, magic
properties, helper methods, and JavaScript equivalents for both PHP and JavaScript
code.
properties, helper methods, and JavaScript equivalents.
BEM-STYLE NAMING: All enum magic properties and methods use double underscore
to clearly separate field name from property/method name:
$user->role_id__label (not role_id_label)
User_Model::role_id__enum() (not role_id_enum_val)
This makes it immediately clear when accessing generated enum properties vs
regular model attributes, and enables reliable grep searches.
DEFINING ENUMS
@@ -56,63 +64,67 @@ DEFINING ENUMS
PHP MAGIC PROPERTIES (Instance)
For a model instance with an enum field, these properties are automatically available:
For a model instance with an enum field, these properties are automatically
available using BEM-style double underscore:
field_label Returns the label for the current value
$user->status_id = 1;
echo $user->status_label; // "Active"
field__label Returns the label for the current value
$user->status_id = 1;
echo $user->status_id__label; // "Active"
field_constant Returns the constant name for the current value
echo $user->status_constant; // "STATUS_ACTIVE"
field__constant Returns the constant name for the current value
echo $user->status_id__constant; // "STATUS_ACTIVE"
field_enum_val Returns ALL properties for the current value
$props = $user->status_enum_val;
// ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', ...]
field_[property] Returns any custom property for the current value
echo $user->status_badge; // "bg-success"
echo $user->status_visible_frontend; // true
field__[property] Returns any custom property for the current value
echo $user->status_id__badge; // "bg-success"
echo $user->status_id__visible_frontend; // true
STATIC METHODS (PHP and JavaScript)
These four methods are available as static methods on model classes in both PHP
and JavaScript. The JavaScript stubs are auto-generated to mirror PHP behavior.
These methods are available as static methods on model classes in both PHP
and JavaScript. Uses BEM-style double underscore naming.
Model::field_enum_val()
Model::field__enum()
Returns all enum definitions for a field with full metadata:
$statuses = User_Model::status_id_enum_val();
$statuses = User_Model::status_id__enum();
// [1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', ...], ...]
// JavaScript equivalent:
const statuses = User_Model.status_id_enum_val();
const statuses = User_Model.status_id__enum();
Model::field_enum_select()
Model::field__enum(id) [JavaScript only]
Returns single enum's metadata by ID, or null + console.error if invalid:
User_Model.status_id__enum(User_Model.STATUS_ACTIVE).badge // "bg-success"
User_Model.status_id__enum(1).selectable // true
User_Model.status_id__enum(999) // null + console.error
Model::field__enum_select()
Returns selectable items for dropdowns (respects 'selectable' and 'order'):
$options = User_Model::status_id_enum_select();
$options = User_Model::status_id__enum_select();
// [1 => 'Active', 2 => 'Inactive'] (excludes selectable: false items)
// JavaScript equivalent:
const options = User_Model.status_id_enum_select();
const options = User_Model.status_id__enum_select();
Model::field_enum_labels()
Model::field__enum_labels()
Returns simple id => label map (all items, ignores selectable flag):
$labels = User_Model::status_id_enum_labels();
$labels = User_Model::status_id__enum_labels();
// [1 => 'Active', 2 => 'Inactive', 3 => 'Archived']
// JavaScript equivalent:
const labels = User_Model.status_id_enum_labels();
const labels = User_Model.status_id__enum_labels();
Model::field_enum_ids()
Model::field__enum_ids()
Returns array of all valid enum IDs:
$ids = User_Model::status_id_enum_ids();
$ids = User_Model::status_id__enum_ids();
// [1, 2, 3]
// JavaScript equivalent:
const ids = User_Model.status_id_enum_ids();
const ids = User_Model.status_id__enum_ids();
PHP CONSTANTS
@@ -138,32 +150,61 @@ JAVASCRIPT ACCESS
Project_Model.STATUS_ACTIVE // 2
Project_Model.STATUS_PLANNING // 1
Static Methods (same as PHP - see STATIC METHODS section above)
Project_Model.status_enum_val() // Full enum definitions with metadata
Project_Model.status_enum_select() // Selectable items for dropdowns
Project_Model.status_enum_labels() // Simple id => label map
Project_Model.status_enum_ids() // Array of valid IDs
Static Methods (BEM-style double underscore)
Project_Model.status__enum() // Full enum definitions with metadata
Project_Model.status__enum(id) // Single enum metadata by ID
Project_Model.status__enum_select() // Selectable items for dropdowns
Project_Model.status__enum_labels() // Simple id => label map
Project_Model.status__enum_ids() // Array of valid IDs
Instance Properties (after fetch)
Instance Properties (after fetch, BEM-style)
const project = await Project_Model.fetch(1);
project.status // 2 (raw value)
project.status_label // "Active"
project.status_badge // "bg-success"
project.status // 2 (raw value)
project.status__label // "Active"
project.status__badge // "bg-success"
AJAX/JSON EXPORT
When models are converted to arrays/JSON, enum properties are automatically included:
When models are converted to arrays/JSON, enum properties are automatically
included using BEM-style naming:
$user->toArray() returns:
[
'id' => 1,
'status_id' => 1,
'status_id_label' => 'Active', // Added automatically
'status_id_constant' => 'STATUS_ACTIVE', // Added automatically
'status_id_badge' => 'bg-success', // Custom properties too
'status_id__label' => 'Active', // Added automatically
'status_id__constant' => 'STATUS_ACTIVE', // Added automatically
'status_id__badge' => 'bg-success', // Custom properties too
// ... all enum properties for current value
]
ANTI-ALIASING POLICY
RSpade considers aliasing an anti-pattern. The BEM-style naming exists
specifically to make enum properties grepable and self-documenting.
WRONG - Aliasing in fetch():
public static function fetch($id) {
$data = parent::fetch($id);
$data['type_label'] = $contact->type_id__label; // Alias - BAD
$data['type_icon'] = $contact->type_id__icon; // Alias - BAD
return $data;
}
RIGHT - Use full BEM-style names:
// In JavaScript, use the automatic property names:
contact.type_id__label
contact.type_id__icon
Why aliasing is harmful:
1. Makes grep searches unreliable (can't find all usages of type_id__label)
2. Adds no value (we're not paying by the byte in source code)
3. Creates maintenance burden (two names for the same thing)
4. Obscures the data source (is 'type_label' a DB column or computed?)
The fetch() function's purpose is SECURITY - removing private data that
the current user shouldn't see. It is not for aliasing or adding data.
ADVANCED FEATURES
Ordering
@@ -174,10 +215,11 @@ ADVANCED FEATURES
1 => ['label' => 'High', 'order' => 1],
2 => ['label' => 'Medium', 'order' => 2],
]
// enum_select() returns: [1 => 'High', 2 => 'Medium', 3 => 'Low']
// __enum_select() returns: [1 => 'High', 2 => 'Medium', 3 => 'Low']
Selective Options
Use 'selectable' => false to hide options from dropdowns while keeping them valid:
Use 'selectable' => false to hide options from dropdowns while keeping
them valid:
3 => [
'constant' => 'STATUS_ARCHIVED',
@@ -197,18 +239,18 @@ PRACTICAL APPLICATIONS
Populating Select Boxes
<!-- Blade template -->
<select name="status_id">
@foreach(User_Model::status_id_enum_select() as $id => $label)
@foreach(User_Model::status_id__enum_select() as $id => $label)
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</select>
Dynamic CSS Classes
<span class="badge {{ $auction->auction_status_badge }}">
{{ $auction->auction_status_label }}
<span class="badge {{ $auction->auction_status__badge }}">
{{ $auction->auction_status__label }}
</span>
Business Logic Flags
if ($auction->auction_status_can_bid) {
if ($auction->auction_status__can_bid) {
// Show bidding interface
}
@@ -223,7 +265,7 @@ PRACTICAL APPLICATIONS
]
// Check permissions
if (in_array('users.create', $user->role_permissions)) {
if (in_array('users.create', $user->role__permissions)) {
// User can create users
}
@@ -255,6 +297,7 @@ BEST PRACTICES
5. Keep enum values immutable - add new values, don't change existing
6. Document custom properties in your model
7. Run rsx:migrate:document_models after adding enums
8. NEVER alias enum properties - use full BEM-style names
EXAMPLE IMPLEMENTATION
@@ -312,15 +355,15 @@ EXAMPLE IMPLEMENTATION
// Usage in controller
if ($project->status === Project_Model::STATUS_IN_PROGRESS) {
if ($project->priority_days < 3) {
if ($project->priority__days < 3) {
// Escalate critical project
}
}
// Usage in Blade view
<div class="{{ $project->status_badge }}">
{{ $project->status_label }}
@if($project->status_can_edit)
<div class="{{ $project->status__badge }}">
{{ $project->status__label }}
@if($project->status__can_edit)
<button>Edit</button>
@endif
</div>
@@ -330,4 +373,4 @@ SEE ALSO
php artisan rsx:man model_fetch - Model fetching from JavaScript
php artisan rsx:man models - RSX model system overview
RSpade 1.0 September 2025 ENUMS(7)
RSpade 1.0 December 2025 ENUMS(7)

View File

@@ -774,6 +774,16 @@ INSTANTIATION METHODS
append: true, // Append instead of replace
});
Replacing Existing Components:
When called on an element with an existing component, .component()
destroys the old component and creates a new one. Class preservation:
- Removed: PascalCase component names (capital start, no __)
- Preserved: Utility classes (text-muted), BEM child classes (Parent__child)
- Preserved: All HTML attributes
This allows parent components to add BEM-style classes for targeting
(e.g., Parent__slot) that survive child component replacement.
Client-side (HTML attributes):
<div data-component="User_Card"
data-component-data='{"name": "John"}'>

View File

@@ -29,7 +29,7 @@ STATUS (as of 2025-11-23)
Implemented:
- fetch() and fetch_or_null() methods
- Lazy relationship loading (belongsTo, hasMany, morphTo, etc.)
- Enum properties on instances ({column}_{field} pattern)
- Enum properties on instances (BEM-style {column}__{field} pattern)
- Static enum constants and accessor methods
- Automatic model hydration from Ajax responses
- JavaScript class hierarchy with Base_* stubs
@@ -140,31 +140,34 @@ IMPLEMENTING FETCHABLE MODELS
}
}
Augmented Array Return (recommended for CRUD pages):
class Client_Model extends Rsx_Model
Augmented Array Return (for model method outputs only):
class Contact_Model extends Rsx_Model
{
// Model methods that produce NEW computed values
public function full_name(): string
{
return trim($this->first_name . ' ' . $this->last_name);
}
public function mailto_link(): string
{
return '<a href="mailto:' . e($this->email) . '">' . e($this->email) . '</a>';
}
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
$client = static::withTrashed()->find($id);
if (!$client) {
$contact = static::withTrashed()->find($id);
if (!$contact) {
return false;
}
// Start with model's toArray() to get __MODEL and base data
$data = $client->toArray();
$data = $contact->toArray();
// Augment with computed/formatted fields
$data['status_label'] = ucfirst($client->status);
$data['status_badge'] = match($client->status) {
'active' => 'bg-success',
'inactive' => 'bg-secondary',
default => 'bg-warning'
};
$data['created_at_formatted'] = $client->created_at->format('M d, Y');
$data['created_at_human'] = $client->created_at->diffForHumans();
$data['region_name'] = $client->region_name();
$data['country_name'] = $client->country_name();
// Augment ONLY with outputs from defined model methods
$data['full_name'] = $contact->full_name();
$data['mailto_link'] = $contact->mailto_link();
return $data;
}
@@ -172,7 +175,6 @@ IMPLEMENTING FETCHABLE MODELS
IMPORTANT: Always start with $model->toArray() when augmenting data.
This preserves the __MODEL property needed for JavaScript hydration.
The result is a hydrated model instance with your extra fields added.
DO NOT manually construct the return array like this (outdated pattern):
return [
@@ -182,6 +184,87 @@ IMPLEMENTING FETCHABLE MODELS
];
This loses __MODEL and returns a plain object instead of a model instance.
FETCH() IS FOR SECURITY, NOT ALIASING
The fetch() method exists for ONE purpose: security filtering - removing private
data that the current user shouldn't see. It is NOT for:
- Renaming fields (aliasing)
- Formatting dates (use Rsx_Date/Rsx_Time on client)
- Adding computed properties that should be in enums
- Reshaping data for frontend convenience
ANTI-ALIASING POLICY
RSpade considers aliasing in fetch() an anti-pattern. Enum magic properties use
BEM-style double underscore naming specifically to be grepable and self-documenting.
Why aliasing is harmful:
1. Makes grep searches unreliable (can't find all usages of type_id__label)
2. Adds no value (we're not paying by the byte in source code)
3. Creates maintenance burden (two names for the same thing)
4. Obscures the data source (is 'type_label' a DB column or computed?)
VALID PATTERNS IN FETCH()
1. Security removal (the primary purpose):
unset($data['password_hash']);
unset($data['api_secret']);
2. Model method with MATCHING name:
$data['full_name'] = $model->full_name();
$data['formatted_address'] = $model->formatted_address();
$data['unread_count'] = $model->unread_count();
The key MUST match the method name. This ensures:
- Single source of truth (method defines the computation)
- Same name in PHP and JavaScript
- Grepable across codebase
3. Conditional with matching property or method:
$data['foo'] = $condition ? $model->foo : null;
$data['jazz'] = $model->jazz_allowed() ? $model->jazz : 'Not permitted';
$data['secret'] = $user->is_admin ? $model->secret : '[REDACTED]';
Both sides of the ternary must use matching property/method names
or be literal values. This allows conditional defaults or permission-
based field masking.
INVALID PATTERNS (Violations)
1. Aliasing enum properties:
$data['type_label'] = $model->type_id__label; // BAD - use full name
$data['status_badge'] = $model->status_id__badge; // BAD - use full name
2. Date formatting:
$data['created_formatted'] = $model->created_at->format('M d'); // BAD
$data['updated_human'] = $model->updated_at->diffForHumans(); // BAD
// Dates are YYYY-mm-dd, datetimes are ISO UTC - format on client!
3. Relationship plucking without method:
$data['client_name'] = $model->client->name; // BAD - make a method
4. Inline computations:
$data['is_owner'] = $model->user_id === Session::get_user_id(); // BAD
// Make a method: $data['is_owner'] = $model->is_owner();
5. Redundant explicit assignments:
$data['id'] = $model->id; // UNNECESSARY - already in toArray()
$data['name'] = $model->name; // UNNECESSARY - already in toArray()
6. Mismatched method names:
$data['addr'] = $model->formatted_address(); // BAD - name must match
// CORRECT: $data['formatted_address'] = $model->formatted_address();
CLIENT-SIDE PATTERNS
Use full BEM-style names in JavaScript:
contact.type_id__label
contact.type_id__icon
Format dates on client:
Rsx_Date.format(contact.created_at) // "Dec 24, 2025"
Rsx_Time.relative(contact.updated_at) // "2 hours ago"
JAVASCRIPT USAGE
Single Record Fetching:
// fetch() throws if record not found - no need to check for null/false
@@ -205,13 +288,13 @@ JAVASCRIPT USAGE
- fetch() - View/edit pages where record MUST exist (throws on not found)
- fetch_or_null() - Optional lookups where missing is valid (returns null)
Enum Properties on Instances:
Enum Properties on Instances (BEM-style double underscore):
const project = await Project_Model.fetch(1);
// All enum helper properties from PHP are available
console.log(project.status_id); // 2 (raw value)
console.log(project.status_id_label); // "Active"
console.log(project.status_id_badge); // "bg-success"
console.log(project.status_id); // 2 (raw value)
console.log(project.status_id__label); // "Active"
console.log(project.status_id__badge); // "bg-success"
Static Enum Constants:
// Constants available on the class
@@ -220,13 +303,17 @@ JAVASCRIPT USAGE
}
// Get all enum values for dropdowns
const statusOptions = Project_Model.status_id_enum_select();
const statusOptions = Project_Model.status_id__enum_select();
// {1: "Planning", 2: "Active", 3: "On Hold", ...}
// Get full enum config
const statusConfig = Project_Model.status_id_enum_val();
// Get full enum config with all metadata
const statusConfig = Project_Model.status_id__enum();
// {1: {label: "Planning", badge: "bg-info"}, ...}
// Get single enum's metadata by ID
const activeConfig = Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE);
// {label: "Active", badge: "bg-success", ...}
Error Handling:
// In SPA actions, errors bubble up to Universal_Error_Page_Component
// No try/catch needed - just call fetch() and use the result
@@ -266,9 +353,12 @@ JAVASCRIPT CLASS ARCHITECTURE
static STATUS_PLANNING = 1;
static STATUS_ACTIVE = 2;
// Enum accessor methods
static status_enum_val() { ... } // Full enum config
static status_enum_select() { ... } // For dropdown population
// Enum accessor methods (BEM-style double underscore)
static status__enum() { ... } // Full enum config (all values)
static status__enum(id) { ... } // Single enum's metadata by ID
static status__enum_select() { ... } // For dropdown population
static status__enum_labels() { ... } // Simple id => label map
static status__enum_ids() { ... } // Array of valid IDs
// Relationship discovery
static get_relationships() { ... } // Returns array of names
@@ -618,7 +708,7 @@ LAZY RELATIONSHIPS
ENUM PROPERTIES
Enum values are exposed as properties on fetched model instances, mirroring
the PHP magic property behavior. Each custom field defined in the enum
becomes a property named {column}_{field}.
becomes a property using BEM-style naming: {column}__{field} (double underscore).
PHP Enum Definition:
public static $enums = [
@@ -629,19 +719,19 @@ ENUM PROPERTIES
],
];
Resulting JavaScript Instance Properties:
Resulting JavaScript Instance Properties (BEM-style double underscore):
const project = await Project_Model.fetch(123);
// Raw enum value
project.status_id // 2
project.status_id // 2
// Auto-generated properties from enum definition
project.status_id_label // "Active"
project.status_id_badge // "bg-success"
project.status_id__label // "Active"
project.status_id__badge // "bg-success"
// All custom fields become properties
// If enum had 'button_class' => 'btn-success':
project.status_id_button_class // "btn-success"
project.status_id__button_class // "btn-success"
Static Enum Constants:
// Constants available on the class (from 'constant' field)
@@ -649,14 +739,26 @@ ENUM PROPERTIES
console.log('Project is active');
}
Static Enum Methods:
Static Enum Methods (BEM-style double underscore):
// Get full enum config (all values with metadata)
const statusConfig = Project_Model.status_id__enum();
// {1: {label: "Planning", badge: "bg-info"}, 2: {...}, ...}
// Get single enum's metadata by ID
const activeConfig = Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE);
// {label: "Active", badge: "bg-success", ...}
// Get enum values for dropdown population (id => label)
const statusOptions = Project_Model.status_id_enum_select();
const statusOptions = Project_Model.status_id__enum_select();
// {1: "Planning", 2: "Active", 3: "On Hold"}
// Get full enum config (id => all fields)
const statusConfig = Project_Model.status_id_enum_val();
// {1: {label: "Planning", badge: "bg-info"}, 2: {...}, ...}
// Get simple id => label map (all items)
const labels = Project_Model.status_id__enum_labels();
// {1: "Planning", 2: "Active", 3: "On Hold"}
// Get array of valid IDs
const ids = Project_Model.status_id__enum_ids();
// [1, 2, 3]
MODEL CONSTANTS
All public constants defined on a PHP model are automatically exported to

View File

@@ -0,0 +1,298 @@
BEM-STYLE ENUM MAGIC PROPERTY NAMING - MIGRATION GUIDE
Date: 2025-12-26
SUMMARY
Enum magic properties and methods now use BEM-style double underscore naming
to separate field names from property/method names. This makes enum properties
immediately distinguishable from regular model attributes and enables reliable
grep searches across the codebase.
Old: $model->status_id_label, Model::status_id_enum_val()
New: $model->status_id__label, Model::status_id__enum()
AFFECTED FILES
All files that access enum magic properties or static enum methods:
- PHP files using $model->field_property magic properties
- PHP files using Model::field_enum_val(), field_enum_select(), etc.
- JavaScript files using model.field_property instance properties
- JavaScript files using Model.field_enum_val(), field_enum_select(), etc.
- Blade templates with enum property access
- jqhtml templates with enum property access
CHANGES REQUIRED
1. Instance Magic Properties (PHP and JavaScript)
Change single underscore between field and property to double underscore.
BEFORE:
$project->status_id_label
$project->status_id_badge
$user->role_id_permissions
project.status_id_label
user.role_id_badge
AFTER:
$project->status_id__label
$project->status_id__badge
$user->role_id__permissions
project.status_id__label
user.role_id__badge
2. Static Enum Methods (PHP)
Change single underscore to double underscore before method suffix.
BEFORE:
User_Model::role_id_enum_val()
User_Model::role_id_enum_select()
User_Model::role_id_enum_labels()
User_Model::role_id_enum_ids()
AFTER:
User_Model::role_id__enum()
User_Model::role_id__enum_select()
User_Model::role_id__enum_labels()
User_Model::role_id__enum_ids()
3. Static Enum Methods (JavaScript)
Change single underscore to double underscore before method suffix.
Note: field_enum_val() is renamed to field__enum().
BEFORE:
Project_Model.status_id_enum_val()
Project_Model.status_id_enum_val(Project_Model.STATUS_ACTIVE)
Project_Model.status_id_enum_select()
Project_Model.status_id_enum_labels()
Project_Model.status_id_enum_ids()
AFTER:
Project_Model.status_id__enum()
Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE)
Project_Model.status_id__enum_select()
Project_Model.status_id__enum_labels()
Project_Model.status_id__enum_ids()
4. Model Docblocks
Run the document_models command to regenerate docblocks with new naming.
php artisan rsx:migrate:document_models
This updates @property-read and @method annotations in model files.
5. Remove Instance field_enum_val Property Access (REMOVED FEATURE)
The instance property $model->field_enum_val has been removed entirely.
Use the static method instead: Model::field__enum($model->field)
BEFORE:
$current_enum = $project->status_id_enum_val;
$badge = $current_enum['badge'];
AFTER:
$badge = $project->status_id__badge;
// Or for full metadata:
$current_enum = Project_Model::status_id__enum($project->status_id);
SEARCH AND REPLACE PATTERNS
Use these regex patterns to find occurrences that need updating:
PHP instance properties:
Search: \$\w+->\w+_(?:label|badge|constant|icon|[a-z_]+)(?!\w)
(Review each match - only change if it's an enum property)
PHP static methods:
Search: ::\w+_enum_val\(
Replace: ::$1__enum(
Search: ::\w+_enum_select\(
Replace: ::$1__enum_select(
Search: ::\w+_enum_labels\(
Replace: ::$1__enum_labels(
Search: ::\w+_enum_ids\(
Replace: ::$1__enum_ids(
JavaScript instance properties:
Search: \.\w+_(?:label|badge|constant|icon|[a-z_]+)(?!\w|_)
(Review each match - only change if it's an enum property)
JavaScript static methods:
Search: \.\w+_enum_val\(
Replace: .$1__enum(
FETCH() ANTI-ALIASING POLICY (ENFORCED BY PHP-ALIAS-01)
fetch() exists for SECURITY (removing private data), not aliasing.
The PHP-ALIAS-01 code quality rule now enforces this at manifest-time.
VALID PATTERNS IN FETCH()
1. Security removal (the primary purpose of fetch):
$data = $model->toArray();
unset($data['password_hash']);
unset($data['api_secret']);
return $data;
2. Model method with MATCHING name:
// Method name MUST match the key
$data['full_name'] = $model->full_name();
$data['formatted_address'] = $model->formatted_address();
$data['unread_count'] = $model->unread_count();
// The method must be defined on the model:
public function full_name(): string {
return trim($this->first_name . ' ' . $this->last_name);
}
3. Conditional with matching property/method or literals:
// Both ternary branches must use matching name or be literals
$data['secret'] = $user->is_admin ? $model->secret : null;
$data['jazz'] = $model->jazz_allowed() ? $model->jazz : 'Not permitted';
$data['notes'] = $can_view ? $model->notes : '[REDACTED]';
INVALID PATTERNS (Violations)
1. Enum property aliasing:
WRONG:
$data['type_label'] = $model->type_id__label;
$data['status_badge'] = $model->status_id__badge;
RIGHT:
// Don't add these - they're already in toArray() automatically
// Access them in JavaScript with full BEM-style names:
contact.type_id__label
contact.status_id__badge
2. Date formatting:
WRONG:
$data['created_formatted'] = $model->created_at->format('M d, Y');
$data['updated_human'] = $model->updated_at->diffForHumans();
RIGHT:
// Dates are always YYYY-mm-dd, datetimes are ISO UTC
// Format on client with Rsx_Date/Rsx_Time:
Rsx_Date.format(contact.created_at)
Rsx_Time.relative(contact.updated_at)
3. Relationship plucking without method:
WRONG:
$data['client_name'] = $model->client->name;
RIGHT:
// Create a model method:
public function client_name(): ?string {
return $this->client?->name;
}
// Then in fetch:
$data['client_name'] = $model->client_name();
4. Inline computations:
WRONG:
$data['is_owner'] = $model->user_id === Session::get_user_id();
RIGHT:
// Create a model method:
public function is_owner(): bool {
return $this->user_id === Session::get_user_id();
}
// Then in fetch:
$data['is_owner'] = $model->is_owner();
5. Redundant explicit assignments:
WRONG:
$data['id'] = $model->id;
$data['name'] = $model->name;
RIGHT:
// Remove these lines - toArray() already includes all fields
6. Mismatched method names:
WRONG:
$data['addr'] = $model->formatted_address();
RIGHT:
$data['formatted_address'] = $model->formatted_address();
REFACTORING EXISTING CODE
If you have fetch() methods with aliasing, refactor as follows:
Step 1: Identify the alias type
- Enum property alias → Remove it (automatic via toArray)
- Date formatting → Remove it (format on client)
- Computed value → Extract to model method
- Redundant assignment → Remove it
Step 2: For computed values, create model methods
BEFORE (in fetch):
$data['display_name'] = $model->first_name . ' ' . $model->last_name;
$data['is_expired'] = $model->expires_at < now();
AFTER (in model):
public function display_name(): string {
return $this->first_name . ' ' . $this->last_name;
}
public function is_expired(): bool {
return $this->expires_at < now();
}
AFTER (in fetch):
$data['display_name'] = $model->display_name();
$data['is_expired'] = $model->is_expired();
Step 3: Update JavaScript to use proper names
BEFORE:
contact.type_label
project.created_formatted
AFTER:
contact.type_id__label
Rsx_Date.format(project.created_at)
ESCAPE HATCH: CONTROLLER AJAX ENDPOINTS
If you truly need custom response formatting that doesn't fit the model
serialization pattern, use a controller Ajax endpoint instead. Controllers
are NOT checked by PHP-ALIAS-01 - they are the escape hatch for custom
responses like reference data formatting:
// This is fine in a controller:
#[Ajax_Endpoint]
public static function get_options(...) {
return Model::all()->map(fn($m) => [
'value' => $m->id,
'label' => $m->name,
]);
}
VERIFICATION
1. Search for old patterns:
grep -r "_enum_val\(" rsx/
grep -r "_enum_select\(" rsx/
grep -r "_label[^_]" rsx/ # Review matches for enum properties
2. Verify bundle compiles without errors:
php artisan rsx:bundle:compile --all
3. Run the application and verify:
- Dropdowns populate correctly (using __enum_select())
- Labels display correctly (using __label properties)
- Badge classes apply correctly (using __badge properties)
REFERENCE
php artisan rsx:man enum - Complete enum documentation
php artisan rsx:man model_fetch - Model fetch and anti-aliasing policy