Fix Form_Utils bugs and unify error handling documentation Protect framework files from auto-modification when not in developer mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
335 lines
12 KiB
PHP
Executable File
335 lines
12 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\Commands\Migrate;
|
|
|
|
use App\Console\Commands\FrameworkDeveloperCommand;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\RSpade\Core\Manifest\Manifest;
|
|
|
|
class Document_Models_Command extends FrameworkDeveloperCommand
|
|
{
|
|
protected $signature = 'rsx:migrate:document_models
|
|
{--dry-run : Show what would be changed without modifying files}';
|
|
|
|
protected $description = 'Generate IDE type hints and enum constants for RSX models based on database schema';
|
|
|
|
/**
|
|
* Type mapping from database types to PHP types
|
|
*/
|
|
protected $typeMap = [
|
|
'bigint' => 'int',
|
|
'int' => 'int',
|
|
'integer' => 'int',
|
|
'tinyint' => 'int',
|
|
'smallint' => 'int',
|
|
'mediumint' => 'int',
|
|
'decimal' => 'float',
|
|
'float' => 'float',
|
|
'double' => 'float',
|
|
'real' => 'float',
|
|
'boolean' => 'bool',
|
|
'date' => 'string',
|
|
'datetime' => 'string',
|
|
'timestamp' => 'string',
|
|
'time' => 'string',
|
|
'year' => 'int',
|
|
'char' => 'string',
|
|
'varchar' => 'string',
|
|
'text' => 'string',
|
|
'mediumtext' => 'string',
|
|
'longtext' => 'string',
|
|
'json' => 'array',
|
|
'jsonb' => 'array',
|
|
'blob' => 'string',
|
|
'binary' => 'string',
|
|
];
|
|
|
|
public function handle()
|
|
{
|
|
if (app()->environment('production')) {
|
|
$this->error('This command cannot be run in production environment');
|
|
return 1;
|
|
}
|
|
|
|
$this->info('Discovering RSX models from manifest...');
|
|
|
|
// Use Manifest to discover all models extending Rsx_Model_Abstract
|
|
$manifest_data = Manifest::php_get_extending('App\RSpade\Core\Database\Models\Rsx_Model_Abstract');
|
|
|
|
$models = [];
|
|
foreach ($manifest_data as $key => $class_info) {
|
|
// The key is the class name, the info contains 'file', 'fqcn', etc.
|
|
$className = $class_info['fqcn'] ?? $class_info['class'] ?? null;
|
|
$filePath = $class_info['file'] ?? null;
|
|
|
|
if (!$className || !$filePath) {
|
|
continue;
|
|
}
|
|
|
|
// Skip abstract classes
|
|
if (isset($class_info['is_abstract']) && $class_info['is_abstract']) {
|
|
continue;
|
|
}
|
|
|
|
// Skip the base abstract classes themselves
|
|
if (in_array($className, [
|
|
'App\RSpade\Core\Database\Models\Rsx_Model_Abstract',
|
|
'App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract',
|
|
'App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract'
|
|
])) {
|
|
continue;
|
|
}
|
|
|
|
// Store the full path to the file (prepend project root if needed)
|
|
$fullPath = base_path($filePath);
|
|
if (!file_exists($fullPath)) {
|
|
$this->warn("File not found: $fullPath");
|
|
continue;
|
|
}
|
|
|
|
// Skip framework files (app/RSpade/) unless in framework developer mode
|
|
// This prevents end users from accidentally modifying framework source
|
|
if (str_starts_with($filePath, 'app/RSpade/') && !config('rsx.code_quality.is_framework_developer', false)) {
|
|
continue;
|
|
}
|
|
|
|
$models[$className] = $fullPath;
|
|
}
|
|
|
|
$this->info('Found ' . count($models) . ' models to process');
|
|
|
|
$processedCount = 0;
|
|
$errorCount = 0;
|
|
|
|
foreach ($models as $className => $filePath) {
|
|
try {
|
|
$this->processModel($className, $filePath);
|
|
$processedCount++;
|
|
} catch (\Exception $e) {
|
|
$errorCount++;
|
|
$this->error("Error processing $className: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
$this->info("Completed: $processedCount models processed, $errorCount errors");
|
|
|
|
return $errorCount > 0 ? 1 : 0;
|
|
}
|
|
|
|
protected function processModel($className, $filePath)
|
|
{
|
|
$this->info("Processing: $className");
|
|
|
|
// Ensure class is loaded
|
|
if (!class_exists($className)) {
|
|
throw new \Exception("Class $className not found");
|
|
}
|
|
|
|
// Instantiate model to get table name
|
|
$model = new $className();
|
|
$tableName = $model->getTable();
|
|
|
|
if (!$tableName) {
|
|
throw new \Exception("Could not determine table name for $className");
|
|
}
|
|
|
|
// Get column information
|
|
$columns = Schema::getColumnListing($tableName);
|
|
if (empty($columns)) {
|
|
$this->warn(" No columns found for table $tableName");
|
|
return;
|
|
}
|
|
|
|
// Build property annotations
|
|
$properties = [];
|
|
|
|
foreach ($columns as $column) {
|
|
try {
|
|
$type = DB::getSchemaBuilder()->getColumnType($tableName, $column);
|
|
$phpType = $this->mapDatabaseTypeToPhp($type);
|
|
|
|
// Note: Type casting (boolean, datetime, date, time) is handled automatically
|
|
// by getCasts() in Rsx_Model_Abstract via manifest metadata.
|
|
// No need to generate $casts arrays in model files.
|
|
|
|
$properties[] = " * @property $phpType \$$column";
|
|
} catch (\Exception $e) {
|
|
$this->warn(" Could not determine type for column $column: " . $e->getMessage());
|
|
$properties[] = " * @property mixed \$$column";
|
|
}
|
|
}
|
|
|
|
// Process enums if they exist
|
|
$enumConstants = [];
|
|
$enumProperties = [];
|
|
$enumMethods = [];
|
|
|
|
if (property_exists($className, 'enums') && !empty($className::$enums)) {
|
|
foreach ($className::$enums as $columnName => $enumDefinitions) {
|
|
// Add enum accessor properties
|
|
$enumProperties[] = " * @property-read string \${$columnName}_label";
|
|
$enumProperties[] = " * @property-read string \${$columnName}_constant";
|
|
$enumProperties[] = " * @property-read array \${$columnName}_enum_val";
|
|
|
|
// Add enum static methods
|
|
$enumMethods[] = " * @method static array {$columnName}_enum()";
|
|
$enumMethods[] = " * @method static array {$columnName}_enum_select()";
|
|
$enumMethods[] = " * @method static array {$columnName}_enum_ids()";
|
|
|
|
// Generate constants for each enum value
|
|
foreach ($enumDefinitions as $value => $definition) {
|
|
if (isset($definition['constant'])) {
|
|
$constantName = $definition['constant'];
|
|
$enumConstants[] = " const $constantName = $value;";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the complete DocBlock
|
|
$docBlock = "/**\n";
|
|
$docBlock .= " * _AUTO_GENERATED_ Database type hints - do not edit manually\n";
|
|
$docBlock .= " * Generated on: " . date('Y-m-d H:i:s') . "\n";
|
|
$docBlock .= " * Table: $tableName\n";
|
|
$docBlock .= " *\n";
|
|
|
|
// Add property annotations
|
|
if (!empty($properties)) {
|
|
$docBlock .= implode("\n", $properties) . "\n";
|
|
}
|
|
|
|
// Add enum properties
|
|
if (!empty($enumProperties)) {
|
|
$docBlock .= " *\n";
|
|
$docBlock .= implode("\n", $enumProperties) . "\n";
|
|
}
|
|
|
|
// Add enum methods
|
|
if (!empty($enumMethods)) {
|
|
$docBlock .= " *\n";
|
|
$docBlock .= implode("\n", $enumMethods) . "\n";
|
|
}
|
|
|
|
// Add Eloquent mixin for IDE support
|
|
$docBlock .= " *\n";
|
|
$docBlock .= " * @mixin \\Eloquent\n";
|
|
$docBlock .= " */";
|
|
|
|
// Build enum constants block if needed
|
|
$constantsBlock = '';
|
|
if (!empty($enumConstants)) {
|
|
$constantsBlock = "\n /**\n";
|
|
$constantsBlock .= " * _AUTO_GENERATED_ Enum constants\n";
|
|
$constantsBlock .= " */\n";
|
|
$constantsBlock .= implode("\n", $enumConstants) . "\n";
|
|
}
|
|
|
|
// Now modify the file
|
|
$this->updateModelFile($filePath, $className, $docBlock, $constantsBlock);
|
|
}
|
|
|
|
protected function updateModelFile($filePath, $className, $docBlock, $constantsBlock)
|
|
{
|
|
$content = file_get_contents($filePath);
|
|
|
|
if ($this->option('dry-run')) {
|
|
$this->info(" [DRY-RUN] Would update $filePath");
|
|
$this->line(" DocBlock to add:");
|
|
$this->line($docBlock);
|
|
if ($constantsBlock) {
|
|
$this->line(" Constants to add:");
|
|
$this->line($constantsBlock);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Remove existing auto-generated sections
|
|
$content = preg_replace(
|
|
'/\/\*\*\s*\n\s*\*\s*_AUTO_GENERATED_.*?\*\/\s*\n/s',
|
|
'',
|
|
$content
|
|
);
|
|
|
|
// Remove existing auto-generated dates array (deprecated)
|
|
$content = preg_replace(
|
|
'/\s*\/\*\*\s*\n\s*\*\s*_AUTO_GENERATED_\s*Date columns.*?\];/s',
|
|
'',
|
|
$content
|
|
);
|
|
|
|
// Remove existing auto-generated casts array (no longer generated)
|
|
$content = preg_replace(
|
|
'/\s*\/\*\*\s*\n\s*\*\s*_AUTO_GENERATED_\s*Type casts.*?\];/s',
|
|
'',
|
|
$content
|
|
);
|
|
|
|
// Remove existing auto-generated constants
|
|
$content = preg_replace(
|
|
'/\s*\/\*\*\s*\n\s*\*\s*_AUTO_GENERATED_\s*Enum constants.*?\n(?:\s*const\s+\w+\s*=\s*[^;]+;\n)+/s',
|
|
'',
|
|
$content
|
|
);
|
|
|
|
// Remove duplicate constant declarations that match what we're about to generate
|
|
// This cleans up any manual or leftover constants that duplicate the auto-generated ones
|
|
if ($constantsBlock) {
|
|
// Extract each constant line from the block we're about to add
|
|
preg_match_all('/const\s+\w+\s*=\s*[^;]+;/', $constantsBlock, $matches);
|
|
foreach ($matches[0] as $constantLine) {
|
|
$trimmedConstant = trim($constantLine);
|
|
// Remove any lines in the file that match this exact constant declaration
|
|
$content = preg_replace(
|
|
'/^\s*' . preg_quote($trimmedConstant, '/') . '\s*$/m',
|
|
'',
|
|
$content
|
|
);
|
|
}
|
|
// Clean up any resulting multiple blank lines
|
|
$content = preg_replace('/\n{3,}/', "\n\n", $content);
|
|
}
|
|
|
|
// Find the class declaration
|
|
$classPattern = '/(^|\n)((?:abstract\s+)?class\s+' . preg_quote(class_basename($className), '/') . '\s+extends\s+[^\{]+)\s*\{/';
|
|
|
|
if (!preg_match($classPattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
|
|
throw new \Exception("Could not find class declaration in $filePath");
|
|
}
|
|
|
|
$classDeclarationStart = $matches[0][1];
|
|
$classOpeningBrace = $classDeclarationStart + strlen($matches[0][0]) - 1;
|
|
|
|
// Insert DocBlock before class declaration
|
|
$beforeClass = substr($content, 0, $matches[1][1] + strlen($matches[1][0]));
|
|
$classDeclaration = $matches[2][0];
|
|
$afterClassDeclaration = substr($content, $classOpeningBrace);
|
|
|
|
// Build the new content
|
|
$newContent = $beforeClass . $docBlock . "\n" . $classDeclaration . " {";
|
|
|
|
// Add constants right after opening brace
|
|
if ($constantsBlock) {
|
|
$newContent .= $constantsBlock;
|
|
}
|
|
|
|
// Add the rest of the file
|
|
$newContent .= substr($afterClassDeclaration, 1); // Skip the opening brace we already added
|
|
|
|
// Write the file
|
|
file_put_contents($filePath, $newContent);
|
|
$this->info(" Updated $filePath");
|
|
}
|
|
|
|
protected function mapDatabaseTypeToPhp($dbType)
|
|
{
|
|
$dbType = strtolower($dbType);
|
|
|
|
// Handle types with parentheses (e.g., "varchar(255)")
|
|
if (preg_match('/^(\w+)/', $dbType, $matches)) {
|
|
$dbType = $matches[1];
|
|
}
|
|
|
|
return $this->typeMap[$dbType] ?? 'mixed';
|
|
}
|
|
} |