Files
rspade_system/app/RSpade/Commands/Migrate/Document_Models_Command.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

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

328 lines
11 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;
}
$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 = [];
$dateColumns = [];
foreach ($columns as $column) {
try {
$type = DB::getSchemaBuilder()->getColumnType($tableName, $column);
$phpType = $this->mapDatabaseTypeToPhp($type);
// Track datetime columns for $dates array
if (in_array($type, ['datetime', 'timestamp', 'date', 'time'])) {
$dateColumns[] = $column;
}
$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 dates array if needed
$datesArray = '';
if (!empty($dateColumns)) {
$datesArray = "\n /**\n";
$datesArray .= " * _AUTO_GENERATED_ Date columns for Carbon casting\n";
$datesArray .= " */\n";
$datesArray .= " protected \$dates = [\n";
foreach ($dateColumns as $column) {
$datesArray .= " '$column',\n";
}
$datesArray .= " ];\n";
}
// 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, $datesArray, $constantsBlock);
}
protected function updateModelFile($filePath, $className, $docBlock, $datesArray, $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 ($datesArray) {
$this->line(" Dates array to add:");
$this->line($datesArray);
}
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
$content = preg_replace(
'/\s*\/\*\*\s*\n\s*\*\s*_AUTO_GENERATED_\s*Date columns.*?\];/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
);
// 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 dates array after constants (or after opening brace)
if ($datesArray) {
$newContent .= $datesArray;
}
// 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';
}
}