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

0
.env.dist Normal file → Executable file
View File

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) - **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) - **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) - **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()`. 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; 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, * fetch() exists for SECURITY (removing private data), not aliasing.
* which creates confusion about what the field actually represents.
* *
* VIOLATION - Dropping parts from a name (confusing): * VALID PATTERNS:
* 'type_label' => $contact->type_id_label, // Dropped "id" - what happened to it? * 1. Model method with MATCHING name:
* 'client_label' => $record->client_id_label, // Dropped "id" - confusing * 'full_name' => $model->full_name()
* 'name' => $user->display_name, // Dropped "display" - loses context * 'unread_count' => $this->unread_count()
* *
* ALLOWED - Renaming to a completely different concept: * 2. Conditional with matching property/method or literals:
* 'value' => $client->id, // "value" is a UI concept, not a shortened "id" * 'foo' => $condition ? $model->foo : null
* 'label' => $client->name, // "label" is a UI concept, not a shortened "name" * 'secret' => $user->is_admin ? $model->secret : '[REDACTED]'
* 'client_id' => $client->id, // Adding context, not dropping it
* 'id' => $client->id, // Same name - no aliasing
* *
* ALLOWED - Transformations: * INVALID PATTERNS:
* 'type_id_label_upper' => strtoupper($contact->type_id_label), * 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 * 2. Method call with mismatched name:
* PROPER SUBSET of the source property's parts (all key parts exist in source, * 'addr' => $model->formatted_address() // BAD - name must match
* but source has additional parts). This catches "dropping parts" without *
* flagging legitimate renames to different concepts. * 3. Redundant explicit assignments (unnecessary):
* 'id' => $model->id // Already in toArray()
* *
* Applies to: * Applies to:
* - Controller methods with #[Ajax_Endpoint] attribute
* - Model fetch() methods with #[Ajax_Endpoint_Model_Fetch] 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 class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
{ {
@@ -48,7 +49,7 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
*/ */
public function get_name(): string 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 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 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; return;
} }
// Determine file type and get relevant methods // Only check models - controllers are an escape hatch for custom responses
$extends = $metadata['extends'] ?? null; $extends = $metadata['extends'] ?? null;
$methods_to_check = [];
if ($extends === 'Rsx_Controller_Abstract') { if ($extends !== 'Rsx_Model_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
return; return;
} }
// Check fetch() method with #[Ajax_Endpoint_Model_Fetch]
$methods_to_check = $this->get_model_fetch_methods($metadata);
if (empty($methods_to_check)) { if (empty($methods_to_check)) {
return; 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 * 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); $original_lines = explode("\n", $original_contents);
foreach ($lines as $offset => $line) { foreach ($lines as $offset => $line) {
// Pattern: 'key' => $var->property or 'key' => $var['property'] $actual_line_num = $method_start_line + $offset;
// We need to detect when key != property (with no transformation)
// Match: 'key_name' => $something->property_name // Check for line-level exception
// or: 'key_name' => $something['property_name'] if ($this->line_has_exception($original_lines, $actual_line_num)) {
// Without function wrapping continue;
}
// Pattern for object property access: 'key' => $var->prop // Pattern: 'key' => ...
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)) { // We need to analyze what's on the right side
$key = $matches[1]; if (!preg_match("/['\"]([a-zA-Z_][a-zA-Z0-9_]*)['\"]\\s*=>/", $line, $key_match)) {
$property = $matches[2]; continue;
}
if ($this->is_problematic_alias($key, $property) && !$this->has_transformation($line, $matches[0])) { $key = $key_match[1];
// Check for line-level exception
$actual_line_num = $method_start_line + $offset;
if ($this->line_has_exception($original_lines, $actual_line_num)) {
continue;
}
// 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( $this->add_violation(
$file_path, $file_path,
$actual_line_num, $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), trim($line),
$this->build_suggestion($key, $property), $this->build_method_mismatch_suggestion($key, $method_called),
'high' 'high'
); );
} }
continue;
} }
// Pattern for array access: 'key' => $var['prop'] // Check for property access: $var->property or $var['property']
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)) { $property = null;
$key = $matches[1];
$property = $matches[2];
if ($this->is_problematic_alias($key, $property) && !$this->has_transformation($line, $matches[0])) { // Object property: $var->prop
// Check for line-level exception if (preg_match('/\$([a-zA-Z_][a-zA-Z0-9_]*)->([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*[,;\]\)]|$)/', $value_part, $prop_match)) {
$actual_line_num = $method_start_line + $offset; $property = $prop_match[2];
if ($this->line_has_exception($original_lines, $actual_line_num)) { }
continue; // 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( $this->add_violation(
$file_path, $file_path,
$actual_line_num, $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), 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' 'high'
); );
} }
@@ -252,85 +253,112 @@ class FieldAliasing_CodeQualityRule extends CodeQualityRule_Abstract
} }
/** /**
* Check if the key is a problematic alias of the property * Check if expression contains a ternary operator (not inside a string)
*
* 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)
*/ */
private function is_problematic_alias(string $key, string $property): bool private function is_ternary_expression(string $value): bool
{ {
// Same name is never a violation // Remove string contents to avoid false positives
if ($key === $property) { $no_strings = preg_replace('/(["\'])(?:[^\\\\]|\\\\.)*?\1/', '', $value);
return false; return str_contains($no_strings, '?') && str_contains($no_strings, ':');
}
// 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;
} }
/** /**
* 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 // Extract the true and false branches
$pos = strpos($line, $matched_portion); // This is simplified - a full parser would be needed for nested ternaries
if ($pos === false) { $no_strings = preg_replace('/(["\'])(?:[^\\\\]|\\\\.)*?\1/', '""', $value_part);
return false;
// 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 $true_branch = trim(substr($value_part, $q_pos + 1, $c_pos - $q_pos - 1));
if (preg_match("/=>\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(/", $line, $fn_match)) { $false_branch = trim(substr($value_part, $c_pos + 1));
// There's a function call before the value
// 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; return true;
} }
// Check for method chaining or casting // Method call: $var->method() - method must match key
if (preg_match("/=>\\s*\\([^)]+\\)\\s*\\$/", $line)) { if (preg_match('/\$[a-zA-Z_][a-zA-Z0-9_]*->([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/', $branch, $m)) {
// Cast like (string)$var->prop 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; return true;
} }
// Check for string concatenation // Number
if (preg_match("/=>\\s*['\"].*['\"]\\s*\\.\\s*\\$/", $line) || preg_match("/\\$[^,]+\\.\\s*['\"]/", $line)) { if (is_numeric($value)) {
return true; return true;
} }
// Check for ternary operator // String literal
if (strpos($line, '?') !== false && strpos($line, ':') !== false) { if (preg_match('/^(["\']).*\1$/', $value)) {
return true; return true;
} }
// Check for null coalescing // Empty array
if (strpos($line, '??') !== false) { if ($value === '[]') {
return true; 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 = []; return implode("\n", [
$suggestions[] = "PROBLEM: Field name shortened by dropping parts."; "PROBLEM: Method call key doesn't match method name.",
$suggestions[] = ""; "",
$suggestions[] = "The key '{$key}' contains only some parts of '{$property}'."; "fetch() anti-aliasing policy requires method keys to match method names.",
$suggestions[] = "This is confusing because it obscures what was removed."; "This ensures a single source of truth and consistent naming across PHP/JS.",
$suggestions[] = ""; "",
$suggestions[] = "FIX: Use the full property name:"; "FIX: Use the method name as the key:",
$suggestions[] = ""; "",
$suggestions[] = " // WRONG - parts dropped, confusing"; " // WRONG",
$suggestions[] = " '{$key}' => \$model->{$property},"; " '{$key}' => \$model->{$method}(),",
$suggestions[] = ""; "",
$suggestions[] = " // CORRECT - full name preserved"; " // CORRECT",
$suggestions[] = " '{$property}' => \$model->{$property},"; " '{$method}' => \$model->{$method}(),",
$suggestions[] = ""; "",
$suggestions[] = "NOTE: Renaming to a DIFFERENT concept is allowed:"; "See: php artisan rsx:man model_fetch",
$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", $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)) { if (property_exists($className, 'enums') && !empty($className::$enums)) {
foreach ($className::$enums as $columnName => $enumDefinitions) { foreach ($className::$enums as $columnName => $enumDefinitions) {
// Add enum accessor properties (instance properties for current value) // Add enum accessor properties (BEM-style: field__property)
$enumProperties[] = " * @property-read string \${$columnName}_label"; $enumProperties[] = " * @property-read string \${$columnName}__label";
$enumProperties[] = " * @property-read string \${$columnName}_constant"; $enumProperties[] = " * @property-read string \${$columnName}__constant";
$enumProperties[] = " * @property-read array \${$columnName}_enum_val";
// Add enum static methods (mirrored in JavaScript stubs) // Add enum static methods (BEM-style, mirrored in JavaScript stubs)
$enumMethods[] = " * @method static array {$columnName}_enum_val() Get all enum definitions with full metadata"; $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_select() Get selectable items for dropdowns";
$enumMethods[] = " * @method static array {$columnName}_enum_labels() Get simple id => label map"; $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"; $enumMethods[] = " * @method static array {$columnName}__enum_ids() Get array of all valid enum IDs";
// Generate constants for each enum value // Generate constants for each enum value
foreach ($enumDefinitions as $value => $definition) { 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 * _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 * Table: _api_keys
* *
* @property int $id * @property int $id
@@ -53,7 +53,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Api_Key_Model extends Rsx_System_Model_Abstract class Api_Key_Model extends Rsx_System_Model_Abstract
{ {
protected $table = '_api_keys'; protected $table = '_api_keys';
public static $enums = []; public static $enums = [];

View File

@@ -402,10 +402,18 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= "\n"; $content .= "\n";
} }
// Generate enum value getter with Proxy for maintaining order // Generate enum getter with Proxy for maintaining order (BEM-style: field__enum)
$content .= " static __{$column}_enum_val = null;\n"; $content .= " /**\n";
$content .= " static {$column}_enum_val(enum_value) {\n"; $content .= " * Get enum metadata for {$column}.\n";
$content .= " if (!this.__{$column}_enum_val) {\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 data = {};\n";
$content .= " const order = [];\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 .= " // 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 .= " ownKeys() {\n";
$content .= " return order.map(String);\n"; $content .= " return order.map(String);\n";
$content .= " },\n"; $content .= " },\n";
@@ -434,19 +442,23 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " });\n"; $content .= " });\n";
$content .= " }\n"; $content .= " }\n";
$content .= " if (enum_value !== undefined) {\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 .= " if (!result) {\n";
$content .= " console.error(`Invalid enum value '\${enum_value}' for {$column}`);\n"; $content .= " console.error(`Invalid enum value '\${enum_value}' for {$column}`);\n";
$content .= " return null;\n"; $content .= " return null;\n";
$content .= " }\n"; $content .= " }\n";
$content .= " return result;\n"; $content .= " return result;\n";
$content .= " }\n"; $content .= " }\n";
$content .= " return this.__{$column}_enum_val;\n"; $content .= " return this.__{$column}__enum;\n";
$content .= " }\n\n"; $content .= " }\n\n";
// Generate enum_select() - Selectable items for dropdowns (respects selectable: false) // Generate enum_select() - Selectable items for dropdowns (respects selectable: false)
$content .= " static {$column}_enum_select() {\n"; $content .= " /**\n";
$content .= " const fullData = this.{$column}_enum_val();\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 data = {};\n";
$content .= " const order = [];\n"; $content .= " const order = [];\n";
$content .= " \n"; $content .= " \n";
@@ -477,7 +489,11 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " }\n\n"; $content .= " }\n\n";
// Generate enum_labels() - Simple id => label map (all items, ignores selectable) // 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"; $content .= " const values = {};\n";
foreach ($enum_values as $value => $props) { foreach ($enum_values as $value => $props) {
if (isset($props['label'])) { if (isset($props['label'])) {
@@ -490,7 +506,11 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " }\n\n"; $content .= " }\n\n";
// Generate enum_ids() - Array of all valid enum IDs // 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 ["; $content .= " return [";
$ids = array_keys($enum_values); $ids = array_keys($enum_values);
$content .= implode(', ', array_map('json_encode', $ids)); $content .= implode(', ', array_map('json_encode', $ids));

View File

@@ -37,16 +37,16 @@ use RuntimeException;
* ] * ]
* ]; * ];
* *
* This provides magic properties and methods: * This provides magic properties and methods (BEM-style double underscore):
* - $model->status_label - Get label for current enum value * - $model->status__label - Get label for current enum value
* - $model->status_constant - Get constant name for current value * - $model->status__constant - Get constant name for current value
* - $model->status_enum_val - Get all properties for current value * - $model->status__badge - Get any custom property for current value
* *
* Static methods (available in both PHP and JavaScript): * Static methods (available in both PHP and JavaScript):
* - Model::status_enum_val() - Get all enum definitions with full metadata * - 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_select() - Get selectable items for dropdowns (respects selectable: false)
* - Model::status_enum_labels() - Get simple id => label lookup map * - Model::status__enum_labels() - Get simple id => label lookup map
* - Model::status_enum_ids() - Get array of all valid enum IDs * - Model::status__enum_ids() - Get array of all valid enum IDs
*/ */
#[Monoprogenic] #[Monoprogenic]
#[Instantiatable] #[Instantiatable]
@@ -86,11 +86,11 @@ abstract class Rsx_Model_Abstract extends Model
/** /**
* Private helper to resolve enum magic properties and methods * Private helper to resolve enum magic properties and methods
* *
* Handles (these are mirrored in JavaScript stubs): * Handles (these are mirrored in JavaScript stubs, BEM-style double underscore):
* - field_enum_val() - Returns all enum definitions with full metadata * - field__enum() - Returns all enum definitions with full metadata
* - field_enum_select() - Returns selectable items for dropdowns * - field__enum_select() - Returns selectable items for dropdowns
* - field_enum_labels() - Returns simple id => label map * - field__enum_labels() - Returns simple id => label map
* - field_enum_ids() - Returns array of all valid enum IDs * - field__enum_ids() - Returns array of all valid enum IDs
* *
* @param string $key The property/method being accessed * @param string $key The property/method being accessed
* @param mixed $value Optional value for filtering selectable items * @param mixed $value Optional value for filtering selectable items
@@ -119,13 +119,13 @@ abstract class Rsx_Model_Abstract extends Model
return $keyA <=> $keyB; return $keyA <=> $keyB;
}); });
// field_enum_val() - All enum definitions with full metadata // field__enum() - All enum definitions with full metadata
if ($key == $column . '_enum_val') { if ($key == $column . '__enum') {
return $sorted_config; return $sorted_config;
} }
// field_enum_select() - Selectable items for dropdowns (respects selectable: false) // field__enum_select() - Selectable items for dropdowns (respects selectable: false)
if ($key == $column . '_enum_select') { if ($key == $column . '__enum_select') {
$return = []; $return = [];
foreach ($sorted_config as $k => $v) { foreach ($sorted_config as $k => $v) {
@@ -140,8 +140,8 @@ abstract class Rsx_Model_Abstract extends Model
return $return; return $return;
} }
// field_enum_labels() - Simple id => label map (all items, ignores selectable) // field__enum_labels() - Simple id => label map (all items, ignores selectable)
if ($key == $column . '_enum_labels') { if ($key == $column . '__enum_labels') {
$return = []; $return = [];
foreach ($sorted_config as $k => $v) { foreach ($sorted_config as $k => $v) {
if (isset($v['label'])) { if (isset($v['label'])) {
@@ -151,8 +151,8 @@ abstract class Rsx_Model_Abstract extends Model
return $return; return $return;
} }
// field_enum_ids() - Array of all valid enum IDs // field__enum_ids() - Array of all valid enum IDs
if ($key == $column . '_enum_ids') { if ($key == $column . '__enum_ids') {
return array_keys($sorted_config); return array_keys($sorted_config);
} }
} }
@@ -164,39 +164,30 @@ abstract class Rsx_Model_Abstract extends Model
/** /**
* Magic getter for enum properties * Magic getter for enum properties
* *
* Provides access to: * Uses BEM-style double underscore to separate field from property:
* - field_label - Label for current enum value * - field__label - Label for current enum value
* - field_constant - Constant name for current value * - field__constant - Constant name for current value
* - field_enum_val - All properties for current value * - field__badge - Any custom property for current value
* - field_enum, field_enum_select, field_enum_ids - Via _get_static_magic * - field__enum(), field__enum_select(), etc. - Via _get_static_magic
* *
* @param string $key * @param string $key
* @return mixed * @return mixed
*/ */
public function __get($key) 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); $static_call = self::_get_static_magic($key);
if ($static_call !== null) { if ($static_call !== null) {
return $static_call; 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)) { if (!empty(static::$enums)) {
foreach (static::$enums as $column => $enum_config) { foreach (static::$enums as $column => $enum_config) {
// $object->field_enum_val returns all properties for current value // Look for specific enum property (e.g., field__label, field__constant)
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)
foreach ($enum_config as $enum_val => $enum_properties) { foreach ($enum_config as $enum_val => $enum_properties) {
foreach ($enum_properties as $prop_name => $prop_value) { 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; return $prop_value;
} }
} }
@@ -219,18 +210,13 @@ abstract class Rsx_Model_Abstract extends Model
*/ */
public function __isset($key) public function __isset($key)
{ {
// Check for enum magic properties // Check for enum magic properties (BEM-style: field__property)
if (!empty(static::$enums)) { if (!empty(static::$enums)) {
foreach (static::$enums as $column => $enum_config) { foreach (static::$enums as $column => $enum_config) {
// field_enum_val // field__label, field__constant, field__* (any custom enum property)
if ($key == $column . '_enum_val') {
return true;
}
// field_label, field_constant, field_* (any custom enum property)
foreach ($enum_config as $enum_val => $enum_properties) { foreach ($enum_config as $enum_val => $enum_properties) {
foreach ($enum_properties as $prop_name => $prop_value) { 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 // Property exists if current column value matches this enum value
if ($this->$column == $enum_val) { if ($this->$column == $enum_val) {
return true; return true;
@@ -247,11 +233,11 @@ abstract class Rsx_Model_Abstract extends Model
/** /**
* Magic static method handler for enum methods * Magic static method handler for enum methods
* *
* Provides static access to (mirrored in JavaScript stubs): * Provides static access to (mirrored in JavaScript stubs, BEM-style):
* - Model::field_enum_val() - All enum definitions with full metadata * - Model::field__enum() - All enum definitions with full metadata
* - Model::field_enum_select() - Selectable items for dropdowns * - Model::field__enum_select() - Selectable items for dropdowns
* - Model::field_enum_labels() - Simple id => label map * - Model::field__enum_labels() - Simple id => label map
* - Model::field_enum_ids() - Array of all valid enum IDs * - Model::field__enum_ids() - Array of all valid enum IDs
* *
* @param string $key * @param string $key
* @param array $args * @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) { foreach (static::$enums as $column => $definitions) {
if (isset($this->$column) && isset($definitions[$this->$column])) { if (isset($this->$column) && isset($definitions[$this->$column])) {
foreach ($definitions[$this->$column] as $prop => $value) { foreach ($definitions[$this->$column] as $prop => $value) {
// Add all enum properties to the export // 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 * _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 * Table: _file_attachments
* *
* @property int $id * @property int $id
@@ -59,19 +59,18 @@ use App\RSpade\Core\Files\File_Storage_Model;
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* *
* @property-read string $file_type_id_label * @property-read string $file_type_id__label
* @property-read string $file_type_id_constant * @property-read string $file_type_id__constant
* @property-read array $file_type_id_enum_val
* *
* @method static array file_type_id_enum_val() Get all enum definitions with full metadata * @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_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_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_ids() Get array of all valid enum IDs
* *
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class File_Attachment_Model extends Rsx_Site_Model_Abstract class File_Attachment_Model extends Rsx_Site_Model_Abstract
{ {
/** /**
* _AUTO_GENERATED_ Enum constants * _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_DOCUMENT = 6;
const FILE_TYPE_OTHER = 7; const FILE_TYPE_OTHER = 7;
/** __AUTO_GENERATED: */ /** __AUTO_GENERATED: */
/** __/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 * _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 * Table: _file_storage
* *
* @property int $id * @property int $id
@@ -30,7 +30,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class File_Storage_Model extends Rsx_Model_Abstract class File_Storage_Model extends Rsx_Model_Abstract
{ {
// Required static properties from parent abstract class // Required static properties from parent abstract class
public static $enums = []; public static $enums = [];
public static $rel = []; 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 * _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 * Table: countries
* *
* @property int $id * @property int $id
@@ -32,7 +32,7 @@ use App\RSpade\Core\Models\Region_Model;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Country_Model extends Rsx_Model_Abstract class Country_Model extends Rsx_Model_Abstract
{ {
public static $enums = []; public static $enums = [];
protected $table = 'countries'; 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 * _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 * Table: ip_addresses
* *
* @property int $id * @property int $id
@@ -30,7 +30,7 @@ use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Ip_Address_Model extends Rsx_System_Model_Abstract class Ip_Address_Model extends Rsx_System_Model_Abstract
{ {
/** /**
* Enum field definitions * Enum field definitions
* @var array * @var array

View File

@@ -24,7 +24,7 @@ use App\RSpade\Core\Session\Session;
*/ */
/** /**
* _AUTO_GENERATED_ Database type hints - do not edit manually * _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 * Table: login_users
* *
* @property int $id * @property int $id
@@ -40,21 +40,19 @@ use App\RSpade\Core\Session\Session;
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* *
* @property-read string $status_id_label * @property-read string $status_id__label
* @property-read string $status_id_constant * @property-read string $status_id__constant
* @property-read array $status_id_enum_val * @property-read string $is_verified__label
* @property-read string $is_verified_label * @property-read string $is_verified__constant
* @property-read string $is_verified_constant
* @property-read array $is_verified_enum_val
* *
* @method static array status_id_enum_val() Get all enum definitions with full metadata * @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_select() Get selectable items for dropdowns
* @method static array status_id_enum_labels() Get simple id => label map * @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 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() 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_select() Get selectable items for dropdowns
* @method static array is_verified_enum_labels() Get simple id => label map * @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 is_verified__enum_ids() Get array of all valid enum IDs
* *
* @mixin \Eloquent * @mixin \Eloquent
*/ */
@@ -62,7 +60,7 @@ class Login_User_Model extends Rsx_Model_Abstract implements
\Illuminate\Contracts\Auth\Authenticatable, \Illuminate\Contracts\Auth\Authenticatable,
\Illuminate\Contracts\Auth\Access\Authorizable, \Illuminate\Contracts\Auth\Access\Authorizable,
\Illuminate\Contracts\Auth\CanResetPassword \Illuminate\Contracts\Auth\CanResetPassword
{ {
/** /**
* _AUTO_GENERATED_ Enum constants * _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 * _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 * Table: regions
* *
* @property int $id * @property int $id
@@ -31,7 +31,7 @@ use App\RSpade\Core\Models\Country_Model;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Region_Model extends Rsx_Model_Abstract class Region_Model extends Rsx_Model_Abstract
{ {
public static $enums = []; public static $enums = [];
protected $table = 'regions'; 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 * _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 * Table: sites
* *
* @property int $id * @property int $id
@@ -31,7 +31,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Site_Model extends Rsx_Model_Abstract class Site_Model extends Rsx_Model_Abstract
{ {
use SoftDeletes; 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 * _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 * Table: user_invites
* *
* @property int $id * @property int $id
@@ -28,7 +28,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class User_Invite_Model extends Rsx_Site_Model_Abstract class User_Invite_Model extends Rsx_Site_Model_Abstract
{ {
/** /**
* Enum field definitions * Enum field definitions
* @var array * @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 * _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 * Table: users
* *
* @property int $id * @property int $id
@@ -48,19 +48,18 @@ use App\RSpade\Core\Models\User_Profile_Model;
* @property string $invite_accepted_at * @property string $invite_accepted_at
* @property string $invite_expires_at * @property string $invite_expires_at
* *
* @property-read string $role_id_label * @property-read string $role_id__label
* @property-read string $role_id_constant * @property-read string $role_id__constant
* @property-read array $role_id_enum_val
* *
* @method static array role_id_enum_val() Get all enum definitions with full metadata * @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_select() Get selectable items for dropdowns
* @method static array role_id_enum_labels() Get simple id => label map * @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_ids() Get array of all valid enum IDs
* *
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class User_Model extends Rsx_Site_Model_Abstract class User_Model extends Rsx_Site_Model_Abstract
{ {
/** /**
* _AUTO_GENERATED_ Enum constants * _AUTO_GENERATED_ Enum constants
*/ */
@@ -73,6 +72,7 @@ class User_Model extends Rsx_Site_Model_Abstract
const ROLE_VIEWER = 700; const ROLE_VIEWER = 700;
const ROLE_DISABLED = 800; const ROLE_DISABLED = 800;
/** __AUTO_GENERATED: */ /** __AUTO_GENERATED: */
/** __/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 * _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 * Table: user_permissions
* *
* @property int $id * @property int $id
@@ -22,7 +22,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class User_Permission_Model extends Rsx_Model_Abstract class User_Permission_Model extends Rsx_Model_Abstract
{ {
protected $table = 'user_permissions'; protected $table = 'user_permissions';
protected $fillable = []; // No mass assignment - always explicit 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 * _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 * Table: user_profiles
* *
* @property int $id * @property int $id
@@ -51,7 +51,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class User_Profile_Model extends Rsx_Model_Abstract class User_Profile_Model extends Rsx_Model_Abstract
{ {
/** /**
* The table associated with the model * 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 * _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 * Table: user_verifications
* *
* @property int $id * @property int $id
@@ -27,19 +27,18 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* *
* @property-read string $verification_type_id_label * @property-read string $verification_type_id__label
* @property-read string $verification_type_id_constant * @property-read string $verification_type_id__constant
* @property-read array $verification_type_id_enum_val
* *
* @method static array verification_type_id_enum_val() Get all enum definitions with full metadata * @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_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_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_ids() Get array of all valid enum IDs
* *
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class User_Verification_Model extends Rsx_Model_Abstract class User_Verification_Model extends Rsx_Model_Abstract
{ {
/** /**
* _AUTO_GENERATED_ Enum constants * _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_EMAIL_RECOVERY = 3;
const VERIFICATION_TYPE_SMS_RECOVERY = 4; const VERIFICATION_TYPE_SMS_RECOVERY = 4;
/** __AUTO_GENERATED: */ /** __AUTO_GENERATED: */
/** __/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 * _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 * Table: _search_indexes
* *
* @property int $id * @property int $id
@@ -37,7 +37,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Search_Index_Model extends Rsx_Site_Model_Abstract class Search_Index_Model extends Rsx_Site_Model_Abstract
{ {
// Required static properties from parent abstract class // Required static properties from parent abstract class
public static $enums = []; public static $enums = [];
public static $rel = []; 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 * _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 * Table: _sessions
* *
* @property int $id * @property int $id
@@ -63,7 +63,7 @@ use App\RSpade\Core\Session\User_Agent;
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Session extends Rsx_System_Model_Abstract class Session extends Rsx_System_Model_Abstract
{ {
// Enum definitions (required by abstract parent) // Enum definitions (required by abstract parent)
public static $enums = []; 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 * _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 * Table: _flash_alerts
* *
* @property int $id * @property int $id
@@ -18,19 +18,18 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @property int $updated_by * @property int $updated_by
* @property string $updated_at * @property string $updated_at
* *
* @property-read string $type_id_label * @property-read string $type_id__label
* @property-read string $type_id_constant * @property-read string $type_id__constant
* @property-read array $type_id_enum_val
* *
* @method static array type_id_enum_val() Get all enum definitions with full metadata * @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_select() Get selectable items for dropdowns
* @method static array type_id_enum_labels() Get simple id => label map * @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_ids() Get array of all valid enum IDs
* *
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class Flash_Alert_Model extends Rsx_Model_Abstract class Flash_Alert_Model extends Rsx_Model_Abstract
{ {
/** /**
* _AUTO_GENERATED_ Enum constants * _AUTO_GENERATED_ Enum constants
*/ */
@@ -39,6 +38,7 @@ class Flash_Alert_Model extends Rsx_Model_Abstract
const TYPE_INFO = 3; const TYPE_INFO = 3;
const TYPE_WARNING = 4; const TYPE_WARNING = 4;
/** __AUTO_GENERATED: */ /** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */ /** __/AUTO_GENERATED */

View File

@@ -13,8 +13,16 @@ SYNOPSIS
DESCRIPTION DESCRIPTION
The enum system provides a powerful way to define predefined values for database The enum system provides a powerful way to define predefined values for database
fields with associated metadata. It automatically generates constants, magic fields with associated metadata. It automatically generates constants, magic
properties, helper methods, and JavaScript equivalents for both PHP and JavaScript properties, helper methods, and JavaScript equivalents.
code.
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 DEFINING ENUMS
@@ -56,63 +64,67 @@ DEFINING ENUMS
PHP MAGIC PROPERTIES (Instance) 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 field__label Returns the label for the current value
$user->status_id = 1; $user->status_id = 1;
echo $user->status_label; // "Active" echo $user->status_id__label; // "Active"
field_constant Returns the constant name for the current value field__constant Returns the constant name for the current value
echo $user->status_constant; // "STATUS_ACTIVE" echo $user->status_id__constant; // "STATUS_ACTIVE"
field_enum_val Returns ALL properties for the current value field__[property] Returns any custom property for the current value
$props = $user->status_enum_val; echo $user->status_id__badge; // "bg-success"
// ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', ...] echo $user->status_id__visible_frontend; // true
field_[property] Returns any custom property for the current value
echo $user->status_badge; // "bg-success"
echo $user->status_visible_frontend; // true
STATIC METHODS (PHP and JavaScript) STATIC METHODS (PHP and JavaScript)
These four methods are available as static methods on model classes in both PHP These methods are available as static methods on model classes in both PHP
and JavaScript. The JavaScript stubs are auto-generated to mirror PHP behavior. 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: 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', ...], ...] // [1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', ...], ...]
// JavaScript equivalent: // 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'): 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) // [1 => 'Active', 2 => 'Inactive'] (excludes selectable: false items)
// JavaScript equivalent: // 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): 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'] // [1 => 'Active', 2 => 'Inactive', 3 => 'Archived']
// JavaScript equivalent: // 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: Returns array of all valid enum IDs:
$ids = User_Model::status_id_enum_ids(); $ids = User_Model::status_id__enum_ids();
// [1, 2, 3] // [1, 2, 3]
// JavaScript equivalent: // JavaScript equivalent:
const ids = User_Model.status_id_enum_ids(); const ids = User_Model.status_id__enum_ids();
PHP CONSTANTS PHP CONSTANTS
@@ -138,32 +150,61 @@ JAVASCRIPT ACCESS
Project_Model.STATUS_ACTIVE // 2 Project_Model.STATUS_ACTIVE // 2
Project_Model.STATUS_PLANNING // 1 Project_Model.STATUS_PLANNING // 1
Static Methods (same as PHP - see STATIC METHODS section above) Static Methods (BEM-style double underscore)
Project_Model.status_enum_val() // Full enum definitions with metadata Project_Model.status__enum() // Full enum definitions with metadata
Project_Model.status_enum_select() // Selectable items for dropdowns Project_Model.status__enum(id) // Single enum metadata by ID
Project_Model.status_enum_labels() // Simple id => label map Project_Model.status__enum_select() // Selectable items for dropdowns
Project_Model.status_enum_ids() // Array of valid IDs 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); const project = await Project_Model.fetch(1);
project.status // 2 (raw value) project.status // 2 (raw value)
project.status_label // "Active" project.status__label // "Active"
project.status_badge // "bg-success" project.status__badge // "bg-success"
AJAX/JSON EXPORT 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: $user->toArray() returns:
[ [
'id' => 1, 'id' => 1,
'status_id' => 1, 'status_id' => 1,
'status_id_label' => 'Active', // Added automatically 'status_id__label' => 'Active', // Added automatically
'status_id_constant' => 'STATUS_ACTIVE', // Added automatically 'status_id__constant' => 'STATUS_ACTIVE', // Added automatically
'status_id_badge' => 'bg-success', // Custom properties too 'status_id__badge' => 'bg-success', // Custom properties too
// ... all enum properties for current value // ... 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 ADVANCED FEATURES
Ordering Ordering
@@ -174,10 +215,11 @@ ADVANCED FEATURES
1 => ['label' => 'High', 'order' => 1], 1 => ['label' => 'High', 'order' => 1],
2 => ['label' => 'Medium', 'order' => 2], 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 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 => [ 3 => [
'constant' => 'STATUS_ARCHIVED', 'constant' => 'STATUS_ARCHIVED',
@@ -197,18 +239,18 @@ PRACTICAL APPLICATIONS
Populating Select Boxes Populating Select Boxes
<!-- Blade template --> <!-- Blade template -->
<select name="status_id"> <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> <option value="{{ $id }}">{{ $label }}</option>
@endforeach @endforeach
</select> </select>
Dynamic CSS Classes Dynamic CSS Classes
<span class="badge {{ $auction->auction_status_badge }}"> <span class="badge {{ $auction->auction_status__badge }}">
{{ $auction->auction_status_label }} {{ $auction->auction_status__label }}
</span> </span>
Business Logic Flags Business Logic Flags
if ($auction->auction_status_can_bid) { if ($auction->auction_status__can_bid) {
// Show bidding interface // Show bidding interface
} }
@@ -223,7 +265,7 @@ PRACTICAL APPLICATIONS
] ]
// Check permissions // Check permissions
if (in_array('users.create', $user->role_permissions)) { if (in_array('users.create', $user->role__permissions)) {
// User can create users // User can create users
} }
@@ -255,6 +297,7 @@ BEST PRACTICES
5. Keep enum values immutable - add new values, don't change existing 5. Keep enum values immutable - add new values, don't change existing
6. Document custom properties in your model 6. Document custom properties in your model
7. Run rsx:migrate:document_models after adding enums 7. Run rsx:migrate:document_models after adding enums
8. NEVER alias enum properties - use full BEM-style names
EXAMPLE IMPLEMENTATION EXAMPLE IMPLEMENTATION
@@ -312,15 +355,15 @@ EXAMPLE IMPLEMENTATION
// Usage in controller // Usage in controller
if ($project->status === Project_Model::STATUS_IN_PROGRESS) { if ($project->status === Project_Model::STATUS_IN_PROGRESS) {
if ($project->priority_days < 3) { if ($project->priority__days < 3) {
// Escalate critical project // Escalate critical project
} }
} }
// Usage in Blade view // Usage in Blade view
<div class="{{ $project->status_badge }}"> <div class="{{ $project->status__badge }}">
{{ $project->status_label }} {{ $project->status__label }}
@if($project->status_can_edit) @if($project->status__can_edit)
<button>Edit</button> <button>Edit</button>
@endif @endif
</div> </div>
@@ -330,4 +373,4 @@ SEE ALSO
php artisan rsx:man model_fetch - Model fetching from JavaScript php artisan rsx:man model_fetch - Model fetching from JavaScript
php artisan rsx:man models - RSX model system overview 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 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): Client-side (HTML attributes):
<div data-component="User_Card" <div data-component="User_Card"
data-component-data='{"name": "John"}'> data-component-data='{"name": "John"}'>

View File

@@ -29,7 +29,7 @@ STATUS (as of 2025-11-23)
Implemented: Implemented:
- fetch() and fetch_or_null() methods - fetch() and fetch_or_null() methods
- Lazy relationship loading (belongsTo, hasMany, morphTo, etc.) - 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 - Static enum constants and accessor methods
- Automatic model hydration from Ajax responses - Automatic model hydration from Ajax responses
- JavaScript class hierarchy with Base_* stubs - JavaScript class hierarchy with Base_* stubs
@@ -140,31 +140,34 @@ IMPLEMENTING FETCHABLE MODELS
} }
} }
Augmented Array Return (recommended for CRUD pages): Augmented Array Return (for model method outputs only):
class Client_Model extends Rsx_Model 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] #[Ajax_Endpoint_Model_Fetch]
public static function fetch($id) public static function fetch($id)
{ {
$client = static::withTrashed()->find($id); $contact = static::withTrashed()->find($id);
if (!$client) { if (!$contact) {
return false; return false;
} }
// Start with model's toArray() to get __MODEL and base data // Start with model's toArray() to get __MODEL and base data
$data = $client->toArray(); $data = $contact->toArray();
// Augment with computed/formatted fields // Augment ONLY with outputs from defined model methods
$data['status_label'] = ucfirst($client->status); $data['full_name'] = $contact->full_name();
$data['status_badge'] = match($client->status) { $data['mailto_link'] = $contact->mailto_link();
'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();
return $data; return $data;
} }
@@ -172,7 +175,6 @@ IMPLEMENTING FETCHABLE MODELS
IMPORTANT: Always start with $model->toArray() when augmenting data. IMPORTANT: Always start with $model->toArray() when augmenting data.
This preserves the __MODEL property needed for JavaScript hydration. 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): DO NOT manually construct the return array like this (outdated pattern):
return [ return [
@@ -182,6 +184,87 @@ IMPLEMENTING FETCHABLE MODELS
]; ];
This loses __MODEL and returns a plain object instead of a model instance. 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 JAVASCRIPT USAGE
Single Record Fetching: Single Record Fetching:
// fetch() throws if record not found - no need to check for null/false // 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() - View/edit pages where record MUST exist (throws on not found)
- fetch_or_null() - Optional lookups where missing is valid (returns null) - 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); const project = await Project_Model.fetch(1);
// All enum helper properties from PHP are available // All enum helper properties from PHP are available
console.log(project.status_id); // 2 (raw value) console.log(project.status_id); // 2 (raw value)
console.log(project.status_id_label); // "Active" console.log(project.status_id__label); // "Active"
console.log(project.status_id_badge); // "bg-success" console.log(project.status_id__badge); // "bg-success"
Static Enum Constants: Static Enum Constants:
// Constants available on the class // Constants available on the class
@@ -220,13 +303,17 @@ JAVASCRIPT USAGE
} }
// Get all enum values for dropdowns // 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", ...} // {1: "Planning", 2: "Active", 3: "On Hold", ...}
// Get full enum config // Get full enum config with all metadata
const statusConfig = Project_Model.status_id_enum_val(); const statusConfig = Project_Model.status_id__enum();
// {1: {label: "Planning", badge: "bg-info"}, ...} // {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: Error Handling:
// In SPA actions, errors bubble up to Universal_Error_Page_Component // In SPA actions, errors bubble up to Universal_Error_Page_Component
// No try/catch needed - just call fetch() and use the result // No try/catch needed - just call fetch() and use the result
@@ -266,9 +353,12 @@ JAVASCRIPT CLASS ARCHITECTURE
static STATUS_PLANNING = 1; static STATUS_PLANNING = 1;
static STATUS_ACTIVE = 2; static STATUS_ACTIVE = 2;
// Enum accessor methods // Enum accessor methods (BEM-style double underscore)
static status_enum_val() { ... } // Full enum config static status__enum() { ... } // Full enum config (all values)
static status_enum_select() { ... } // For dropdown population 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 // Relationship discovery
static get_relationships() { ... } // Returns array of names static get_relationships() { ... } // Returns array of names
@@ -618,7 +708,7 @@ LAZY RELATIONSHIPS
ENUM PROPERTIES ENUM PROPERTIES
Enum values are exposed as properties on fetched model instances, mirroring Enum values are exposed as properties on fetched model instances, mirroring
the PHP magic property behavior. Each custom field defined in the enum 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: PHP Enum Definition:
public static $enums = [ 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); const project = await Project_Model.fetch(123);
// Raw enum value // Raw enum value
project.status_id // 2 project.status_id // 2
// Auto-generated properties from enum definition // Auto-generated properties from enum definition
project.status_id_label // "Active" project.status_id__label // "Active"
project.status_id_badge // "bg-success" project.status_id__badge // "bg-success"
// All custom fields become properties // All custom fields become properties
// If enum had 'button_class' => 'btn-success': // If enum had 'button_class' => 'btn-success':
project.status_id_button_class // "btn-success" project.status_id__button_class // "btn-success"
Static Enum Constants: Static Enum Constants:
// Constants available on the class (from 'constant' field) // Constants available on the class (from 'constant' field)
@@ -649,14 +739,26 @@ ENUM PROPERTIES
console.log('Project is active'); 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) // 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"} // {1: "Planning", 2: "Active", 3: "On Hold"}
// Get full enum config (id => all fields) // Get simple id => label map (all items)
const statusConfig = Project_Model.status_id_enum_val(); const labels = Project_Model.status_id__enum_labels();
// {1: {label: "Planning", badge: "bg-info"}, 2: {...}, ...} // {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 MODEL CONSTANTS
All public constants defined on a PHP model are automatically exported to 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

View File

@@ -702,6 +702,8 @@ this.$sid('result_container').component('My_Component', {
}); });
``` ```
**Class preservation**: Only PascalCase component names (capital start, no `__`) are replaced. Utility classes (`text-muted`), BEM child classes (`Parent__child`), and all attributes are preserved.
### Incremental Scaffolding ### Incremental Scaffolding
**Undefined components work immediately** - they render as div with the component name as a class. **Undefined components work immediately** - they render as div with the component name as a class.
@@ -931,7 +933,7 @@ User_Model::create(['email' => $email]);
**CRITICAL: Read `php artisan rsx:man enum` for complete documentation before implementing.** **CRITICAL: Read `php artisan rsx:man enum` for complete documentation before implementing.**
Integer-backed enums with model-level mapping to constants, labels, and custom properties. Integer-backed enums with model-level mapping to constants, labels, and custom properties. Uses BEM-style double underscore naming for magic properties.
```php ```php
class Project_Model extends Rsx_Model_Abstract { class Project_Model extends Rsx_Model_Abstract {
@@ -943,15 +945,30 @@ class Project_Model extends Rsx_Model_Abstract {
]; ];
} }
// Usage // Usage - BEM-style: field__property (double underscore)
$project->status_id = Project_Model::STATUS_ACTIVE; $project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_label; // "Active" echo $project->status_id__label; // "Active"
echo $project->status_badge; // "bg-success" (custom property) echo $project->status_id__badge; // "bg-success" (custom property)
``` ```
**Special properties**: **Special properties**:
- `order` - Sort position in dropdowns (default: 0, lower first) - `order` - Sort position in dropdowns (default: 0, lower first)
- `selectable` - Include in dropdown options (default: true). Non-selectable items excluded from `field_enum_select()` but still shown if current value. - `selectable` - Include in dropdown options (default: true). Non-selectable items excluded from `field__enum_select()` but still shown if current value.
**JavaScript static methods** (BEM-style double underscore):
```js
// Get all enum data
Project_Model.status_id__enum()
// Get specific enum's metadata by ID
Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE).badge // "bg-success"
Project_Model.status_id__enum(2).selectable // false
// Other methods
Project_Model.status_id__enum_select() // For dropdowns
Project_Model.status_id__enum_labels() // id => label map
Project_Model.status_id__enum_ids() // Array of valid IDs
```
**Migration:** Use BIGINT for enum columns, TINYINT(1) for booleans. Run `rsx:migrate:document_models` after adding enums. **Migration:** Use BIGINT for enum columns, TINYINT(1) for booleans. Run `rsx:migrate:document_models` after adding enums.
@@ -969,7 +986,7 @@ public static function fetch($id)
```javascript ```javascript
const project = await Project_Model.fetch(1); // Throws if not found const project = await Project_Model.fetch(1); // Throws if not found
const maybe = await Project_Model.fetch_or_null(999); // Returns null if not found const maybe = await Project_Model.fetch_or_null(999); // Returns null if not found
console.log(project.status_label); // Enum properties populated console.log(project.status_id__label); // Enum properties (BEM-style)
console.log(Project_Model.STATUS_ACTIVE); // Static enum constants console.log(Project_Model.STATUS_ACTIVE); // Static enum constants
// Lazy relationships (requires #[Ajax_Endpoint_Model_Fetch] on relationship method) // Lazy relationships (requires #[Ajax_Endpoint_Model_Fetch] on relationship method)
@@ -979,6 +996,8 @@ const tasks = await project.tasks(); // hasMany → Model[]
**Security**: Both `fetch()` and relationships require `#[Ajax_Endpoint_Model_Fetch]` attribute. Related models must also implement `fetch()` with this attribute. **Security**: Both `fetch()` and relationships require `#[Ajax_Endpoint_Model_Fetch]` attribute. Related models must also implement `fetch()` with this attribute.
**fetch() is for SECURITY, not aliasing**: The `fetch()` method exists to remove private data users shouldn't see. NEVER alias enum properties (e.g., `type_label` instead of `type_id__label`) or format dates server-side. Use the full BEM-style names and format dates on client with `Rsx_Date`/`Rsx_Time`.
Details: `php artisan rsx:man model_fetch` Details: `php artisan rsx:man model_fetch`
### Migrations ### Migrations

