info('Regenerating model constants...'); // Warnings encountered during processing to show at the end of execution $warnings = []; // We will be getting a master list of models. $modelClassList = []; // Get all models from the manifest // Models typically extend 'Model' or 'Rsx_Model_Abstract' $model_entries = Manifest::php_get_extending('Rsx_Model_Abstract'); // Remove duplicates (in case a model is found multiple times) $unique_models = []; foreach ($model_entries as $model) { if (isset($model['fqcn'])) { $unique_models[$model['fqcn']] = $model; } } // Track processed models $processed_count = 0; $error_count = 0; foreach ($unique_models as $model_metadata) { // Get the file path and metadata from manifest $file_path = base_path($model_metadata['file']); $namespace = $model_metadata['namespace'] ?? ''; $className = $model_metadata['class'] ?? ''; $fullClassName = $model_metadata['fqcn'] ?? ''; if (!$namespace || !$className || !$fullClassName) { continue; } // Silently process - only output changes/warnings try { $content = File::get($file_path); // Convert to unix format $content = str_replace("\r\n", "\n", $content); if (in_array($fullClassName, $modelClassList)) { continue; } $modelClassList[] = $fullClassName; // Skip abstract classes if (\App\RSpade\Core\Manifest\Manifest::php_is_abstract($fullClassName)) { continue; } $table = with(new $fullClassName())->getTable(); $columnNames = Schema::getColumnListing($table); $reflector = new ReflectionClass($fullClassName); $enums = $reflector->getStaticPropertyValue('enums', []); // We don't need 'rel' for constants generation // Error check - validate enum integrity BEFORE processing // These checks run outside try-catch to fail immediately foreach ($enums as $column => $enumValues) { // Validate enum values are integers foreach ($enumValues as $value => $props) { if (!is_int($value) && !ctype_digit((string)$value)) { $this->error("Invalid enum value '{$value}' for column '{$column}' in model '{$fullClassName}'."); $this->newLine(); $this->error("ENUM VALUES MUST BE INTEGERS."); $this->newLine(); $this->line("The purpose of enum values is to store an INTEGER in the database that corresponds to a"); $this->line("string in the enum definition. The string label can then be changed in the enum 'label'"); $this->line("property without affecting the database value, so long as it continues to correspond to"); $this->line("the same numeric integer."); $this->newLine(); $this->line("Example of a properly defined enum:"); $this->newLine(); $this->line(" protected static \$enums = ["); $this->line(" 'status' => ["); $this->line(" 1 => ['label' => 'Active', 'constant' => 'STATUS_ACTIVE'],"); $this->line(" 2 => ['label' => 'Inactive', 'constant' => 'STATUS_INACTIVE'],"); $this->line(" 3 => ['label' => 'Pending', 'constant' => 'STATUS_PENDING'],"); $this->line(" ],"); $this->line(" ];"); $this->newLine(); $this->line("For more information, run: php artisan rsx:man enums"); return 1; // Exit with error code } } // Validate enum column is integer type in database $columnType = DB::getSchemaBuilder()->getColumnType($table, $column); $validIntegerTypes = ['integer', 'bigint', 'smallint', 'tinyint', 'mediumint']; // Special case: allow 'boolean' type (TINYINT(1)) ONLY if enum values are 0 and 1 $isBooleanEnum = false; if ($columnType === 'boolean') { $enumKeys = array_keys($enumValues); sort($enumKeys); if ($enumKeys === [0, 1]) { $isBooleanEnum = true; } } if (!in_array($columnType, $validIntegerTypes) && !$isBooleanEnum) { $this->error("Invalid column type '{$columnType}' for enum column '{$column}' in table '{$table}' (model '{$fullClassName}')."); $this->newLine(); $this->error("ENUM COLUMNS MUST BE INTEGER TYPES."); $this->newLine(); // Special message for boolean/TINYINT if ($columnType === 'boolean') { $this->line("TINYINT columns are reported as 'boolean' by Laravel because TINYINT is ONLY for true/false values."); $this->line("For enum values with multiple options (1, 2, 3, etc.), you MUST use INT or BIGINT."); $this->newLine(); } $this->line("Enum values are stored as integers in the database. The column must be defined as an"); $this->line("integer type (INT, BIGINT, SMALLINT, or MEDIUMINT), not VARCHAR, TINYINT, or other types."); $this->newLine(); $this->line("Current column type: {$columnType}"); $this->line("Required column types: " . implode(', ', $validIntegerTypes)); $this->newLine(); $this->line("To fix this issue:"); $this->line("1. Create a migration to change the column type to INT or BIGINT"); $this->line("2. Example migration:"); $this->newLine(); $this->line(" public function up()"); $this->line(" {"); $this->line(" Schema::table('{$table}', function (Blueprint \$table) {"); $this->line(" \$table->integer('{$column}')->change();"); $this->line(" });"); $this->line(" }"); $this->newLine(); $this->line("For more information, run: php artisan rsx:man enums"); return 1; // Exit with error code } // Check for duplicate constants $constants = []; foreach ($enumValues as $value => $props) { if (isset($props['constant'])) { if (in_array($props['constant'], $constants)) { throw new Exception("Duplicate constant '{$props['constant']}' found in '{$column}' for value '{$value}'"); } $constants[] = $props['constant']; } } } $indent = str_repeat(' ', 4); $docblock = "/**\n * _AUTO_GENERATED_\n"; // Process columns foreach ($columnNames as $columnName) { $columnType = DB::getSchemaBuilder()->getColumnType($table, $columnName); if ($columnType == 'bigint') { $columnType = 'integer'; } if ($columnType == 'datetime') { $columnType = '\Carbon\Carbon'; } if ($columnType == 'text') { $columnType = 'string'; } $docblock .= " * @property {$columnType} \${$columnName}\n"; } // Skip if no enums defined if (empty($enums)) { continue; } $constants = ''; if (count($enums) > 0) { $constants = $indent . "/** __AUTO_GENERATED: */\n"; } // Process enum magic methods and properties // Note: Validation already performed in early error check section above foreach ($enums as $column => $enumValues) { $docblock .= " * @method static mixed {$column}_enum()\n"; $docblock .= " * @method static mixed {$column}_enum_select()\n"; $docblock .= " * @method static mixed {$column}_enum_ids()\n"; $seenProps = []; foreach ($enumValues as $value => $props) { foreach ($props as $p => $v) { if (!in_array($p, $seenProps)) { $docblock .= " * @property-read mixed \${$column}_{$p}\n"; $seenProps[] = $p; } } if (!empty($props['constant'])) { $constants .= $indent . 'const ' . $props['constant'] . ' = ' . $value . ";\n"; } } } $docblock .= " * @mixin \\Eloquent\n"; $docblock .= ' */'; if (count($enums) > 0) { $constants .= $indent . "/** __/AUTO_GENERATED */\n"; } $org_content = $content; // Remove old comment blocks $content = self::remove_auto_generated_code_blocks($content, '/*', '*/', '_AUTO_GENERATED_'); $content = self::remove_auto_generated_code_blocks($content, '/** __AUTO_GENERATED: */', '/** __/AUTO_GENERATED */', ); // -- Remove double newlines at top of file -- $parts = explode("\nclass ", $content, 2); if (count($parts) == 2) { while (strpos($parts[0], "\n\n\n") !== false) { $parts[0] = str_replace("\n\n\n", "\n\n", $parts[0]); } $content = implode("\nclass ", $parts); } /** special case for a weird occurance of empty comments */ $content = str_replace("/*\n */\n", '', $content); // Collect all constant names we're about to generate $constants_to_generate = []; foreach ($enums as $column => $enumValues) { foreach ($enumValues as $value => $props) { if (!empty($props['constant'])) { $constants_to_generate[] = $props['constant']; } } } // Remove existing constants that match names we're about to generate if (!empty($constants_to_generate)) { $lines = explode("\n", $content); $filtered_lines = []; foreach ($lines as $line) { $should_keep = true; // Check if this line defines a constant we're about to generate foreach ($constants_to_generate as $const_name) { // Match pattern: (whitespace)const (CONSTANT_NAME) = if (preg_match('/^\s*const\s+' . preg_quote($const_name, '/') . '\s*=/', $line)) { $should_keep = false; break; } } if ($should_keep) { $filtered_lines[] = $line; } } $content = implode("\n", $filtered_lines); } // Add in new constants if (empty($constants)) { $constants = ''; } // Remove /* ... */ comments to find class / namespace $content_clean = preg_replace('!/\*.*?\*/!s', '', $content); // Remove // comments (at the start of a line) to find class / namespace $content_clean = preg_replace('/^\s*\/\/.*$/m', '', $content_clean); // Extract namespace (if necessary) preg_match('/namespace\s+([^;]+);/', $content_clean, $namespaceMatches); $namespace = $namespaceMatches[1] ?? ''; // Extract class name and parent class name (if any) preg_match('/class\s+(\S+)(?:\s+extends\s+(\S+))?/', $content_clean, $classMatches); $className = $classMatches[1] ?? ''; $parentClassName = $classMatches[2] ?? ''; $lines = explode("\n", $content); // Construct the pattern to find the class definition line // Note: If the class extends another, include that in the pattern $classPattern = '/^\s*class\s+' . preg_quote($className, '/'); if (!empty($parentClassName)) { $classPattern .= '\s+extends\s+' . preg_quote($parentClassName, '/'); } $classPattern .= '\s*$/'; $lineNumber = null; // Find the line number of the class definition in the original content foreach ($lines as $index => $line) { if (preg_match($classPattern, $line)) { $lineNumber = $index; break; } } if ($lineNumber === null) { echo "\nWarning, $file_path malformed (0)\n"; continue; } // Split the content at the class definition line $firstPart = array_slice($lines, 0, $lineNumber); $secondPart = array_slice($lines, $lineNumber, 1); $thirdPart = array_slice($lines, $lineNumber + 1); // Trim the remainder $thirdPart = explode("\n", trim(implode("\n", $thirdPart))); // var_dump($thirdPart); if (trim($thirdPart[0]) != '{') { echo "\nWarning, $file_path malformed (1)\n"; continue; } $secondPart[] = '{'; $thirdPart = array_slice($thirdPart, 1); // Do a trim on 'thirdPart' while (count($thirdPart) > 1 && ($item = reset($thirdPart)) !== null && trim($item) === '') { array_shift($thirdPart); } $content = implode("\n", array_merge($firstPart, [$docblock], $secondPart, [$constants], $thirdPart)); // Save if changed if ($org_content != $content) { File::put($file_path, $content); $processed_count++; $this->line(" Updated: {$className}"); } // Skip "No changes" output - only report updates // No method checks needed for this project // The functions from LP_Model like can_view_ajax, filter_for_ajax, etc. are not required here } catch (Exception $e) { $error_count++; $this->error(" Error processing {$className}: " . $e->getMessage()); $warnings[] = "Failed to process {$fullClassName}: " . $e->getMessage(); } } // Display summary $this->newLine(); if ($processed_count > 0) { $this->info("✅ Successfully updated {$processed_count} model(s) with constants and docblocks."); } else { $this->info('No models needed updating.'); } if ($error_count > 0) { $this->error("⚠️ Encountered {$error_count} error(s) during processing."); } foreach ($warnings as $warning) { $this->warn($warning); } } /** Complicated script to get rid of existing code blocks containg string _AUTO_GENERATED */ private function remove_auto_generated_code_blocks($content, $comment_start = '/*', $comment_end = '*/', $comment_contains = false) { $output = []; $remainder = $content; while (true) { $parts = explode($comment_start, $remainder, 2); $output[] = $parts[0]; if (count($parts) < 2) { break; } $remainder = $parts[1]; $parts = explode($comment_end, $remainder, 2); if (count($parts) < 2) { $output[] = $comment_start . $remainder; break; } if ($comment_contains !== false && strpos($parts[0], $comment_contains) === false) { $output[] = $comment_start . $parts[0] . $comment_end; } $remainder = $parts[1]; } $content = implode('', $output); return $content; } }