lthn.io/app/Core/Console/Commands/MakeWebsiteCommand.php

606 lines
17 KiB
PHP
Raw Permalink Normal View History

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
/**
* Generate a new Website scaffold.
*
* Creates a domain-isolated website in the Website namespace
* that is loaded based on incoming HTTP host.
*
* Usage: php artisan make:website MarketingSite --domain=marketing.test
*/
class MakeWebsiteCommand extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'make:website
{name : The name of the website (e.g., MarketingSite, Blog)}
{--domain= : Primary domain pattern (e.g., example.test, example.com)}
{--web : Include web routes}
{--admin : Include admin routes}
{--api : Include API routes}
{--all : Include all route types}
{--force : Overwrite existing website}';
/**
* The console command description.
*/
protected $description = 'Create a new domain-isolated website';
/**
* Files created during generation for summary table.
*
* @var array<array{file: string, description: string}>
*/
protected array $createdFiles = [];
/**
* Execute the console command.
*/
public function handle(): int
{
$name = Str::studly($this->argument('name'));
$domain = $this->option('domain') ?: Str::snake($name, '-').'.test';
$websitePath = $this->getWebsitePath($name);
if (File::isDirectory($websitePath) && ! $this->option('force')) {
$this->newLine();
$this->components->error("Website [{$name}] already exists!");
$this->newLine();
$this->components->warn('Use --force to overwrite the existing website.');
$this->newLine();
return self::FAILURE;
}
$this->newLine();
$this->components->info("Creating website: <comment>{$name}</comment>");
$this->components->twoColumnDetail('Domain', "<fg=yellow>{$domain}</>");
$this->newLine();
// Create directory structure
$this->createDirectoryStructure($websitePath);
// Create Boot.php
$this->createBootFile($websitePath, $name, $domain);
// Create optional route files
$this->createOptionalFiles($websitePath, $name);
// Show summary table of created files
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Created Files</>', '<fg=gray>Description</>');
foreach ($this->createdFiles as $file) {
$this->components->twoColumnDetail(
"<fg=cyan>{$file['file']}</>",
"<fg=gray>{$file['description']}</>"
);
}
$this->newLine();
$this->components->info("Website [{$name}] created successfully!");
$this->newLine();
$this->components->twoColumnDetail('Location', "<fg=yellow>{$websitePath}</>");
$this->newLine();
$this->components->info('Next steps:');
$this->line(" <fg=gray>1.</> Configure your local dev server to serve <fg=yellow>{$domain}</>");
$this->line(' <fg=gray>(e.g.,</> valet link '.Str::snake($name, '-').'<fg=gray>)</>');
$this->line(" <fg=gray>2.</> Visit <fg=cyan>http://{$domain}</> to see your website");
$this->line(' <fg=gray>3.</> Add routes, views, and controllers as needed');
$this->newLine();
return self::SUCCESS;
}
/**
* Get the path for the website.
*/
protected function getWebsitePath(string $name): string
{
// Websites go in app/Website for consuming applications
return base_path("app/Website/{$name}");
}
/**
* Create the directory structure for the website.
*/
protected function createDirectoryStructure(string $websitePath): void
{
$directories = [
$websitePath,
"{$websitePath}/View",
"{$websitePath}/View/Blade",
"{$websitePath}/View/Blade/layouts",
];
if ($this->hasRoutes()) {
$directories[] = "{$websitePath}/Routes";
}
foreach ($directories as $directory) {
File::ensureDirectoryExists($directory);
}
$this->components->task('Creating directory structure', fn () => true);
}
/**
* Check if any route handlers are requested.
*/
protected function hasRoutes(): bool
{
return $this->option('web')
|| $this->option('admin')
|| $this->option('api')
|| $this->option('all')
|| ! $this->hasAnyOption();
}
/**
* Check if any specific option was provided.
*/
protected function hasAnyOption(): bool
{
return $this->option('web')
|| $this->option('admin')
|| $this->option('api')
|| $this->option('all');
}
/**
* Create the Boot.php file.
*/
protected function createBootFile(string $websitePath, string $name, string $domain): void
{
$namespace = "Website\\{$name}";
$domainPattern = $this->buildDomainPattern($domain);
$listeners = $this->buildListenersArray();
$handlers = $this->buildHandlerMethods($name);
$content = <<<PHP
<?php
declare(strict_types=1);
namespace {$namespace};
use Core\Events\DomainResolving;
{$this->buildUseStatements()}
use Illuminate\Support\ServiceProvider;
/**
* {$name} Website - Domain-isolated website provider.
*
* This website is loaded when the incoming HTTP host matches
* the domain pattern defined in \$domains.
*/
class Boot extends ServiceProvider
{
/**
* Domain patterns this website responds to.
*
* Uses regex patterns. Common examples:
* - '/^example\\.test\$/' - exact match
* - '/^example\\.(com|test)\$/' - multiple TLDs
* - '/^(www\\.)?example\\.com\$/' - optional www
*
* @var array<string>
*/
public static array \$domains = [
'{$domainPattern}',
];
/**
* Events this module listens to for lazy loading.
*
* @var array<class-string, string>
*/
public static array \$listens = [
DomainResolving::class => 'onDomainResolving',
{$listeners}
];
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
/**
* Handle domain resolution - register if domain matches.
*/
public function onDomainResolving(DomainResolving \$event): void
{
foreach (self::\$domains as \$pattern) {
if (\$event->matches(\$pattern)) {
\$event->register(self::class);
return;
}
}
}
{$handlers}
}
PHP;
File::put("{$websitePath}/Boot.php", $content);
$this->createdFiles[] = ['file' => 'Boot.php', 'description' => 'Domain-isolated website provider'];
$this->components->task('Creating Boot.php', fn () => true);
}
/**
* Build the domain regex pattern.
*/
protected function buildDomainPattern(string $domain): string
{
// Escape dots and create a regex pattern
$escaped = preg_quote($domain, '/');
return '/^'.$escaped.'$/';
}
/**
* Build the use statements for the Boot file.
*/
protected function buildUseStatements(): string
{
$statements = [];
if ($this->option('web') || $this->option('all') || ! $this->hasAnyOption()) {
$statements[] = 'use Core\Events\WebRoutesRegistering;';
}
if ($this->option('admin') || $this->option('all')) {
$statements[] = 'use Core\Events\AdminPanelBooting;';
}
if ($this->option('api') || $this->option('all')) {
$statements[] = 'use Core\Events\ApiRoutesRegistering;';
}
return implode("\n", $statements);
}
/**
* Build the listeners array content (excluding DomainResolving).
*/
protected function buildListenersArray(): string
{
$listeners = [];
if ($this->option('web') || $this->option('all') || ! $this->hasAnyOption()) {
$listeners[] = " WebRoutesRegistering::class => 'onWebRoutes',";
}
if ($this->option('admin') || $this->option('all')) {
$listeners[] = " AdminPanelBooting::class => 'onAdminPanel',";
}
if ($this->option('api') || $this->option('all')) {
$listeners[] = " ApiRoutesRegistering::class => 'onApiRoutes',";
}
return implode("\n", $listeners);
}
/**
* Build the handler methods.
*/
protected function buildHandlerMethods(string $name): string
{
$methods = [];
$websiteName = Str::snake($name);
if ($this->option('web') || $this->option('all') || ! $this->hasAnyOption()) {
$methods[] = <<<PHP
/**
* Register web routes and views.
*/
public function onWebRoutes(WebRoutesRegistering \$event): void
{
\$event->views('{$websiteName}', __DIR__.'/View/Blade');
if (file_exists(__DIR__.'/Routes/web.php')) {
\$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}
PHP;
}
if ($this->option('admin') || $this->option('all')) {
$methods[] = <<<'PHP'
/**
* Register admin panel routes.
*/
public function onAdminPanel(AdminPanelBooting $event): void
{
if (file_exists(__DIR__.'/Routes/admin.php')) {
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
}
}
PHP;
}
if ($this->option('api') || $this->option('all')) {
$methods[] = <<<'PHP'
/**
* Register API routes.
*/
public function onApiRoutes(ApiRoutesRegistering $event): void
{
if (file_exists(__DIR__.'/Routes/api.php')) {
$event->routes(fn () => require __DIR__.'/Routes/api.php');
}
}
PHP;
}
return implode("\n", $methods);
}
/**
* Create optional files based on flags.
*/
protected function createOptionalFiles(string $websitePath, string $name): void
{
$websiteName = Str::snake($name);
if ($this->option('web') || $this->option('all') || ! $this->hasAnyOption()) {
$this->createWebRoutes($websitePath, $websiteName);
$this->createLayout($websitePath, $name);
$this->createHomepage($websitePath, $websiteName);
}
if ($this->option('admin') || $this->option('all')) {
$this->createAdminRoutes($websitePath, $websiteName);
}
if ($this->option('api') || $this->option('all')) {
$this->createApiRoutes($websitePath, $websiteName);
}
}
/**
* Create web routes file.
*/
protected function createWebRoutes(string $websitePath, string $websiteName): void
{
$content = <<<PHP
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| {$websiteName} Web Routes
|--------------------------------------------------------------------------
|
| Public web routes for this website.
|
*/
Route::get('/', function () {
return view('{$websiteName}::home');
})->name('{$websiteName}.home');
PHP;
File::put("{$websitePath}/Routes/web.php", $content);
$this->createdFiles[] = ['file' => 'Routes/web.php', 'description' => 'Public web routes'];
$this->components->task('Creating Routes/web.php', fn () => true);
}
/**
* Create admin routes file.
*/
protected function createAdminRoutes(string $websitePath, string $websiteName): void
{
$content = <<<PHP
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| {$websiteName} Admin Routes
|--------------------------------------------------------------------------
|
| Admin routes for this website.
|
*/
Route::prefix('admin/{$websiteName}')->name('{$websiteName}.admin.')->group(function () {
Route::get('/', function () {
return 'Admin dashboard for {$websiteName}';
})->name('index');
});
PHP;
File::put("{$websitePath}/Routes/admin.php", $content);
$this->createdFiles[] = ['file' => 'Routes/admin.php', 'description' => 'Admin panel routes'];
$this->components->task('Creating Routes/admin.php', fn () => true);
}
/**
* Create API routes file.
*/
protected function createApiRoutes(string $websitePath, string $websiteName): void
{
$content = <<<PHP
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| {$websiteName} API Routes
|--------------------------------------------------------------------------
|
| API routes for this website.
|
*/
Route::prefix('{$websiteName}')->name('api.{$websiteName}.')->group(function () {
Route::get('/health', function () {
return response()->json(['status' => 'ok', 'website' => '{$websiteName}']);
})->name('health');
});
PHP;
File::put("{$websitePath}/Routes/api.php", $content);
$this->createdFiles[] = ['file' => 'Routes/api.php', 'description' => 'REST API routes'];
$this->components->task('Creating Routes/api.php', fn () => true);
}
/**
* Create a base layout file.
*/
protected function createLayout(string $websitePath, string $name): void
{
$content = <<<BLADE
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ \$title ?? '{$name}' }}</title>
<!-- Styles -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
<!-- Navigation -->
<nav class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="/" class="text-xl font-bold text-gray-800">
{$name}
</a>
</div>
</div>
</div>
</nav>
<!-- Page Content -->
<main>
{{ \$slot }}
</main>
</div>
</body>
</html>
BLADE;
File::put("{$websitePath}/View/Blade/layouts/app.blade.php", $content);
$this->createdFiles[] = ['file' => 'View/Blade/layouts/app.blade.php', 'description' => 'Base layout template'];
$this->components->task('Creating View/Blade/layouts/app.blade.php', fn () => true);
}
/**
* Create a homepage view.
*/
protected function createHomepage(string $websitePath, string $websiteName): void
{
$name = Str::studly($websiteName);
$content = <<<BLADE
<x-{$websiteName}::layouts.app>
<x-slot name="title">Welcome - {$name}</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm rounded-lg">
<div class="p-6 text-gray-900">
<h1 class="text-3xl font-bold mb-4">Welcome to {$name}</h1>
<p class="text-gray-600">
This is your new website. Start building something amazing!
</p>
</div>
</div>
</div>
</div>
</x-{$websiteName}::layouts.app>
BLADE;
File::put("{$websitePath}/View/Blade/home.blade.php", $content);
$this->createdFiles[] = ['file' => 'View/Blade/home.blade.php', 'description' => 'Homepage view'];
$this->components->task('Creating View/Blade/home.blade.php', fn () => true);
}
/**
* Get shell completion suggestions for arguments and options.
*/
public function complete(
CompletionInput $input,
CompletionSuggestions $suggestions
): void {
if ($input->mustSuggestArgumentValuesFor('name')) {
// Suggest common website naming patterns
$suggestions->suggestValues([
'MarketingSite',
'Blog',
'Documentation',
'LandingPage',
'Portal',
'Dashboard',
'Support',
]);
}
if ($input->mustSuggestOptionValuesFor('domain')) {
// Suggest common development domains
$suggestions->suggestValues([
'example.test',
'app.test',
'site.test',
'dev.test',
]);
}
}
}