57
node_modules/.package-lock.json generated vendored
View File

@@ -2211,9 +2211,9 @@
} }
}, },
"node_modules/@jqhtml/core": { "node_modules/@jqhtml/core": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.31.tgz",
"integrity": "sha512-6VuZCo4aCr9Qk5LhOnG0Fv6GE2Z/mUfXnSwE5iSk3v+i7bE4IgEMrQVGsndaJtHHLRRnB2n+Aed2W3H5eeV9Fg==", "integrity": "sha512-VbTAbFF8QVOljNjf+1OZ4cFTE3NaeNzyMidqrooYSsHZvb6Ja5NIMDft+M4FxeidrMoRIwa7QN09XgiJWBVNRg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
@@ -2237,9 +2237,9 @@
} }
}, },
"node_modules/@jqhtml/parser": { "node_modules/@jqhtml/parser": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.31.tgz",
"integrity": "sha512-UHiGZ0bueaOGtSIQahitzc+1mJ/pibYZgYUOf6gc3a788Gq37lRA5IuyOKtoe7YYPQjJCyH43joF+Qv4bNBXDA==", "integrity": "sha512-ILV1onWn+rMdwaPd6DYIPM0dj2aUExTJ4ww4c0/+h3Zk50gnxMJQc6fOirDrTB1nWwKtY19yFDMPFYnurOJ2wA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
@@ -2277,9 +2277,9 @@
} }
}, },
"node_modules/@jqhtml/ssr": { "node_modules/@jqhtml/ssr": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.31.tgz",
"integrity": "sha512-gNpwsWkeqT8TEGzvi6vccjhtFvT28b3NOHiqNSpgGUHgkMupHU4oqEi/QDNhEeU87kNVvqEhTsEIqAXX07Wt3Q==", "integrity": "sha512-EpZ597l/3MEgpvAJTqcZ81cVTJwYxJzs8BLXAdbnQY+ySeOYQL8ot31IV3hdS8b6FnYezzaHN+jSBtqZZsAYnQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jquery": "^3.7.1", "jquery": "^3.7.1",
@@ -2373,9 +2373,9 @@
} }
}, },
"node_modules/@jqhtml/vscode-extension": { "node_modules/@jqhtml/vscode-extension": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.31.tgz",
"integrity": "sha512-+l2kI1Uj/kSCeM1bzCHbEtsRE+X6VpxQpw7wfrExqMKYvrzRmU6yiQADHuc85CFg8F2HF+7d7XA9zvgj8cOXcg==", "integrity": "sha512-aQoKAxLz1ziuJ6I2EfFXxUBvPEsDxirR2q/6VwEzYqfVTHdKiw6M2Sk25Nhm/lrg5dNLuiKa+snRodV8yEOWpQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"vscode": "^1.74.0" "vscode": "^1.74.0"
@@ -2574,26 +2574,6 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@popperjs/core": { "node_modules/@popperjs/core": {
"version": "2.11.8", "version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -2745,19 +2725,6 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",

View File

@@ -4698,13 +4698,16 @@ function init_jquery_plugin(jQuery) {
catch (error) { catch (error) {
console.warn('[JQHTML] Error stopping existing component during replacement:', error); console.warn('[JQHTML] Error stopping existing component during replacement:', error);
} }
// Remove component classes (any class starting with capital letter) // Remove component classes (any class starting with capital letter, except BEM classes)
const classes = element.attr('class'); const classes = element.attr('class');
if (classes) { if (classes) {
const classList = classes.split(/\s+/); const classList = classes.split(/\s+/);
const nonComponentClasses = classList.filter((cls) => { const nonComponentClasses = classList.filter((cls) => {
// Keep class if it doesn't start with capital letter // Keep class if:
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase(); // 1. It's empty
// 2. It doesn't start with a capital letter
// 3. It's a BEM-style class (contains __) - these persist across reinitialization
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase() || cls.includes('__');
}); });
element.attr('class', nonComponentClasses.join(' ')); element.attr('class', nonComponentClasses.join(' '));
} }
@@ -4981,7 +4984,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // Version - will be replaced during build with actual version from package.json
const version = '2.3.30'; const version = '2.3.31';
// Default export with all functionality // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // Core

File diff suppressed because one or more lines are too long

View File

@@ -4694,13 +4694,16 @@ function init_jquery_plugin(jQuery) {
catch (error) { catch (error) {
console.warn('[JQHTML] Error stopping existing component during replacement:', error); console.warn('[JQHTML] Error stopping existing component during replacement:', error);
} }
// Remove component classes (any class starting with capital letter) // Remove component classes (any class starting with capital letter, except BEM classes)
const classes = element.attr('class'); const classes = element.attr('class');
if (classes) { if (classes) {
const classList = classes.split(/\s+/); const classList = classes.split(/\s+/);
const nonComponentClasses = classList.filter((cls) => { const nonComponentClasses = classList.filter((cls) => {
// Keep class if it doesn't start with capital letter // Keep class if:
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase(); // 1. It's empty
// 2. It doesn't start with a capital letter
// 3. It's a BEM-style class (contains __) - these persist across reinitialization
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase() || cls.includes('__');
}); });
element.attr('class', nonComponentClasses.join(' ')); element.attr('class', nonComponentClasses.join(' '));
} }
@@ -4977,7 +4980,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // Version - will be replaced during build with actual version from package.json
const version = '2.3.30'; const version = '2.3.31';
// Default export with all functionality // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // Core

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/** /**
* JQHTML Core v2.3.30 * JQHTML Core v2.3.31
* (c) 2025 JQHTML Team * (c) 2025 JQHTML Team
* Released under the MIT License * Released under the MIT License
*/ */
@@ -4699,13 +4699,16 @@ function init_jquery_plugin(jQuery) {
catch (error) { catch (error) {
console.warn('[JQHTML] Error stopping existing component during replacement:', error); console.warn('[JQHTML] Error stopping existing component during replacement:', error);
} }
// Remove component classes (any class starting with capital letter) // Remove component classes (any class starting with capital letter, except BEM classes)
const classes = element.attr('class'); const classes = element.attr('class');
if (classes) { if (classes) {
const classList = classes.split(/\s+/); const classList = classes.split(/\s+/);
const nonComponentClasses = classList.filter((cls) => { const nonComponentClasses = classList.filter((cls) => {
// Keep class if it doesn't start with capital letter // Keep class if:
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase(); // 1. It's empty
// 2. It doesn't start with a capital letter
// 3. It's a BEM-style class (contains __) - these persist across reinitialization
return !cls || cls[0] !== cls[0].toUpperCase() || cls[0] === cls[0].toLowerCase() || cls.includes('__');
}); });
element.attr('class', nonComponentClasses.join(' ')); element.attr('class', nonComponentClasses.join(' '));
} }
@@ -4982,7 +4985,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // Version - will be replaced during build with actual version from package.json
const version = '2.3.30'; const version = '2.3.31';
// Default export with all functionality // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // Core

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"jquery-plugin.d.ts","sourceRoot":"","sources":["../src/jquery-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAQpE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd;;WAEG;QACH,SAAS,IAAI,gBAAgB,GAAG,IAAI,CAAC;QACrC,SAAS,CAAC,cAAc,EAAE,oBAAoB,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,gBAAgB,CAAC;QAC9F,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,gBAAgB,CAAC;QAE/E;;;;;;;WAOG;QACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;KACvC;CACF;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CA0apD"} {"version":3,"file":"jquery-plugin.d.ts","sourceRoot":"","sources":["../src/jquery-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAQpE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd;;WAEG;QACH,SAAS,IAAI,gBAAgB,GAAG,IAAI,CAAC;QACrC,SAAS,CAAC,cAAc,EAAE,oBAAoB,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,gBAAgB,CAAC;QAC9F,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,gBAAgB,CAAC;QAE/E;;;;;;;WAOG;QACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;KACvC;CACF;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CA6apD"}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@jqhtml/core", "name": "@jqhtml/core",
"version": "2.3.30", "version": "2.3.31",
"description": "Core runtime library for JQHTML", "description": "Core runtime library for JQHTML",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1377,7 +1377,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) { for (const [name, component] of this.components) {
code += `// Component: ${name}\n`; code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`; code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.3.30',\n`; // Version will be replaced during build code += ` _jqhtml_version: '2.3.31',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`; code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`; code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`; code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@jqhtml/parser", "name": "@jqhtml/parser",
"version": "2.3.30", "version": "2.3.31",
"description": "JQHTML template parser - converts templates to JavaScript", "description": "JQHTML template parser - converts templates to JavaScript",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@jqhtml/ssr", "name": "@jqhtml/ssr",
"version": "2.3.30", "version": "2.3.31",
"description": "Server-Side Rendering for JQHTML components - renders components to HTML for SEO", "description": "Server-Side Rendering for JQHTML components - renders components to HTML for SEO",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@@ -1 +1 @@
2.3.30 2.3.31

View File

@@ -2,7 +2,7 @@
"name": "@jqhtml/vscode-extension", "name": "@jqhtml/vscode-extension",
"displayName": "JQHTML", "displayName": "JQHTML",
"description": "Syntax highlighting and language support for JQHTML template files", "description": "Syntax highlighting and language support for JQHTML template files",
"version": "2.3.30", "version": "2.3.31",
"publisher": "jqhtml", "publisher": "jqhtml",
"license": "MIT", "license": "MIT",
"publishConfig": { "publishConfig": {

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017-present Devon Govett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1 +0,0 @@
This is the linux-x64-musl build of @parcel/watcher. See https://github.com/parcel-bundler/watcher for details.

View File

@@ -1,33 +0,0 @@
{
"name": "@parcel/watcher-linux-x64-musl",
"version": "2.5.1",
"main": "watcher.node",
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/watcher.git"
},
"description": "A native C++ Node module for querying and subscribing to filesystem events. Used by Parcel 2.",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"files": [
"watcher.node"
],
"engines": {
"node": ">= 10.0.0"
},
"os": [
"linux"
],
"cpu": [
"x64"
],
"libc": [
"musl"
]
}

Binary file not shown.

View File

@@ -1,3 +0,0 @@
# `@rollup/rollup-linux-x64-musl`
This is the **x86_64-unknown-linux-musl** binary for `rollup`

View File

@@ -1,25 +0,0 @@
{
"name": "@rollup/rollup-linux-x64-musl",
"version": "4.54.0",
"os": [
"linux"
],
"cpu": [
"x64"
],
"files": [
"rollup.linux-x64-musl.node"
],
"description": "Native bindings for Rollup",
"author": "Lukas Taegert-Atkinson",
"homepage": "https://rollupjs.org/",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/rollup/rollup.git"
},
"libc": [
"musl"
],
"main": "./rollup.linux-x64-musl.node"
}

24
package-lock.json generated
View File

@@ -2660,9 +2660,9 @@
} }
}, },
"node_modules/@jqhtml/core": { "node_modules/@jqhtml/core": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.3.31.tgz",
"integrity": "sha512-6VuZCo4aCr9Qk5LhOnG0Fv6GE2Z/mUfXnSwE5iSk3v+i7bE4IgEMrQVGsndaJtHHLRRnB2n+Aed2W3H5eeV9Fg==", "integrity": "sha512-VbTAbFF8QVOljNjf+1OZ4cFTE3NaeNzyMidqrooYSsHZvb6Ja5NIMDft+M4FxeidrMoRIwa7QN09XgiJWBVNRg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.1",
@@ -2686,9 +2686,9 @@
} }
}, },
"node_modules/@jqhtml/parser": { "node_modules/@jqhtml/parser": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.3.31.tgz",
"integrity": "sha512-UHiGZ0bueaOGtSIQahitzc+1mJ/pibYZgYUOf6gc3a788Gq37lRA5IuyOKtoe7YYPQjJCyH43joF+Qv4bNBXDA==", "integrity": "sha512-ILV1onWn+rMdwaPd6DYIPM0dj2aUExTJ4ww4c0/+h3Zk50gnxMJQc6fOirDrTB1nWwKtY19yFDMPFYnurOJ2wA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
@@ -2726,9 +2726,9 @@
} }
}, },
"node_modules/@jqhtml/ssr": { "node_modules/@jqhtml/ssr": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/ssr/-/ssr-2.3.31.tgz",
"integrity": "sha512-gNpwsWkeqT8TEGzvi6vccjhtFvT28b3NOHiqNSpgGUHgkMupHU4oqEi/QDNhEeU87kNVvqEhTsEIqAXX07Wt3Q==", "integrity": "sha512-EpZ597l/3MEgpvAJTqcZ81cVTJwYxJzs8BLXAdbnQY+ySeOYQL8ot31IV3hdS8b6FnYezzaHN+jSBtqZZsAYnQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jquery": "^3.7.1", "jquery": "^3.7.1",
@@ -2822,9 +2822,9 @@
} }
}, },
"node_modules/@jqhtml/vscode-extension": { "node_modules/@jqhtml/vscode-extension": {
"version": "2.3.30", "version": "2.3.31",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.30.tgz", "resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.3.31.tgz",
"integrity": "sha512-+l2kI1Uj/kSCeM1bzCHbEtsRE+X6VpxQpw7wfrExqMKYvrzRmU6yiQADHuc85CFg8F2HF+7d7XA9zvgj8cOXcg==", "integrity": "sha512-aQoKAxLz1ziuJ6I2EfFXxUBvPEsDxirR2q/6VwEzYqfVTHdKiw6M2Sk25Nhm/lrg5dNLuiKa+snRodV8yEOWpQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"vscode": "^1.74.0" "vscode": "^1.74.0"