'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'; } }