mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 14:11:10 -05:00
- 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.
1163 lines
39 KiB
PHP
1163 lines
39 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Filesystem\Filesystem;
|
|
|
|
class MakeThrillWikiCrud extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*/
|
|
protected $signature = 'make:thrillwiki-crud {name : The name of the CRUD entity}
|
|
{--model= : Override the model name}
|
|
{--controller= : Override the controller name}
|
|
{--migration : Generate migration file}
|
|
{--api : Generate API routes and resources}
|
|
{--with-tests : Generate test files}
|
|
{--force : Overwrite existing files}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*/
|
|
protected $description = 'Generate a complete ThrillWiki CRUD with controller, model, views, routes, and tests';
|
|
|
|
/**
|
|
* Filesystem instance
|
|
*/
|
|
protected Filesystem $files;
|
|
|
|
/**
|
|
* Create a new command instance.
|
|
*/
|
|
public function __construct(Filesystem $files)
|
|
{
|
|
parent::__construct();
|
|
$this->files = $files;
|
|
}
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle(): int
|
|
{
|
|
$name = $this->argument('name');
|
|
$this->info("🚀 Generating ThrillWiki CRUD for: {$name}");
|
|
|
|
// Prepare naming conventions
|
|
$names = $this->prepareNames($name);
|
|
|
|
try {
|
|
// Generate files
|
|
$this->generateModel($names);
|
|
$this->generateController($names);
|
|
$this->generateFormRequest($names);
|
|
$this->generateViews($names);
|
|
$this->generateRoutes($names);
|
|
|
|
if ($this->option('migration')) {
|
|
$this->generateMigration($names);
|
|
}
|
|
|
|
if ($this->option('api')) {
|
|
$this->generateApiResources($names);
|
|
}
|
|
|
|
if ($this->option('with-tests')) {
|
|
$this->generateTests($names);
|
|
}
|
|
|
|
$this->displaySuccessMessage($names);
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error("❌ Error generating CRUD: " . $e->getMessage());
|
|
return self::FAILURE;
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Prepare all naming variations
|
|
*/
|
|
protected function prepareNames(string $name): array
|
|
{
|
|
$studly = Str::studly($name);
|
|
$snake = Str::snake($name);
|
|
$kebab = Str::kebab($name);
|
|
$plural = Str::plural($snake);
|
|
$pluralStudly = Str::studly($plural);
|
|
|
|
return [
|
|
'name' => $name,
|
|
'studly' => $studly,
|
|
'snake' => $snake,
|
|
'kebab' => $kebab,
|
|
'plural' => $plural,
|
|
'pluralStudly' => $pluralStudly,
|
|
'camel' => Str::camel($name),
|
|
'pluralCamel' => Str::camel($plural),
|
|
'title' => Str::title(str_replace('_', ' ', $snake)),
|
|
'pluralTitle' => Str::title(str_replace('_', ' ', $plural)),
|
|
'model' => $this->option('model') ?: $studly,
|
|
'controller' => $this->option('controller') ?: $studly . 'Controller',
|
|
'table' => $plural,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate the Eloquent model
|
|
*/
|
|
protected function generateModel(array $names): void
|
|
{
|
|
$path = app_path("Models/{$names['model']}.php");
|
|
|
|
if ($this->files->exists($path) && !$this->option('force')) {
|
|
$this->warn("⚠️ Model already exists: {$path}");
|
|
return;
|
|
}
|
|
|
|
$stub = $this->getModelStub();
|
|
$content = $this->replaceStubVariables($stub, $names);
|
|
|
|
$this->files->put($path, $content);
|
|
$this->info("✅ Model created: app/Models/{$names['model']}.php");
|
|
}
|
|
|
|
/**
|
|
* Generate the controller
|
|
*/
|
|
protected function generateController(array $names): void
|
|
{
|
|
$path = app_path("Http/Controllers/{$names['controller']}.php");
|
|
|
|
if ($this->files->exists($path) && !$this->option('force')) {
|
|
$this->warn("⚠️ Controller already exists: {$path}");
|
|
return;
|
|
}
|
|
|
|
$stub = $this->getControllerStub();
|
|
$content = $this->replaceStubVariables($stub, $names);
|
|
|
|
$this->files->put($path, $content);
|
|
$this->info("✅ Controller created: app/Http/Controllers/{$names['controller']}.php");
|
|
}
|
|
|
|
/**
|
|
* Generate form request validation
|
|
*/
|
|
protected function generateFormRequest(array $names): void
|
|
{
|
|
$requestName = $names['studly'] . 'Request';
|
|
$path = app_path("Http/Requests/{$requestName}.php");
|
|
|
|
if ($this->files->exists($path) && !$this->option('force')) {
|
|
$this->warn("⚠️ Form Request already exists: {$path}");
|
|
return;
|
|
}
|
|
|
|
if (!$this->files->isDirectory(dirname($path))) {
|
|
$this->files->makeDirectory(dirname($path), 0755, true);
|
|
}
|
|
|
|
$stub = $this->getFormRequestStub();
|
|
$content = $this->replaceStubVariables($stub, array_merge($names, ['requestName' => $requestName]));
|
|
|
|
$this->files->put($path, $content);
|
|
$this->info("✅ Form Request created: app/Http/Requests/{$requestName}.php");
|
|
}
|
|
|
|
/**
|
|
* Generate Blade views
|
|
*/
|
|
protected function generateViews(array $names): void
|
|
{
|
|
$viewPath = resource_path("views/{$names['plural']}");
|
|
|
|
if (!$this->files->isDirectory($viewPath)) {
|
|
$this->files->makeDirectory($viewPath, 0755, true);
|
|
}
|
|
|
|
$views = ['index', 'show', 'create', 'edit'];
|
|
|
|
foreach ($views as $view) {
|
|
$path = "{$viewPath}/{$view}.blade.php";
|
|
|
|
if ($this->files->exists($path) && !$this->option('force')) {
|
|
$this->warn("⚠️ View already exists: {$path}");
|
|
continue;
|
|
}
|
|
|
|
$stub = $this->getViewStub($view);
|
|
$content = $this->replaceStubVariables($stub, $names);
|
|
|
|
$this->files->put($path, $content);
|
|
$this->info("✅ View created: resources/views/{$names['plural']}/{$view}.blade.php");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate routes
|
|
*/
|
|
protected function generateRoutes(array $names): void
|
|
{
|
|
$webRoutesPath = base_path('routes/web.php');
|
|
$routeDefinition = $this->getRouteDefinition($names);
|
|
|
|
// Check if route already exists
|
|
$webRoutes = $this->files->get($webRoutesPath);
|
|
if (strpos($webRoutes, "Route::resource('{$names['plural']}'") !== false) {
|
|
$this->warn("⚠️ Routes may already exist for {$names['plural']}");
|
|
return;
|
|
}
|
|
|
|
// Append route to web.php
|
|
$this->files->append($webRoutesPath, "\n" . $routeDefinition);
|
|
$this->info("✅ Routes added to routes/web.php");
|
|
}
|
|
|
|
/**
|
|
* Generate migration
|
|
*/
|
|
protected function generateMigration(array $names): void
|
|
{
|
|
$migrationName = "create_{$names['table']}_table";
|
|
$timestamp = date('Y_m_d_His');
|
|
$path = database_path("migrations/{$timestamp}_{$migrationName}.php");
|
|
|
|
$stub = $this->getMigrationStub();
|
|
$content = $this->replaceStubVariables($stub, $names);
|
|
|
|
$this->files->put($path, $content);
|
|
$this->info("✅ Migration created: database/migrations/{$timestamp}_{$migrationName}.php");
|
|
}
|
|
|
|
/**
|
|
* Generate API resources
|
|
*/
|
|
protected function generateApiResources(array $names): void
|
|
{
|
|
// API Controller
|
|
$apiPath = app_path("Http/Controllers/Api/{$names['controller']}.php");
|
|
|
|
if (!$this->files->isDirectory(dirname($apiPath))) {
|
|
$this->files->makeDirectory(dirname($apiPath), 0755, true);
|
|
}
|
|
|
|
$stub = $this->getApiControllerStub();
|
|
$content = $this->replaceStubVariables($stub, $names);
|
|
|
|
$this->files->put($apiPath, $content);
|
|
$this->info("✅ API Controller created: app/Http/Controllers/Api/{$names['controller']}.php");
|
|
|
|
// API Resource
|
|
$resourceName = $names['studly'] . 'Resource';
|
|
$resourcePath = app_path("Http/Resources/{$resourceName}.php");
|
|
|
|
if (!$this->files->isDirectory(dirname($resourcePath))) {
|
|
$this->files->makeDirectory(dirname($resourcePath), 0755, true);
|
|
}
|
|
|
|
$stub = $this->getApiResourceStub();
|
|
$content = $this->replaceStubVariables($stub, array_merge($names, ['resourceName' => $resourceName]));
|
|
|
|
$this->files->put($resourcePath, $content);
|
|
$this->info("✅ API Resource created: app/Http/Resources/{$resourceName}.php");
|
|
|
|
// API Routes
|
|
$apiRoutesPath = base_path('routes/api.php');
|
|
$apiRouteDefinition = $this->getApiRouteDefinition($names);
|
|
|
|
$this->files->append($apiRoutesPath, "\n" . $apiRouteDefinition);
|
|
$this->info("✅ API Routes added to routes/api.php");
|
|
}
|
|
|
|
/**
|
|
* Generate test files
|
|
*/
|
|
protected function generateTests(array $names): void
|
|
{
|
|
$testName = $names['controller'] . 'Test';
|
|
$testPath = base_path("tests/Feature/{$testName}.php");
|
|
|
|
if ($this->files->exists($testPath) && !$this->option('force')) {
|
|
$this->warn("⚠️ Test already exists: {$testPath}");
|
|
return;
|
|
}
|
|
|
|
$stub = $this->getTestStub();
|
|
$content = $this->replaceStubVariables($stub, array_merge($names, ['testName' => $testName]));
|
|
|
|
$this->files->put($testPath, $content);
|
|
$this->info("✅ Test created: tests/Feature/{$testName}.php");
|
|
}
|
|
|
|
/**
|
|
* Get model stub content
|
|
*/
|
|
protected function getModelStub(): string
|
|
{
|
|
return '<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class {{ studly }} extends Model
|
|
{
|
|
use HasFactory, SoftDeletes;
|
|
|
|
/**
|
|
* The table associated with the model.
|
|
*/
|
|
protected $table = \'{{ table }}\';
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*/
|
|
protected $fillable = [
|
|
\'name\',
|
|
\'description\',
|
|
\'is_active\',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast.
|
|
*/
|
|
protected $casts = [
|
|
\'is_active\' => \'boolean\',
|
|
\'created_at\' => \'datetime\',
|
|
\'updated_at\' => \'datetime\',
|
|
\'deleted_at\' => \'datetime\',
|
|
];
|
|
|
|
/**
|
|
* Scope for active records
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where(\'is_active\', true);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for this model
|
|
*/
|
|
public function getCacheKey(string $suffix = \'\'): string
|
|
{
|
|
return \'thrillwiki.{{ snake }}.\'.$this->id.($suffix ? \'.\'.$suffix : \'\');
|
|
}
|
|
}';
|
|
}
|
|
|
|
/**
|
|
* Get controller stub content
|
|
*/
|
|
protected function getControllerStub(): string
|
|
{
|
|
return '<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\{{ studly }};
|
|
use App\Http\Requests\{{ studly }}Request;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
|
|
class {{ controller }} extends Controller
|
|
{
|
|
/**
|
|
* Display a listing of the resource.
|
|
*/
|
|
public function index(Request $request): View
|
|
{
|
|
$query = {{ studly }}::query();
|
|
|
|
// Search functionality
|
|
if ($request->filled(\'search\')) {
|
|
$search = $request->get(\'search\');
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where(\'name\', \'ILIKE\', "%{$search}%")
|
|
->orWhere(\'description\', \'ILIKE\', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Filter by status
|
|
if ($request->filled(\'status\')) {
|
|
$query->where(\'is_active\', $request->get(\'status\') === \'active\');
|
|
}
|
|
|
|
${{ pluralCamel }} = $query->latest()->paginate(15)->withQueryString();
|
|
|
|
return view(\'{{ plural }}.index\', compact(\'{{ pluralCamel }}\'));
|
|
}
|
|
|
|
/**
|
|
* Show the form for creating a new resource.
|
|
*/
|
|
public function create(): View
|
|
{
|
|
return view(\'{{ plural }}.create\');
|
|
}
|
|
|
|
/**
|
|
* Store a newly created resource in storage.
|
|
*/
|
|
public function store({{ studly }}Request $request): RedirectResponse
|
|
{
|
|
${{ camel }} = {{ studly }}::create($request->validated());
|
|
|
|
return redirect()
|
|
->route(\'{{ plural }}.show\', ${{ camel }})
|
|
->with(\'success\', \'{{ title }} created successfully!\');
|
|
}
|
|
|
|
/**
|
|
* Display the specified resource.
|
|
*/
|
|
public function show({{ studly }} ${{ camel }}): View
|
|
{
|
|
return view(\'{{ plural }}.show\', compact(\'{{ camel }}\'));
|
|
}
|
|
|
|
/**
|
|
* Show the form for editing the specified resource.
|
|
*/
|
|
public function edit({{ studly }} ${{ camel }}): View
|
|
{
|
|
return view(\'{{ plural }}.edit\', compact(\'{{ camel }}\'));
|
|
}
|
|
|
|
/**
|
|
* Update the specified resource in storage.
|
|
*/
|
|
public function update({{ studly }}Request $request, {{ studly }} ${{ camel }}): RedirectResponse
|
|
{
|
|
${{ camel }}->update($request->validated());
|
|
|
|
return redirect()
|
|
->route(\'{{ plural }}.show\', ${{ camel }})
|
|
->with(\'success\', \'{{ title }} updated successfully!\');
|
|
}
|
|
|
|
/**
|
|
* Remove the specified resource from storage.
|
|
*/
|
|
public function destroy({{ studly }} ${{ camel }}): RedirectResponse
|
|
{
|
|
${{ camel }}->delete();
|
|
|
|
return redirect()
|
|
->route(\'{{ plural }}.index\')
|
|
->with(\'success\', \'{{ title }} deleted successfully!\');
|
|
}
|
|
}';
|
|
}
|
|
|
|
/**
|
|
* Get form request stub content
|
|
*/
|
|
protected function getFormRequestStub(): string
|
|
{
|
|
return '<?php
|
|
|
|
namespace App\Http\Requests;
|
|
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
|
|
class {{ requestName }} extends FormRequest
|
|
{
|
|
/**
|
|
* Determine if the user is authorized to make this request.
|
|
*/
|
|
public function authorize(): bool
|
|
{
|
|
return true; // Add authorization logic as needed
|
|
}
|
|
|
|
/**
|
|
* Get the validation rules that apply to the request.
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
$rules = [
|
|
\'name\' => [\'required\', \'string\', \'max:255\'],
|
|
\'description\' => [\'nullable\', \'string\'],
|
|
\'is_active\' => [\'boolean\'],
|
|
];
|
|
|
|
// For updates, make name unique except for current record
|
|
if ($this->route(\'{{ camel }}\')) {
|
|
$rules[\'name\'][] = \'unique:{{ table }},name,\' . $this->route(\'{{ camel }}\')->id;
|
|
} else {
|
|
$rules[\'name\'][] = \'unique:{{ table }},name\';
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* Get custom messages for validator errors.
|
|
*/
|
|
public function messages(): array
|
|
{
|
|
return [
|
|
\'name.required\' => \'The {{ snake }} name is required.\',
|
|
\'name.unique\' => \'A {{ snake }} with this name already exists.\',
|
|
];
|
|
}
|
|
}';
|
|
}
|
|
|
|
/**
|
|
* Get view stub content
|
|
*/
|
|
protected function getViewStub(string $view): string
|
|
{
|
|
$stubs = [
|
|
'index' => '@extends(\'layouts.app\')
|
|
|
|
@section(\'content\')
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ pluralTitle }}</h1>
|
|
<a href="{{ route(\'{{ plural }}.create\') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
|
Add New {{ title }}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Search and Filters -->
|
|
<form method="GET" class="mb-6">
|
|
<div class="flex gap-4">
|
|
<input type="text" name="search" value="{{ request(\'search\') }}"
|
|
placeholder="Search {{ plural }}..."
|
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
<select name="status" class="px-4 py-2 border border-gray-300 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
<option value="">All Status</option>
|
|
<option value="active" {{ request(\'status\') === \'active\' ? \'selected\' : \'\' }}>Active</option>
|
|
<option value="inactive" {{ request(\'status\') === \'inactive\' ? \'selected\' : \'\' }}>Inactive</option>
|
|
</select>
|
|
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
|
|
Search
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Results -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
@forelse(${{ pluralCamel }} as ${{ camel }})
|
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
<a href="{{ route(\'{{ plural }}.show\', ${{ camel }}) }}" class="hover:text-blue-600">
|
|
{{ ${{ camel }}->name }}
|
|
</a>
|
|
</h3>
|
|
@if(${{ camel }}->description)
|
|
<p class="text-gray-600 dark:text-gray-400 mt-2">{{ ${{ camel }}->description }}</p>
|
|
@endif
|
|
<div class="flex items-center mt-2 space-x-4">
|
|
<span class="text-sm text-gray-500">
|
|
{{ ${{ camel }}->created_at->format(\'M j, Y\') }}
|
|
</span>
|
|
<span class="px-2 py-1 text-xs rounded-full {{ ${{ camel }}->is_active ? \'bg-green-100 text-green-800\' : \'bg-red-100 text-red-800\' }}">
|
|
{{ ${{ camel }}->is_active ? \'Active\' : \'Inactive\' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<a href="{{ route(\'{{ plural }}.edit\', ${{ camel }}) }}" class="text-blue-600 hover:text-blue-800">Edit</a>
|
|
<form action="{{ route(\'{{ plural }}.destroy\', ${{ camel }}) }}" method="POST" class="inline">
|
|
@csrf
|
|
@method(\'DELETE\')
|
|
<button type="submit" class="text-red-600 hover:text-red-800"
|
|
onclick="return confirm(\'Are you sure?\')">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="p-6 text-center text-gray-500 dark:text-gray-400">
|
|
No {{ plural }} found.
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
@if(${{ pluralCamel }}->hasPages())
|
|
<div class="mt-6">
|
|
{{ ${{ pluralCamel }}->links() }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endsection',
|
|
|
|
'show' => '@extends(\'layouts.app\')
|
|
|
|
@section(\'content\')
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ ${{ camel }}->name }}</h1>
|
|
<div class="flex space-x-2">
|
|
<a href="{{ route(\'{{ plural }}.edit\', ${{ camel }}) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
|
Edit
|
|
</a>
|
|
<a href="{{ route(\'{{ plural }}.index\') }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
|
|
Back to List
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Details</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
|
|
<p class="text-gray-900 dark:text-white">{{ ${{ camel }}->name }}</p>
|
|
</div>
|
|
|
|
@if(${{ camel }}->description)
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
|
<p class="text-gray-900 dark:text-white">{{ ${{ camel }}->description }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Status</label>
|
|
<span class="px-2 py-1 text-xs rounded-full {{ ${{ camel }}->is_active ? \'bg-green-100 text-green-800\' : \'bg-red-100 text-red-800\' }}">
|
|
{{ ${{ camel }}->is_active ? \'Active\' : \'Inactive\' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Metadata</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Created</label>
|
|
<p class="text-gray-900 dark:text-white">{{ ${{ camel }}->created_at->format(\'F j, Y \at g:i A\') }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Last Updated</label>
|
|
<p class="text-gray-900 dark:text-white">{{ ${{ camel }}->updated_at->format(\'F j, Y \at g:i A\') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-2 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<form action="{{ route(\'{{ plural }}.destroy\', ${{ camel }}) }}" method="POST" class="inline">
|
|
@csrf
|
|
@method(\'DELETE\')
|
|
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg"
|
|
onclick="return confirm(\'Are you sure you want to delete this {{ snake }}?\')">
|
|
Delete {{ title }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection',
|
|
|
|
'create' => '@extends(\'layouts.app\')
|
|
|
|
@section(\'content\')
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Create New {{ title }}</h1>
|
|
<a href="{{ route(\'{{ plural }}.index\') }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
|
|
Back to List
|
|
</a>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<form action="{{ route(\'{{ plural }}.store\') }}" method="POST">
|
|
@csrf
|
|
|
|
<div class="space-y-6">
|
|
<div>
|
|
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Name *
|
|
</label>
|
|
<input type="text" id="name" name="name" value="{{ old(\'name\') }}" required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
@error(\'name\')
|
|
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Description
|
|
</label>
|
|
<textarea id="description" name="description" rows="4"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">{{ old(\'description\') }}</textarea>
|
|
@error(\'description\')
|
|
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="flex items-center">
|
|
<input type="checkbox" name="is_active" value="1" {{ old(\'is_active\', true) ? \'checked\' : \'\' }}
|
|
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
|
|
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Active</span>
|
|
</label>
|
|
@error(\'is_active\')
|
|
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-2 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<a href="{{ route(\'{{ plural }}.index\') }}" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg">
|
|
Cancel
|
|
</a>
|
|
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
|
Create {{ title }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endsection',
|
|
|
|
'edit' => '@extends(\'layouts.app\')
|
|
|
|
@section(\'content\')
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Edit {{ title }}</h1>
|
|
<div class="flex space-x-2">
|
|
<a href="{{ route(\'{{ plural }}.show\', ${{ camel }}) }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
|
|
View
|
|
</a>
|
|
<a href="{{ route(\'{{ plural }}.index\') }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
|
|
Back to List
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
<form action="{{ route(\'{{ plural }}.update\', ${{ camel }}) }}" method="POST">
|
|
@csrf
|
|
@method(\'PUT\')
|
|
|
|
<div class="space-y-6">
|
|
<div>
|
|
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Name *
|
|
</label>
|
|
<input type="text" id="name" name="name" value="{{ old(\'name\', ${{ camel }}->name) }}" required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
|
@error(\'name\')
|
|
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Description
|
|
</label>
|
|
<textarea id="description" name="description" rows="4"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">{{ old(\'description\', ${{ camel }}->description) }}</textarea>
|
|
@error(\'description\')
|
|
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="flex items-center">
|
|
<input type="checkbox" name="is_active" value="1" {{ old(\'is_active\', ${{ camel }}->is_active) ? \'checked\' : \'\' }}
|
|
class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500">
|
|
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Active</span>
|
|
</label>
|
|
@error(\'is_active\')
|
|
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-2 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<a href="{{ route(\'{{ plural }}.show\', ${{ camel }}) }}" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg">
|
|
Cancel
|
|
</a>
|
|
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
|
Update {{ title }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endsection',
|
|
];
|
|
|
|
return $stubs[$view] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Get migration stub content
|
|
*/
|
|
protected function getMigrationStub(): string
|
|
{
|
|
return '<?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 }}\', function (Blueprint $table) {
|
|
$table->id();
|
|
$table->string(\'name\');
|
|
$table->text(\'description\')->nullable();
|
|
$table->boolean(\'is_active\')->default(true);
|
|
$table->timestamps();
|
|
$table->softDeletes();
|
|
|
|
$table->index([\'name\']);
|
|
$table->index([\'is_active\']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reverse the migrations.
|
|
*/
|
|
public function down(): void
|
|
{
|
|
Schema::dropIfExists(\'{{ table }}\');
|
|
}
|
|
};';
|
|
}
|
|
|
|
/**
|
|
* Get API controller stub content
|
|
*/
|
|
protected function getApiControllerStub(): string
|
|
{
|
|
return '<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\{{ studly }};
|
|
use App\Http\Requests\{{ studly }}Request;
|
|
use App\Http\Resources\{{ studly }}Resource;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
class {{ controller }} extends Controller
|
|
{
|
|
/**
|
|
* Display a listing of the resource.
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$query = {{ studly }}::query();
|
|
|
|
// Search functionality
|
|
if ($request->filled(\'search\')) {
|
|
$search = $request->get(\'search\');
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where(\'name\', \'ILIKE\', "%{$search}%")
|
|
->orWhere(\'description\', \'ILIKE\', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Filter by status
|
|
if ($request->filled(\'status\')) {
|
|
$query->where(\'is_active\', $request->get(\'status\') === \'active\');
|
|
}
|
|
|
|
${{ pluralCamel }} = $query->latest()->paginate(15);
|
|
|
|
return response()->json([
|
|
\'data\' => {{ studly }}Resource::collection(${{ pluralCamel }}),
|
|
\'meta\' => [
|
|
\'current_page\' => ${{ pluralCamel }}->currentPage(),
|
|
\'last_page\' => ${{ pluralCamel }}->lastPage(),
|
|
\'per_page\' => ${{ pluralCamel }}->perPage(),
|
|
\'total\' => ${{ pluralCamel }}->total(),
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Store a newly created resource in storage.
|
|
*/
|
|
public function store({{ studly }}Request $request): JsonResponse
|
|
{
|
|
${{ camel }} = {{ studly }}::create($request->validated());
|
|
|
|
return response()->json([
|
|
\'message\' => \'{{ title }} created successfully\',
|
|
\'data\' => new {{ studly }}Resource(${{ camel }})
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* Display the specified resource.
|
|
*/
|
|
public function show({{ studly }} ${{ camel }}): JsonResponse
|
|
{
|
|
return response()->json([
|
|
\'data\' => new {{ studly }}Resource(${{ camel }})
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update the specified resource in storage.
|
|
*/
|
|
public function update({{ studly }}Request $request, {{ studly }} ${{ camel }}): JsonResponse
|
|
{
|
|
${{ camel }}->update($request->validated());
|
|
|
|
return response()->json([
|
|
\'message\' => \'{{ title }} updated successfully\',
|
|
\'data\' => new {{ studly }}Resource(${{ camel }})
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Remove the specified resource from storage.
|
|
*/
|
|
public function destroy({{ studly }} ${{ camel }}): JsonResponse
|
|
{
|
|
${{ camel }}->delete();
|
|
|
|
return response()->json([
|
|
\'message\' => \'{{ title }} deleted successfully\'
|
|
]);
|
|
}
|
|
}';
|
|
}
|
|
|
|
/**
|
|
* Get API resource stub content
|
|
*/
|
|
protected function getApiResourceStub(): string
|
|
{
|
|
return '<?php
|
|
|
|
namespace App\Http\Resources;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\JsonResource;
|
|
|
|
class {{ resourceName }} extends JsonResource
|
|
{
|
|
/**
|
|
* Transform the resource into an array.
|
|
*/
|
|
public function toArray(Request $request): array
|
|
{
|
|
return [
|
|
\'id\' => $this->id,
|
|
\'name\' => $this->name,
|
|
\'description\' => $this->description,
|
|
\'is_active\' => $this->is_active,
|
|
\'created_at\' => $this->created_at,
|
|
\'updated_at\' => $this->updated_at,
|
|
];
|
|
}
|
|
}';
|
|
}
|
|
|
|
/**
|
|
* Get test stub content
|
|
*/
|
|
protected function getTestStub(): string
|
|
{
|
|
return '<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\{{ studly }};
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class {{ testName }} extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $user;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->user = User::factory()->create();
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_display_{{ plural }}_index(): void
|
|
{
|
|
{{ studly }}::factory()->count(3)->create();
|
|
|
|
$response = $this->actingAs($this->user)->get(route(\'{{ plural }}.index\'));
|
|
|
|
$response->assertStatus(200)
|
|
->assertSee(\'{{ pluralTitle }}\');
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_create_a_{{ snake }}(): void
|
|
{
|
|
${{ camel }}Data = [
|
|
\'name\' => \'Test {{ title }}\',
|
|
\'description\' => \'Test description\',
|
|
\'is_active\' => true,
|
|
];
|
|
|
|
$response = $this->actingAs($this->user)->post(route(\'{{ plural }}.store\'), ${{ camel }}Data);
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas(\'{{ table }}\', ${{ camel }}Data);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_show_a_{{ snake }}(): void
|
|
{
|
|
${{ camel }} = {{ studly }}::factory()->create();
|
|
|
|
$response = $this->actingAs($this->user)->get(route(\'{{ plural }}.show\', ${{ camel }}));
|
|
|
|
$response->assertStatus(200)
|
|
->assertSee(${{ camel }}->name);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_update_a_{{ snake }}(): void
|
|
{
|
|
${{ camel }} = {{ studly }}::factory()->create();
|
|
$updateData = [
|
|
\'name\' => \'Updated {{ title }}\',
|
|
\'description\' => \'Updated description\',
|
|
\'is_active\' => false,
|
|
];
|
|
|
|
$response = $this->actingAs($this->user)->put(route(\'{{ plural }}.update\', ${{ camel }}), $updateData);
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas(\'{{ table }}\', $updateData);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_delete_a_{{ snake }}(): void
|
|
{
|
|
${{ camel }} = {{ studly }}::factory()->create();
|
|
|
|
$response = $this->actingAs($this->user)->delete(route(\'{{ plural }}.destroy\', ${{ camel }}));
|
|
|
|
$response->assertRedirect();
|
|
$this->assertSoftDeleted(\'{{ table }}\', [\'id\' => ${{ camel }}->id]);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_validates_required_fields(): void
|
|
{
|
|
$response = $this->actingAs($this->user)->post(route(\'{{ plural }}.store\'), []);
|
|
|
|
$response->assertSessionHasErrors([\'name\']);
|
|
}
|
|
|
|
/** @test */
|
|
public function it_can_search_{{ plural }}(): void
|
|
{
|
|
${{ camel }}1 = {{ studly }}::factory()->create([\'name\' => \'Searchable {{ title }}\']);
|
|
${{ camel }}2 = {{ studly }}::factory()->create([\'name\' => \'Other {{ title }}\']);
|
|
|
|
$response = $this->actingAs($this->user)->get(route(\'{{ plural }}.index\', [\'search\' => \'Searchable\']));
|
|
|
|
$response->assertStatus(200)
|
|
->assertSee(${{ camel }}1->name)
|
|
->assertDontSee(${{ camel }}2->name);
|
|
}
|
|
}';
|
|
}
|
|
|
|
/**
|
|
* Replace stub variables with actual values
|
|
*/
|
|
protected function replaceStubVariables(string $stub, array $names): string
|
|
{
|
|
foreach ($names as $key => $value) {
|
|
$stub = str_replace("{{ {$key} }}", $value, $stub);
|
|
}
|
|
|
|
return $stub;
|
|
}
|
|
|
|
/**
|
|
* Get route definition
|
|
*/
|
|
protected function getRouteDefinition(array $names): string
|
|
{
|
|
return "// {$names['pluralTitle']} CRUD routes
|
|
Route::resource('{$names['plural']}', App\Http\Controllers\\{$names['controller']}::class);";
|
|
}
|
|
|
|
/**
|
|
* Get API route definition
|
|
*/
|
|
protected function getApiRouteDefinition(array $names): string
|
|
{
|
|
return "// {$names['pluralTitle']} API routes
|
|
Route::apiResource('{$names['plural']}', App\Http\Controllers\Api\\{$names['controller']}::class);";
|
|
}
|
|
|
|
/**
|
|
* Display success message
|
|
*/
|
|
protected function displaySuccessMessage(array $names): void
|
|
{
|
|
$this->info('');
|
|
$this->info('🎉 ThrillWiki CRUD \'' . $names['studly'] . '\' created successfully!');
|
|
$this->info('');
|
|
$this->info('📁 Files Generated:');
|
|
$this->info(' • app/Models/' . $names['model'] . '.php');
|
|
$this->info(' • app/Http/Controllers/' . $names['controller'] . '.php');
|
|
$this->info(' • app/Http/Requests/' . $names['studly'] . 'Request.php');
|
|
$this->info(' • resources/views/' . $names['plural'] . '/ (index, show, create, edit)');
|
|
$this->info(' • routes/web.php (resource routes added)');
|
|
|
|
if ($this->option('migration')) {
|
|
$this->info(' • database/migrations/[timestamp]_create_' . $names['table'] . '_table.php');
|
|
}
|
|
|
|
if ($this->option('api')) {
|
|
$this->info(' • app/Http/Controllers/Api/' . $names['controller'] . '.php');
|
|
$this->info(' • app/Http/Resources/' . $names['studly'] . 'Resource.php');
|
|
$this->info(' • routes/api.php (API routes added)');
|
|
}
|
|
|
|
if ($this->option('with-tests')) {
|
|
$this->info(' • tests/Feature/' . $names['controller'] . 'Test.php');
|
|
}
|
|
|
|
$this->info('');
|
|
$this->info('🚀 Next Steps:');
|
|
$this->info(' 1. Run migration: php artisan migrate');
|
|
$this->info(' 2. Create factory: php artisan make:factory ' . $names['studly'] . 'Factory');
|
|
$this->info(' 3. Update navigation to include new routes');
|
|
$this->info(' 4. Customize fields and validation as needed');
|
|
}
|
|
} |