Files
rspade_system/app/RSpade/Commands/Migrate/Document_Models_Command.php
2025-12-26 02:17:31 +00:00

335 lines
12 KiB
PHP

<?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 (BEM-style: field__property)
$enumProperties[] = " * @property-read string \${$columnName}__label";
$enumProperties[] = " * @property-read string \${$columnName}__constant";
// Add enum static methods (BEM-style, mirrored in JavaScript stubs)
$enumMethods[] = " * @method static array {$columnName}__enum() Get all enum definitions with full metadata";
$enumMethods[] = " * @method static array {$columnName}__enum_select() Get selectable items for dropdowns";
$enumMethods[] = " * @method static array {$columnName}__enum_labels() Get simple id => label map";
$enumMethods[] = " * @method static array {$columnName}__enum_ids() Get array of all valid 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';
}
}