'App\\Traits\\HasLocation', 'HasSlugHistory' => 'App\\Traits\\HasSlugHistory', 'HasStatistics' => 'App\\Traits\\HasStatistics', 'HasCaching' => 'App\\Traits\\HasCaching', 'HasSoftDeletes' => 'Illuminate\\Database\\Eloquent\\SoftDeletes', 'HasFactory' => 'Illuminate\\Database\\Eloquent\\Factories\\HasFactory', ]; /** * Common ThrillWiki relationships by model type */ protected array $relationshipPatterns = [ 'Park' => [ 'areas' => 'hasMany:ParkArea', 'rides' => 'hasManyThrough:Ride,ParkArea', 'operator' => 'belongsTo:Operator', 'photos' => 'morphMany:Photo', 'reviews' => 'morphMany:Review', ], 'Ride' => [ 'park' => 'belongsTo:Park', 'area' => 'belongsTo:ParkArea', 'manufacturer' => 'belongsTo:Manufacturer', 'designer' => 'belongsTo:Designer', 'photos' => 'morphMany:Photo', 'reviews' => 'morphMany:Review', ], 'Operator' => [ 'parks' => 'hasMany:Park', ], 'Manufacturer' => [ 'rides' => 'hasMany:Ride,manufacturer_id', ], 'Designer' => [ 'rides' => 'hasMany:Ride,designer_id', ], 'Review' => [ 'user' => 'belongsTo:User', 'reviewable' => 'morphTo', ], ]; /** * Execute the console command. */ public function handle() { $this->info('🚀 Generating ThrillWiki Model for: ' . $this->argument('name')); $name = $this->argument('name'); $className = Str::studly($name); $tableName = Str::snake(Str::plural($name)); // Generate model $this->generateModel($className); // Generate optional files if ($this->option('migration')) { $this->generateMigration($className, $tableName); } if ($this->option('factory')) { $this->generateFactory($className); } if ($this->option('api-resource')) { $this->generateApiResource($className); } if ($this->option('with-tests')) { $this->generateTests($className); } $this->displaySummary($className); return Command::SUCCESS; } /** * Generate the model file */ protected function generateModel(string $className): void { $modelPath = app_path("Models/{$className}.php"); if (File::exists($modelPath) && !$this->option('force')) { $this->error("Model {$className} already exists! Use --force to overwrite."); return; } $modelContent = $this->buildModelContent($className); $this->ensureDirectoryExists(dirname($modelPath)); File::put($modelPath, $modelContent); $this->line("✅ Model created: app/Models/{$className}.php"); } /** * Build the model content with ThrillWiki patterns */ protected function buildModelContent(string $className): string { $tableName = Str::snake(Str::plural($className)); $traits = $this->getTraitsForModel($className); $relationships = $this->getRelationshipsForModel($className); $cachingMethods = $this->option('cached') ? $this->getCachingMethods($className) : ''; $stub = <<<'PHP' */ protected $fillable = [ 'name', 'description', 'is_active', // Add more fillable attributes as needed ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'is_active' => 'boolean', 'created_at' => 'datetime', 'updated_at' => 'datetime', // Add more casts as needed ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ // Add hidden attributes if needed ]; // Query Scopes /** * Scope a query to only include active records. */ public function scopeActive($query) { return $query->where('is_active', true); } /** * Scope for optimized queries with common relationships. */ public function scopeOptimized($query) { return $query->with($this->getOptimizedRelations()); } // ThrillWiki Methods /** * Get optimized relations for this model. */ public function getOptimizedRelations(): array { return [ // Define common relationships to eager load ]; } /** * Get cache key for this model instance. */ public function getCacheKey(string $suffix = ''): string { $key = strtolower(class_basename($this)) . '.' . $this->id; return $suffix ? $key . '.' . $suffix : $key; } {RELATIONSHIPS} {CACHING_METHODS} } PHP; return str_replace([ '{CLASS_NAME}', '{TABLE_NAME}', '{TRAIT_IMPORTS}', '{TRAITS}', '{RELATIONSHIPS}', '{CACHING_METHODS}', ], [ $className, $tableName, $this->buildTraitImports($traits), $this->buildTraitUses($traits), $relationships, $cachingMethods, ], $stub); } /** * Get traits for the model based on options and model type */ protected function getTraitsForModel(string $className): array { $traits = ['HasFactory']; // Always include HasFactory // Add SoftDeletes for most models $traits[] = 'HasSoftDeletes'; // Add caching if requested if ($this->option('cached')) { $traits[] = 'HasCaching'; } // Add location trait for location-based models if (in_array($className, ['Park', 'Company', 'ParkArea'])) { $traits[] = 'HasLocation'; } // Add slug history for main entities if (in_array($className, ['Park', 'Ride', 'Company', 'Designer'])) { $traits[] = 'HasSlugHistory'; } // Add statistics for countable entities if (in_array($className, ['Park', 'Ride', 'User'])) { $traits[] = 'HasStatistics'; } return $traits; } /** * Build trait import statements */ protected function buildTraitImports(array $traits): string { $imports = []; foreach ($traits as $trait) { if (isset($this->thrillWikiTraits[$trait])) { $imports[] = "use {$this->thrillWikiTraits[$trait]};"; } } return implode("\n", $imports); } /** * Build trait use statements */ protected function buildTraitUses(array $traits): string { $uses = array_map(function($trait) { return " use {$trait};"; }, $traits); return implode("\n", $uses); } /** * Get relationships for the model */ protected function getRelationshipsForModel(string $className): string { if (!$this->option('with-relationships')) { return ''; } if (!isset($this->relationshipPatterns[$className])) { return ''; } $relationships = []; foreach ($this->relationshipPatterns[$className] as $method => $definition) { $relationships[] = $this->buildRelationshipMethod($method, $definition); } return "\n // Relationships\n\n" . implode("\n\n", $relationships); } /** * Build a relationship method */ protected function buildRelationshipMethod(string $method, string $definition): string { [$type, $model, $foreignKey] = array_pad(explode(',', str_replace(':', ',', $definition)), 3, null); $methodBody = match($type) { 'hasMany' => $foreignKey ? "return \$this->hasMany({$model}::class, '{$foreignKey}');" : "return \$this->hasMany({$model}::class);", 'belongsTo' => $foreignKey ? "return \$this->belongsTo({$model}::class, '{$foreignKey}');" : "return \$this->belongsTo({$model}::class);", 'hasManyThrough' => "return \$this->hasManyThrough({$model}::class, {$foreignKey}::class);", 'morphMany' => "return \$this->morphMany({$model}::class, 'morphable');", 'morphTo' => "return \$this->morphTo();", default => "return \$this->{$type}({$model}::class);" }; return " /**\n * Get the {$method} relationship.\n */\n public function {$method}()\n {\n {$methodBody}\n }"; } /** * Get caching methods for the model */ protected function getCachingMethods(string $className): string { return <<<'PHP' // Caching Methods /** * Remember a value in cache with model-specific key. */ public function remember(string $key, $callback, int $ttl = 3600) { return cache()->remember($this->getCacheKey($key), $ttl, $callback); } /** * Invalidate cache for this model. */ public function invalidateCache(string $key = null): void { if ($key) { cache()->forget($this->getCacheKey($key)); } else { // Clear all cache keys for this model cache()->forget($this->getCacheKey()); } } /** * Boot method to handle cache invalidation. */ protected static function boot() { parent::boot(); static::saved(function ($model) { $model->invalidateCache(); }); static::deleted(function ($model) { $model->invalidateCache(); }); } PHP; } /** * Generate migration file */ protected function generateMigration(string $className, string $tableName): void { $migrationName = 'create_' . $tableName . '_table'; $timestamp = date('Y_m_d_His'); $migrationFile = database_path("migrations/{$timestamp}_{$migrationName}.php"); $migrationContent = $this->buildMigrationContent($className, $tableName, $migrationName); File::put($migrationFile, $migrationContent); $this->line("✅ Migration created: database/migrations/{$timestamp}_{$migrationName}.php"); } /** * Build migration content */ protected function buildMigrationContent(string $className, string $tableName, string $migrationName): string { $migrationClass = Str::studly($migrationName); $stub = <<<'PHP' id(); $table->string('name'); $table->text('description')->nullable(); $table->boolean('is_active')->default(true); // Add common ThrillWiki fields $table->string('slug')->unique(); // Add indexes for performance $table->index(['is_active']); $table->index(['name']); $table->index(['slug']); $table->timestamps(); $table->softDeletes(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('{TABLE_NAME}'); } }; PHP; return str_replace('{TABLE_NAME}', $tableName, $stub); } /** * Generate factory file */ protected function generateFactory(string $className): void { $factoryPath = database_path("factories/{$className}Factory.php"); if (File::exists($factoryPath) && !$this->option('force')) { $this->error("Factory {$className}Factory already exists! Use --force to overwrite."); return; } $factoryContent = $this->buildFactoryContent($className); $this->ensureDirectoryExists(dirname($factoryPath)); File::put($factoryPath, $factoryContent); $this->line("✅ Factory created: database/factories/{$className}Factory.php"); } /** * Build factory content */ protected function buildFactoryContent(string $className): string { $stub = <<<'PHP' */ class {CLASS_NAME}Factory extends Factory { /** * The name of the factory's corresponding model. * * @var string */ protected $model = {CLASS_NAME}::class; /** * Define the model's default state. * * @return array */ public function definition(): array { $name = $this->faker->unique()->words(2, true); return [ 'name' => $name, 'slug' => Str::slug($name), 'description' => $this->faker->paragraphs(2, true), 'is_active' => $this->faker->boolean(90), // 90% chance of being active 'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'), 'updated_at' => function (array $attributes) { return $this->faker->dateTimeBetween($attributes['created_at'], 'now'); }, ]; } /** * Indicate that the model is active. */ public function active(): static { return $this->state(fn (array $attributes) => [ 'is_active' => true, ]); } /** * Indicate that the model is inactive. */ public function inactive(): static { return $this->state(fn (array $attributes) => [ 'is_active' => false, ]); } } PHP; return str_replace('{CLASS_NAME}', $className, $stub); } /** * Generate API resource */ protected function generateApiResource(string $className): void { $resourcePath = app_path("Http/Resources/{$className}Resource.php"); if (File::exists($resourcePath) && !$this->option('force')) { $this->error("Resource {$className}Resource already exists! Use --force to overwrite."); return; } $resourceContent = $this->buildApiResourceContent($className); $this->ensureDirectoryExists(dirname($resourcePath)); File::put($resourcePath, $resourceContent); $this->line("✅ API Resource created: app/Http/Resources/{$className}Resource.php"); } /** * Build API resource content */ protected function buildApiResourceContent(string $className): string { $stub = <<<'PHP' */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'slug' => $this->slug, 'description' => $this->description, 'is_active' => $this->is_active, 'created_at' => $this->created_at?->toISOString(), 'updated_at' => $this->updated_at?->toISOString(), // Include relationships when loaded $this->mergeWhen($this->relationLoaded('relationships'), [ // Add relationship data here ]), ]; } /** * Get additional data that should be returned with the resource array. * * @return array */ public function with(Request $request): array { return [ 'meta' => [ 'model' => '{CLASS_NAME}', 'generated_at' => now()->toISOString(), ], ]; } } PHP; return str_replace('{CLASS_NAME}', $className, $stub); } /** * Generate test files */ protected function generateTests(string $className): void { $testPath = base_path("tests/Feature/{$className}Test.php"); if (File::exists($testPath) && !$this->option('force')) { $this->error("Test {$className}Test already exists! Use --force to overwrite."); return; } $testContent = $this->buildTestContent($className); $this->ensureDirectoryExists(dirname($testPath)); File::put($testPath, $testContent); $this->line("✅ Test created: tests/Feature/{$className}Test.php"); } /** * Build test content */ protected function buildTestContent(string $className): string { $stub = <<<'PHP' create(); $this->assertDatabaseHas('{TABLE_NAME}', [ 'id' => ${LOWER_CLASS_NAME}->id, 'name' => ${LOWER_CLASS_NAME}->name, ]); } /** * Test model factory. */ public function test_{LOWER_CLASS_NAME}_factory_works(): void { ${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create(); $this->assertInstanceOf({CLASS_NAME}::class, ${LOWER_CLASS_NAME}); $this->assertNotEmpty(${LOWER_CLASS_NAME}->name); $this->assertIsBool(${LOWER_CLASS_NAME}->is_active); } /** * Test active scope. */ public function test_active_scope_filters_correctly(): void { {CLASS_NAME}::factory()->active()->create(); {CLASS_NAME}::factory()->inactive()->create(); $activeCount = {CLASS_NAME}::active()->count(); $totalCount = {CLASS_NAME}::count(); $this->assertEquals(1, $activeCount); $this->assertEquals(2, $totalCount); } /** * Test cache key generation. */ public function test_cache_key_generation(): void { ${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create(); $cacheKey = ${LOWER_CLASS_NAME}->getCacheKey(); $expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id; $this->assertEquals($expectedKey, $cacheKey); } /** * Test cache key with suffix. */ public function test_cache_key_with_suffix(): void { ${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create(); $cacheKey = ${LOWER_CLASS_NAME}->getCacheKey('details'); $expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id . '.details'; $this->assertEquals($expectedKey, $cacheKey); } /** * Test soft deletes. */ public function test_soft_deletes_work(): void { ${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create(); ${LOWER_CLASS_NAME}->delete(); $this->assertSoftDeleted(${LOWER_CLASS_NAME}); // Test that it's excluded from normal queries $this->assertEquals(0, {CLASS_NAME}::count()); // Test that it's included in withTrashed queries $this->assertEquals(1, {CLASS_NAME}::withTrashed()->count()); } } PHP; return str_replace([ '{CLASS_NAME}', '{LOWER_CLASS_NAME}', '{TABLE_NAME}', ], [ $className, strtolower($className), Str::snake(Str::plural($className)), ], $stub); } /** * Ensure directory exists */ protected function ensureDirectoryExists(string $directory): void { if (!File::isDirectory($directory)) { File::makeDirectory($directory, 0755, true); } } /** * Display summary of generated files */ protected function displaySummary(string $className): void { $this->newLine(); $this->info("🎉 ThrillWiki Model '{$className}' created successfully!"); $this->newLine(); $this->line("📁 Files Generated:"); $this->line(" • app/Models/{$className}.php"); if ($this->option('migration')) { $this->line(" • database/migrations/[timestamp]_create_" . Str::snake(Str::plural($className)) . "_table.php"); } if ($this->option('factory')) { $this->line(" • database/factories/{$className}Factory.php"); } if ($this->option('api-resource')) { $this->line(" • app/Http/Resources/{$className}Resource.php"); } if ($this->option('with-tests')) { $this->line(" • tests/Feature/{$className}Test.php"); } $this->newLine(); $this->line("🚀 Next Steps:"); if ($this->option('migration')) { $this->line(" 1. Run migration: php artisan migrate"); } if ($this->option('with-tests')) { $this->line(" 2. Run tests: php artisan test --filter {$className}Test"); } $this->line(" 3. Customize model attributes and relationships"); $this->line(" 4. Update migration with specific fields"); if ($this->option('factory')) { $this->line(" 5. Customize factory with realistic data"); } $this->newLine(); } }