phases 8
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'namespace' => 'Modules',
|
||||
'modules_path' => base_path('Modules'),
|
||||
'themes_path' => resource_path('themes'),
|
||||
'overwrite' => false,
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class InspectModulesCommand extends Command
|
||||
{
|
||||
protected $signature = 'module:inspect {name?} {--json}';
|
||||
protected $description = 'Validate module manifests and display dependencies and providers.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$files = glob(base_path('Modules/*/module.json')) ?: [];
|
||||
$name = $this->argument('name');
|
||||
$rows = [];
|
||||
foreach ($files as $file) {
|
||||
$manifest = json_decode((string) file_get_contents($file), true);
|
||||
if (! is_array($manifest)) { $this->error("Invalid JSON: {$file}"); return self::FAILURE; }
|
||||
if ($name && strcasecmp((string) ($manifest['name'] ?? ''), (string) $name) !== 0) { continue; }
|
||||
$rows[] = [
|
||||
'name' => $manifest['name'] ?? basename(dirname($file)),
|
||||
'version' => $manifest['version'] ?? 'unknown',
|
||||
'requires' => implode(', ', $manifest['requires'] ?? []),
|
||||
'providers' => count($manifest['providers'] ?? []),
|
||||
];
|
||||
}
|
||||
if ($this->option('json')) { $this->line(json_encode($rows, JSON_PRETTY_PRINT)); return self::SUCCESS; }
|
||||
$this->table(['Module', 'Version', 'Requires', 'Providers'], $rows);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Developer\Support\CrudGenerator;
|
||||
use Throwable;
|
||||
|
||||
final class MakeCrudCommand extends Command
|
||||
{
|
||||
protected $signature = 'crud:make {model} {--module=} {--force}';
|
||||
protected $description = 'Generate model, migration, request, controller, views and test inside a module.';
|
||||
|
||||
public function handle(CrudGenerator $generator): int
|
||||
{
|
||||
$module = $this->option('module') ?: $this->choice('Select the target module', $this->modules());
|
||||
try { $files = $generator->generate((string) $module, $this->argument('model'), (bool) $this->option('force')); }
|
||||
catch (Throwable $e) { $this->error($e->getMessage()); return self::FAILURE; }
|
||||
foreach ($files as $file) { $this->line("<info>created</info> {$file}"); }
|
||||
$this->warn('Register the generated controller routes in the target module Routes/web.php file.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function modules(): array
|
||||
{
|
||||
return collect(glob(base_path('Modules/*/module.json')) ?: [])->map(fn (string $file) => basename(dirname($file)))->sort()->values()->all();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Developer\Support\ModuleGenerator;
|
||||
use Throwable;
|
||||
|
||||
final class MakeModuleCommand extends Command
|
||||
{
|
||||
protected $signature = 'module:make {name} {--force}';
|
||||
protected $description = 'Generate a complete modular Laravel module scaffold.';
|
||||
|
||||
public function handle(ModuleGenerator $generator): int
|
||||
{
|
||||
try { $files = $generator->generate($this->argument('name'), (bool) $this->option('force')); }
|
||||
catch (Throwable $e) { $this->error($e->getMessage()); return self::FAILURE; }
|
||||
foreach ($files as $file) { $this->line("<info>created</info> {$file}"); }
|
||||
$this->newLine(); $this->info('Enable the module in modules_statuses.json, then run composer dump-autoload.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Developer\Support\ThemeGenerator;
|
||||
use Throwable;
|
||||
|
||||
final class MakeThemeCommand extends Command
|
||||
{
|
||||
protected $signature = 'theme:make {name} {--force}';
|
||||
protected $description = 'Generate a theme scaffold with metadata, layout and assets.';
|
||||
|
||||
public function handle(ThemeGenerator $generator): int
|
||||
{
|
||||
try { $files = $generator->generate($this->argument('name'), (bool) $this->option('force')); }
|
||||
catch (Throwable $e) { $this->error($e->getMessage()); return self::FAILURE; }
|
||||
foreach ($files as $file) { $this->line("<info>created</info> {$file}"); }
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Http\Controllers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
|
||||
final class DeveloperController
|
||||
{
|
||||
public function __invoke(): View
|
||||
{
|
||||
$modules = collect(glob(base_path('Modules/*/module.json')) ?: [])
|
||||
->map(function (string $file): array {
|
||||
$manifest = json_decode((string) file_get_contents($file), true) ?: [];
|
||||
|
||||
return [
|
||||
'name' => $manifest['name'] ?? basename(dirname($file)),
|
||||
'version' => $manifest['version'] ?? 'unknown',
|
||||
'description' => $manifest['description'] ?? '',
|
||||
'requires' => $manifest['requires'] ?? [],
|
||||
];
|
||||
})
|
||||
->sortBy('name')
|
||||
->values();
|
||||
|
||||
return view('developer::admin.index', compact('modules'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Developer\Console\Commands\InspectModulesCommand;
|
||||
use Modules\Developer\Console\Commands\MakeCrudCommand;
|
||||
use Modules\Developer\Console\Commands\MakeModuleCommand;
|
||||
use Modules\Developer\Console\Commands\MakeThemeCommand;
|
||||
|
||||
final class DeveloperServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Developer', 'Config/config.php'), 'developer');
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadViewsFrom(module_path('Developer', 'Resources/views'), 'developer');
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
MakeModuleCommand::class,
|
||||
MakeCrudCommand::class,
|
||||
MakeThemeCommand::class,
|
||||
InspectModulesCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path('Developer', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<x-layouts.app :title="__('Developer Tools')">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">{{ __('Developer Tools') }}</h1>
|
||||
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">{{ __('Generate modules, CRUD resources and themes using the platform conventions.') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-xl border border-zinc-200 p-5 dark:border-zinc-700"><h2 class="font-semibold">{{ __('Module Generator') }}</h2><code class="mt-3 block text-sm">php artisan module:make Products</code></div>
|
||||
<div class="rounded-xl border border-zinc-200 p-5 dark:border-zinc-700"><h2 class="font-semibold">{{ __('CRUD Generator') }}</h2><code class="mt-3 block text-sm">php artisan crud:make Product --module=Products</code></div>
|
||||
<div class="rounded-xl border border-zinc-200 p-5 dark:border-zinc-700"><h2 class="font-semibold">{{ __('Theme Generator') }}</h2><code class="mt-3 block text-sm">php artisan theme:make Corporate</code></div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="border-b border-zinc-200 px-5 py-4 font-semibold dark:border-zinc-700">{{ __('Installed modules') }}</div>
|
||||
<div class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
@foreach ($modules as $module)
|
||||
<div class="flex items-start justify-between gap-4 px-5 py-4">
|
||||
<div><div class="font-medium">{{ $module['name'] }}</div><div class="text-sm text-zinc-600 dark:text-zinc-400">{{ $module['description'] }}</div></div>
|
||||
<div class="text-right text-sm"><div>v{{ $module['version'] }}</div><div class="text-zinc-500">{{ implode(', ', $module['requires']) }}</div></div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Developer\Http\Controllers\DeveloperController;
|
||||
|
||||
Route::middleware(['web', 'auth', 'permission:developer.access'])
|
||||
->prefix('administration/developer')
|
||||
->name('administration.developer.')
|
||||
->group(function (): void {
|
||||
Route::get('/', DeveloperController::class)->name('index');
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Support;
|
||||
|
||||
final class CrudGenerator extends Generator
|
||||
{
|
||||
public function generate(string $module, string $model, bool $force = false): array
|
||||
{
|
||||
$module = $this->studly($module);
|
||||
$model = $this->studly($model);
|
||||
$table = str($model)->plural()->snake()->toString();
|
||||
$route = str($model)->plural()->kebab()->toString();
|
||||
$root = rtrim(config('developer.modules_path', base_path('Modules')), '/').'/'.$module;
|
||||
$timestamp = now()->format('Y_m_d_His');
|
||||
|
||||
$files = [
|
||||
"$root/Models/{$model}.php" => $this->model($module, $model),
|
||||
"$root/Http/Controllers/{$model}Controller.php" => $this->controller($module, $model),
|
||||
"$root/Http/Requests/Store{$model}Request.php" => $this->request($module, $model),
|
||||
"$root/Database/Migrations/{$timestamp}_create_{$table}_table.php" => $this->migration($table),
|
||||
"$root/Resources/views/admin/{$route}/index.blade.php" => $this->indexView($model, $route),
|
||||
"$root/Resources/views/admin/{$route}/form.blade.php" => $this->formView($model),
|
||||
"$root/Tests/Feature/{$model}CrudTest.php" => $this->crudTest($module, $model),
|
||||
];
|
||||
|
||||
foreach ($files as $path => $contents) {
|
||||
$this->write($path, $contents, $force);
|
||||
}
|
||||
|
||||
return array_keys($files);
|
||||
}
|
||||
|
||||
private function model(string $module, string $model): string
|
||||
{
|
||||
return "<?php\n\nnamespace Modules\\{$module}\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nfinal class {$model} extends Model\n{\n protected \$guarded = [];\n\n protected function casts(): array\n {\n return ['is_active' => 'boolean'];\n }\n}\n";
|
||||
}
|
||||
|
||||
private function controller(string $module, string $model): string
|
||||
{
|
||||
$plural = str($model)->plural()->kebab();
|
||||
return "<?php\n\nnamespace Modules\\{$module}\\Http\\Controllers;\n\nuse Illuminate\\Http\\RedirectResponse;\nuse Illuminate\\View\\View;\nuse Modules\\{$module}\\Http\\Requests\\Store{$model}Request;\nuse Modules\\{$module}\\Models\\{$model};\n\nfinal class {$model}Controller\n{\n public function index(): View\n {\n return view('".str($module)->snake()."::admin.{$plural}.index', ['records' => {$model}::query()->latest()->paginate()]);\n }\n\n public function store(Store{$model}Request \$request): RedirectResponse\n {\n {$model}::query()->create(\$request->validated());\n return back()->with('status', '{$model} created.');\n }\n}\n";
|
||||
}
|
||||
|
||||
private function request(string $module, string $model): string
|
||||
{
|
||||
return "<?php\n\nnamespace Modules\\{$module}\\Http\\Requests;\n\nuse Illuminate\\Foundation\\Http\\FormRequest;\n\nfinal class Store{$model}Request extends FormRequest\n{\n public function authorize(): bool { return \$this->user() !== null; }\n public function rules(): array { return ['name' => ['required', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean']]; }\n}\n";
|
||||
}
|
||||
|
||||
private function migration(string $table): string
|
||||
{
|
||||
return "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nreturn new class extends Migration {\n public function up(): void { Schema::create('{$table}', function (Blueprint \$table): void { \$table->id(); \$table->string('name'); \$table->boolean('is_active')->default(true); \$table->timestamps(); }); }\n public function down(): void { Schema::dropIfExists('{$table}'); }\n};\n";
|
||||
}
|
||||
|
||||
private function indexView(string $model, string $route): string
|
||||
{
|
||||
return "<x-layouts.app :title=\"__('{$model}')\">\n <div class=\"space-y-6\"><h1 class=\"text-2xl font-semibold\">{$model}</h1>@foreach (\$records as \$record)<div>{{ \$record->name }}</div>@endforeach{{ \$records->links() }}</div>\n</x-layouts.app>\n";
|
||||
}
|
||||
|
||||
private function formView(string $model): string
|
||||
{
|
||||
return "<form method=\"POST\">@csrf<flux:input name=\"name\" :label=\"__('Name')\" required /><flux:button type=\"submit\" variant=\"primary\">{{ __('Save {$model}') }}</flux:button></form>\n";
|
||||
}
|
||||
|
||||
private function crudTest(string $module, string $model): string
|
||||
{
|
||||
return "<?php\n\nuse Modules\\{$module}\\Models\\{$model};\n\nit('can create a {$model}', function (): void { expect({$model}::query()->create(['name' => 'Example']))->toBeInstanceOf({$model}::class); });\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Support;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use RuntimeException;
|
||||
|
||||
abstract class Generator
|
||||
{
|
||||
public function __construct(protected readonly Filesystem $files) {}
|
||||
|
||||
protected function createDirectory(string $path): void
|
||||
{
|
||||
if (! $this->files->isDirectory($path)) {
|
||||
$this->files->makeDirectory($path, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function write(string $path, string $contents, bool $force = false): void
|
||||
{
|
||||
if ($this->files->exists($path) && ! $force) {
|
||||
throw new RuntimeException("File already exists: {$path}");
|
||||
}
|
||||
|
||||
$this->createDirectory(dirname($path));
|
||||
$this->files->put($path, $contents);
|
||||
}
|
||||
|
||||
protected function studly(string $value): string
|
||||
{
|
||||
return str($value)->studly()->toString();
|
||||
}
|
||||
|
||||
protected function snake(string $value): string
|
||||
{
|
||||
return str($value)->snake()->toString();
|
||||
}
|
||||
|
||||
protected function kebab(string $value): string
|
||||
{
|
||||
return str($value)->kebab()->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Support;
|
||||
|
||||
final class ModuleGenerator extends Generator
|
||||
{
|
||||
public function generate(string $name, bool $force = false): array
|
||||
{
|
||||
$name = $this->studly($name);
|
||||
$alias = $this->kebab($name);
|
||||
$root = rtrim(config('developer.modules_path', base_path('Modules')), '/').'/'.$name;
|
||||
$provider = "Modules\\{$name}\\Providers\\{$name}ServiceProvider";
|
||||
|
||||
$files = [
|
||||
"$root/module.json" => json_encode([
|
||||
'name' => $name,
|
||||
'alias' => $alias,
|
||||
'version' => '1.0.0',
|
||||
'description' => "{$name} module.",
|
||||
'keywords' => [$alias],
|
||||
'priority' => 100,
|
||||
'providers' => [$provider],
|
||||
'aliases' => (object) [],
|
||||
'files' => [],
|
||||
'requires' => ['Core', 'Administration'],
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL,
|
||||
"$root/Config/config.php" => "<?php\n\nreturn ['name' => '{$name}'];\n",
|
||||
"$root/Providers/{$name}ServiceProvider.php" => $this->provider($name),
|
||||
"$root/Providers/RouteServiceProvider.php" => $this->routeProvider($name),
|
||||
"$root/Routes/web.php" => "<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::middleware(['web', 'auth'])->prefix('administration/{$alias}')->name('administration.{$alias}.')->group(function (): void {\n // Register module routes here.\n});\n",
|
||||
"$root/Tests/Feature/{$name}ModuleBootTest.php" => $this->test($name),
|
||||
];
|
||||
|
||||
foreach ($files as $path => $contents) {
|
||||
$this->write($path, $contents, $force);
|
||||
}
|
||||
|
||||
return array_keys($files);
|
||||
}
|
||||
|
||||
private function provider(string $name): string
|
||||
{
|
||||
return "<?php\n\nnamespace Modules\\{$name}\\Providers;\n\nuse Illuminate\\Support\\ServiceProvider;\n\nfinal class {$name}ServiceProvider extends ServiceProvider\n{\n public function register(): void\n {\n \$this->app->register(RouteServiceProvider::class);\n \$this->mergeConfigFrom(module_path('{$name}', 'Config/config.php'), '".str($name)->snake()."');\n }\n\n public function boot(): void\n {\n \$this->loadMigrationsFrom(module_path('{$name}', 'Database/Migrations'));\n \$this->loadViewsFrom(module_path('{$name}', 'Resources/views'), '".str($name)->snake()."');\n }\n}\n";
|
||||
}
|
||||
|
||||
private function routeProvider(string $name): string
|
||||
{
|
||||
return "<?php\n\nnamespace Modules\\{$name}\\Providers;\n\nuse Illuminate\\Foundation\\Support\\Providers\\RouteServiceProvider as ServiceProvider;\nuse Illuminate\\Support\\Facades\\Route;\n\nfinal class RouteServiceProvider extends ServiceProvider\n{\n public function boot(): void\n {\n Route::middleware('web')->group(module_path('{$name}', 'Routes/web.php'));\n }\n}\n";
|
||||
}
|
||||
|
||||
private function test(string $name): string
|
||||
{
|
||||
return "<?php\n\nit('boots the {$name} module', function (): void {\n expect(app()->providerIsLoaded(Modules\\{$name}\\Providers\\{$name}ServiceProvider::class))->toBeTrue();\n});\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Developer\Support;
|
||||
|
||||
final class ThemeGenerator extends Generator
|
||||
{
|
||||
public function generate(string $name, bool $force = false): array
|
||||
{
|
||||
$name = $this->kebab($name);
|
||||
$root = rtrim(config('developer.themes_path', resource_path('themes')), '/').'/'.$name;
|
||||
$files = [
|
||||
"$root/theme.json" => json_encode(['name' => str($name)->headline()->toString(), 'slug' => $name, 'version' => '1.0.0', 'parent' => 'common'], JSON_PRETTY_PRINT).PHP_EOL,
|
||||
"$root/views/layouts/app.blade.php" => "<!DOCTYPE html><html lang=\"{{ str_replace('_', '-', app()->getLocale()) }}\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>{{ \$title ?? config('app.name') }}</title>@vite(['resources/css/app.css', 'resources/js/app.js'])</head><body>{{ \$slot }}</body></html>\n",
|
||||
"$root/views/home.blade.php" => "<x-layouts.app><main><h1>{{ config('app.name') }}</h1></main></x-layouts.app>\n",
|
||||
"$root/assets/css/theme.css" => ":root { --theme-radius: 0.5rem; }\n",
|
||||
"$root/README.md" => "# ".str($name)->headline()." Theme\n\nGenerated by `php artisan theme:make {$name}`.\n",
|
||||
];
|
||||
foreach ($files as $path => $contents) { $this->write($path, $contents, $force); }
|
||||
return array_keys($files);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Modules\Developer\Providers\DeveloperServiceProvider;
|
||||
|
||||
it('boots the developer module', function (): void {
|
||||
expect(app()->providerIsLoaded(DeveloperServiceProvider::class))->toBeTrue();
|
||||
});
|
||||
|
||||
it('registers developer commands', function (): void {
|
||||
$this->artisan('module:inspect')->assertSuccessful();
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Developer",
|
||||
"alias": "developer",
|
||||
"version": "1.0.0",
|
||||
"description": "Developer generators, module inspection and scaffolding tools.",
|
||||
"keywords": ["developer", "generator", "scaffold"],
|
||||
"priority": 80,
|
||||
"providers": ["Modules\\Developer\\Providers\\DeveloperServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
Reference in New Issue
Block a user