You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

207 lines
8.4 KiB

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class DomainMakeCommand extends Command
{
protected $signature = 'domain:make
{type : Type to generate: model, action, dto, enum, event, listener, notification, policy, query, provider, relationship-provider, export, mapper, scope, mailable}
{domain : Domain name, e.g. Identity, Account, System}
{name : Class name, supports sub-paths e.g. Backup/DeleteBackup}
{--factory : Also generate a factory (model only)}
{--migration : Also generate a migration (model only)}
{--model= : Associate the export with a model}';
protected $description = 'Generate a file directly into the domain structure (app/Domains/)';
/** @var array<string, string> type → subdirectory */
protected array $types = [
'model' => 'Models',
'action' => 'Actions',
'dto' => 'DTOs',
'enum' => 'Enums',
'event' => 'Events',
'listener' => 'Listeners',
'notification' => 'Notifications',
'policy' => 'Policies',
'scope' => 'Scopes',
'trait' => 'Traits',
'query' => 'Queries',
'provider' => 'Providers',
'relationship-provider' => 'Providers',
'view-provider' => 'Providers',
'export' => 'Exports',
// Integration layer — files live under Integration/<subDir>/
'mapper' => 'Integration/Mappers',
'mailable' => 'Mail',
];
public function __construct(protected Filesystem $files)
{
parent::__construct();
}
public function handle(): int
{
$type = strtolower($this->argument('type'));
$domain = ucfirst($this->argument('domain'));
$name = $this->argument('name'); // may contain sub-path, e.g. Backup/DeleteBackup
if (! isset($this->types[$type])) {
$this->components->error("Unknown type [{$type}]. Supported: ".implode(', ', array_keys($this->types)));
return self::FAILURE;
}
$subDir = $this->types[$type];
$className = class_basename(str_replace('/', '\\', $name));
$subPath = str_contains($name, '/') ? dirname($name) : null;
// ── Integration / Mapper special handling ─────────────────────────────
// Automatically append the 'DataMapper' suffix when the developer omits it,
// keeping the class name consistent with the DataPayloadMapper contract.
if ($type === 'mapper' && ! str_ends_with($className, 'DataMapper')) {
$className .= 'DataMapper';
}
// The $subDir for 'mapper' already encodes the full Integration/Mappers
// nested path, so we must not double-nest an additional subPath beneath it.
// Extra sub-path segments are intentionally ignored for the mapper type.
$relativeDir = ($type === 'mapper')
? "Domains/{$domain}/{$subDir}"
: "Domains/{$domain}/{$subDir}".($subPath ? "/{$subPath}" : '');
// ─────────────────────────────────────────────────────────────────────
$namespace = 'App\\'.str_replace('/', '\\', $relativeDir);
$path = app_path("{$relativeDir}/{$className}.php");
if ($this->files->exists($path)) {
$this->components->error("File already exists: [{$path}]");
return self::FAILURE;
}
$this->files->ensureDirectoryExists(dirname($path));
$this->files->put($path, $this->buildStub($type, $namespace, $className, $domain));
$this->components->info("File [{$path}] created successfully.");
if ($type === 'model' && $this->option('factory')) {
$this->createFactory($domain, $className, $namespace);
}
if ($type === 'model' && $this->option('migration')) {
$table = Str::snake(Str::pluralStudly($className));
$this->call('make:migration', ['name' => "create_{$table}_table"]);
}
return self::SUCCESS;
}
// ─── Stubs ────────────────────────────────────────────────────────────────
protected function buildStub(string $type, string $namespace, string $name, string $domain): string
{
$stubPath = app_path("Console/stubs/domain-make/{$type}.stub");
if (! $this->files->exists($stubPath)) {
$this->components->error("Stub file not found: [{$stubPath}]");
return '';
}
$stub = $this->files->get($stubPath);
$replacements = [
'{{ namespace }}' => $namespace,
'{{ class }}' => $name,
];
if ($type === 'model') {
$replacements['{{ factoryImport }}'] = $this->option('factory')
? "\nuse Database\\Factories\\{$domain}\\{$name}Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;"
: '';
$replacements['{{ factoryMethod }}'] = $this->option('factory')
? "\n protected static function newFactory(): Factory\n {\n return {$name}Factory::new();\n }"
: '';
}
if ($type === 'export') {
$modelOption = $this->option('model');
$modelImport = '';
$queryBody = ' // return YourModel::query();';
if ($modelOption) {
$modelData = $this->resolveModel($modelOption, $domain);
$modelImport = "use {$modelData['full']};\n";
$modelClass = $modelData['class'];
$queryBody = <<<PHP
return {$modelClass}::query()
// ->with('profile') // CRITICAL: Eager load any relations used in map()
// ->when(isset(\$this->filters['status']), fn(Builder \$q) => \$q->where('status', \$this->filters['status']))
;
PHP;
}
$replacements['{{ modelImport }}'] = $modelImport;
$replacements['{{ queryBody }}'] = $queryBody;
}
return str_replace(array_keys($replacements), array_values($replacements), $stub);
}
protected function resolveModel(string $modelOption, string $domain): array
{
if (Str::contains($modelOption, ['\\', '/'])) {
if (Str::startsWith(str_replace('/', '\\', $modelOption), 'App\\')) {
$full = str_replace('/', '\\', $modelOption);
$class = class_basename($full);
} else {
$parts = explode('/', str_replace('\\', '/', $modelOption));
$targetDomain = ucfirst($parts[0]);
$targetName = implode('\\', array_map('ucfirst', array_slice($parts, 1)));
$full = "App\\Domains\\{$targetDomain}\\Models\\{$targetName}";
$class = class_basename($full);
}
} else {
$full = "App\\Domains\\{$domain}\\Models\\".ucfirst($modelOption);
$class = ucfirst($modelOption);
}
return [
'full' => $full,
'class' => $class,
];
}
protected function createFactory(string $domain, string $name, string $modelNamespace): void
{
$factoryNamespace = "Database\\Factories\\{$domain}";
$factoryPath = database_path("factories/{$domain}/{$name}Factory.php");
$this->files->ensureDirectoryExists(dirname($factoryPath));
if ($this->files->exists($factoryPath)) {
$this->components->warn("Factory already exists: [{$factoryPath}]");
return;
}
$stubPath = app_path('Console/stubs/domain-make/factory.stub');
$stub = $this->files->exists($stubPath) ? $this->files->get($stubPath) : '';
$stub = str_replace(
['{{ factoryNamespace }}', '{{ modelNamespace }}', '{{ class }}'],
[$factoryNamespace, $modelNamespace, $name],
$stub
);
$this->files->put($factoryPath, $stub);
$this->components->info("Factory [database/factories/{$domain}/{$name}Factory.php] created successfully.");
}
}