Files
rspade_system/app/RSpade/Commands/Rsx/Constants_Regenerate_Command.php
root faf34e1a9d Fix enum column validation and migration data types
Remove numbered emoji and add no-emoji policy to CLAUDE.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 07:33:24 +00:00

440 lines
19 KiB
PHP
Executable File

<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Commands\Rsx;
use App\RSpade\Core\Manifest\Manifest;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use ReflectionClass;
/**
* Regenerates model constants and docblocks based on $enums
*
* This command scans all model files that extend Rsx_Model_Abstract, looking for
* static $enums properties. For each model with enums defined, it:
*
* 1. Updates PHPDoc comments with @property tags for all database columns
* 2. Adds @method tags for enum-related static methods
* 3. Adds @property-read tags for dynamic enum properties
* 4. Adds class constants for enum values
* 5. Updates model files in-place with new constants and docblocks
*
* The command also validates enum definitions to ensure there are no duplicate constants.
*/
class Constants_Regenerate_Command extends Command
{
protected $signature = 'rsx:constants:regenerate';
protected $description = 'Regenerate model constants and docblocks based on $enums';
public function handle()
{
$this->info('Regenerating model constants from enum definitions...');
// 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;
}
$this->line('Processing: ' . basename($file_path));
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->info(" Updated: {$className}");
} else {
$this->line(" No changes: {$className}");
}
// 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;
}
}