🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
422 lines
18 KiB
PHP
Executable File
422 lines
18 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'];
|
|
|
|
if (!in_array($columnType, $validIntegerTypes)) {
|
|
$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();
|
|
$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, TINYINT, or MEDIUMINT), not VARCHAR or other string 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 an integer");
|
|
$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;
|
|
}
|
|
}
|