mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-23 11:11:11 -05:00
feat: Implement rides management with CRUD functionality
- Added rides index view with search and filter options. - Created rides show view to display ride details. - Implemented API routes for rides. - Developed authentication routes for user registration, login, and email verification. - Created tests for authentication, email verification, password reset, and user profile management. - Added feature tests for rides and operators, including creation, updating, deletion, and searching. - Implemented soft deletes and caching for rides and operators. - Enhanced manufacturer and operator model tests for various functionalities.
This commit is contained in:
1163
app/Console/Commands/MakeThrillWikiCrud.php
Normal file
1163
app/Console/Commands/MakeThrillWikiCrud.php
Normal file
File diff suppressed because it is too large
Load Diff
392
app/Console/Commands/MakeThrillWikiLivewire.php
Normal file
392
app/Console/Commands/MakeThrillWikiLivewire.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MakeThrillWikiLivewire extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'make:thrillwiki-livewire {name : The name of the component}
|
||||
{--reusable : Generate a reusable component with optimization traits}
|
||||
{--with-tests : Generate test files for the component}
|
||||
{--cached : Add caching optimization to the component}
|
||||
{--paginated : Add pagination support to the component}
|
||||
{--force : Overwrite existing files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Create a ThrillWiki-optimized Livewire component with built-in patterns and performance optimization';
|
||||
|
||||
protected Filesystem $files;
|
||||
|
||||
public function __construct(Filesystem $files)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->files = $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$name = $this->argument('name');
|
||||
$className = Str::studly($name);
|
||||
$kebabName = Str::kebab($name);
|
||||
|
||||
$this->info("🚀 Generating ThrillWiki Livewire Component: {$className}");
|
||||
|
||||
// Generate the component class
|
||||
$this->generateComponent($className, $kebabName);
|
||||
|
||||
// Generate the view file
|
||||
$this->generateView($className, $kebabName);
|
||||
|
||||
// Generate tests if requested
|
||||
if ($this->option('with-tests')) {
|
||||
$this->generateTest($className);
|
||||
}
|
||||
|
||||
$this->displaySummary($className, $kebabName);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function generateComponent(string $className, string $kebabName): void
|
||||
{
|
||||
$componentPath = app_path("Livewire/{$className}.php");
|
||||
|
||||
if ($this->files->exists($componentPath) && !$this->option('force')) {
|
||||
$this->error("Component {$className} already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getComponentStub();
|
||||
$content = $this->replaceStubPlaceholders($stub, $className, $kebabName);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($componentPath));
|
||||
$this->files->put($componentPath, $content);
|
||||
|
||||
$this->info("✅ Component created: app/Livewire/{$className}.php");
|
||||
}
|
||||
|
||||
protected function generateView(string $className, string $kebabName): void
|
||||
{
|
||||
$viewPath = resource_path("views/livewire/{$kebabName}.blade.php");
|
||||
|
||||
if ($this->files->exists($viewPath) && !$this->option('force')) {
|
||||
$this->error("View {$kebabName}.blade.php already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getViewStub();
|
||||
$content = $this->replaceViewPlaceholders($stub, $className, $kebabName);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($viewPath));
|
||||
$this->files->put($viewPath, $content);
|
||||
|
||||
$this->info("✅ View created: resources/views/livewire/{$kebabName}.blade.php");
|
||||
}
|
||||
|
||||
protected function generateTest(string $className): void
|
||||
{
|
||||
$testPath = base_path("tests/Feature/Livewire/{$className}Test.php");
|
||||
|
||||
if ($this->files->exists($testPath) && !$this->option('force')) {
|
||||
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getTestStub();
|
||||
$content = $this->replaceTestPlaceholders($stub, $className);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($testPath));
|
||||
$this->files->put($testPath, $content);
|
||||
|
||||
$this->info("✅ Test created: tests/Feature/Livewire/{$className}Test.php");
|
||||
}
|
||||
|
||||
protected function getComponentStub(): string
|
||||
{
|
||||
$traits = [];
|
||||
$imports = ['use Livewire\Component;'];
|
||||
$properties = [];
|
||||
$methods = [];
|
||||
|
||||
// Add pagination if requested
|
||||
if ($this->option('paginated')) {
|
||||
$imports[] = 'use Livewire\WithPagination;';
|
||||
$traits[] = 'WithPagination';
|
||||
$properties[] = ' protected $paginationTheme = \'tailwind\';';
|
||||
}
|
||||
|
||||
// Add caching optimization if requested
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$imports[] = 'use Illuminate\Support\Facades\Cache;';
|
||||
$methods[] = $this->getCachingMethods();
|
||||
}
|
||||
|
||||
// Build traits string
|
||||
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
|
||||
|
||||
// Build properties string
|
||||
$propertiesString = empty($properties) ? '' : "\n" . implode("\n", $properties) . "\n";
|
||||
|
||||
// Build methods string
|
||||
$methodsString = implode("\n\n", $methods);
|
||||
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
{IMPORTS}
|
||||
|
||||
class {CLASS_NAME} extends Component
|
||||
{{TRAITS}{PROPERTIES}
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.{VIEW_NAME}');
|
||||
}{METHODS}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function getViewStub(): string
|
||||
{
|
||||
if ($this->option('reusable')) {
|
||||
return <<<BLADE
|
||||
{{-- ThrillWiki Reusable Component: {CLASS_NAME} --}}
|
||||
<div class="thrillwiki-component"
|
||||
x-data="{ loading: false }"
|
||||
wire:loading.class="opacity-50">
|
||||
|
||||
{{-- Component Header --}}
|
||||
<div class="component-header mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{CLASS_NAME}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{-- Component Content --}}
|
||||
<div class="component-content">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
ThrillWiki component content goes here.
|
||||
</p>
|
||||
|
||||
{{-- Example interactive element --}}
|
||||
<button wire:click="\$refresh"
|
||||
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
|
||||
Refresh Component
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Loading State --}}
|
||||
<div wire:loading wire:target="\$refresh"
|
||||
class="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
|
||||
return <<<BLADE
|
||||
{{-- ThrillWiki Component: {CLASS_NAME} --}}
|
||||
<div class="thrillwiki-component">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
{CLASS_NAME}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
ThrillWiki component content goes here.
|
||||
</p>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
|
||||
protected function getTestStub(): string
|
||||
{
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\{CLASS_NAME};
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class {CLASS_NAME}Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @test */
|
||||
public function component_can_render(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertStatus(200)
|
||||
->assertSee('{CLASS_NAME}');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function component_can_mount_successfully(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function component_follows_thrillwiki_patterns(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertViewIs('livewire.{VIEW_NAME}');
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function getCachingMethods(): string
|
||||
{
|
||||
return <<<PHP
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string \$suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . \$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string \$key, \$callback, int \$ttl = 3600)
|
||||
{
|
||||
return Cache::remember(\$this->getCacheKey(\$key), \$ttl, \$callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string \$key = null): void
|
||||
{
|
||||
if (\$key) {
|
||||
Cache::forget(\$this->getCacheKey(\$key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function replaceStubPlaceholders(string $stub, string $className, string $kebabName): string
|
||||
{
|
||||
$imports = ['use Livewire\Component;'];
|
||||
$traits = [];
|
||||
|
||||
if ($this->option('paginated')) {
|
||||
$imports[] = 'use Livewire\WithPagination;';
|
||||
$traits[] = 'WithPagination';
|
||||
}
|
||||
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$imports[] = 'use Illuminate\Support\Facades\Cache;';
|
||||
}
|
||||
|
||||
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
|
||||
$importsString = implode("\n", $imports);
|
||||
$methodsString = '';
|
||||
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$methodsString = "\n\n" . $this->getCachingMethods();
|
||||
}
|
||||
|
||||
return str_replace(
|
||||
['{IMPORTS}', '{CLASS_NAME}', '{VIEW_NAME}', '{TRAITS}', '{PROPERTIES}', '{METHODS}'],
|
||||
[$importsString, $className, $kebabName, $traitsString, '', $methodsString],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function replaceViewPlaceholders(string $stub, string $className, string $kebabName): string
|
||||
{
|
||||
return str_replace(
|
||||
['{CLASS_NAME}', '{VIEW_NAME}'],
|
||||
[$className, $kebabName],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function replaceTestPlaceholders(string $stub, string $className): string
|
||||
{
|
||||
return str_replace(
|
||||
['{CLASS_NAME}', '{VIEW_NAME}'],
|
||||
[$className, Str::kebab($className)],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function displaySummary(string $className, string $kebabName): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("🎉 ThrillWiki Livewire Component '{$className}' created successfully!");
|
||||
$this->newLine();
|
||||
|
||||
$this->comment("📁 Files Generated:");
|
||||
$this->line(" • app/Livewire/{$className}.php");
|
||||
$this->line(" • resources/views/livewire/{$kebabName}.blade.php");
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • tests/Feature/Livewire/{$className}Test.php");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->comment("🚀 Features Added:");
|
||||
|
||||
if ($this->option('reusable')) {
|
||||
$this->line(" • Reusable component patterns with optimization traits");
|
||||
}
|
||||
|
||||
if ($this->option('cached')) {
|
||||
$this->line(" • Caching optimization methods");
|
||||
}
|
||||
|
||||
if ($this->option('paginated')) {
|
||||
$this->line(" • Pagination support with Tailwind theme");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • Automated test suite with ThrillWiki patterns");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->comment("📝 Next Steps:");
|
||||
$this->line(" 1. Customize the component logic in app/Livewire/{$className}.php");
|
||||
$this->line(" 2. Update the view template in resources/views/livewire/{$kebabName}.blade.php");
|
||||
$this->line(" 3. Include the component in your templates with <livewire:{$kebabName} />");
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" 4. Run tests with: php artisan test --filter {$className}Test");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("✨ Happy coding with ThrillWiki acceleration patterns!");
|
||||
}
|
||||
}
|
||||
857
app/Console/Commands/MakeThrillWikiModel.php
Normal file
857
app/Console/Commands/MakeThrillWikiModel.php
Normal file
@@ -0,0 +1,857 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class MakeThrillWikiModel extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'make:thrillwiki-model {name : The name of the model}
|
||||
{--migration : Generate a migration file}
|
||||
{--factory : Generate a model factory}
|
||||
{--with-relationships : Include common ThrillWiki relationships}
|
||||
{--cached : Add caching traits and methods}
|
||||
{--api-resource : Generate API resource class}
|
||||
{--with-tests : Generate model tests}
|
||||
{--force : Overwrite existing files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a ThrillWiki model with optimized patterns, traits, and optional related files';
|
||||
|
||||
/**
|
||||
* ThrillWiki traits for different model types
|
||||
*/
|
||||
protected array $thrillWikiTraits = [
|
||||
'HasLocation' => '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'
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
{TRAIT_IMPORTS}
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} Model
|
||||
*
|
||||
* Generated by ThrillWiki Model Generator
|
||||
* Includes ThrillWiki optimization patterns and performance enhancements
|
||||
*/
|
||||
class {CLASS_NAME} extends Model
|
||||
{
|
||||
{TRAITS}
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '{TABLE_NAME}';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'is_active',
|
||||
// Add more fillable attributes as needed
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
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'
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('{TABLE_NAME}', function (Blueprint $table) {
|
||||
$table->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'
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\{CLASS_NAME};
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\{CLASS_NAME}>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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'
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} API Resource
|
||||
*
|
||||
* Transforms {CLASS_NAME} model data for API responses
|
||||
* Includes ThrillWiki optimization patterns
|
||||
*/
|
||||
class {CLASS_NAME}Resource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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'
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\{CLASS_NAME};
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} Model Feature Tests
|
||||
*
|
||||
* Tests for ThrillWiki {CLASS_NAME} model functionality
|
||||
*/
|
||||
class {CLASS_NAME}Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase, WithFaker;
|
||||
|
||||
/**
|
||||
* Test model creation.
|
||||
*/
|
||||
public function test_can_create_{LOWER_CLASS_NAME}(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user