Skip to content
Draft
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 91 additions & 3 deletions src/Illuminate/Console/GeneratorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

namespace Illuminate\Console;

use Composer\Autoload\ClassLoader;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Finder\Finder;

use function Laravel\Prompts\text;

abstract class GeneratorCommand extends Command implements PromptsForMissingInput
{
/**
Expand Down Expand Up @@ -117,6 +122,8 @@ abstract class GeneratorCommand extends Command implements PromptsForMissingInpu
'__TRAIT__',
];

protected string $rootNamespace;

/**
* Create a new generator command instance.
*
Expand All @@ -130,6 +137,13 @@ public function __construct(Filesystem $files)
$this->addTestOptions();
}

$this->getDefinition()->addOption(new InputOption(
'in',
null,
InputOption::VALUE_REQUIRED,
"Specify a namespace to generate the {$this->type} class in"
));

$this->files = $files;
}

Expand Down Expand Up @@ -305,9 +319,70 @@ protected function alreadyExists($rawName)
*/
protected function getPath($name)
{
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
try {
return $this->resolvePathForClass($name);
} catch (RuntimeException $e) {
return $this->laravel['path.base'].'/'.str_replace('\\', '/', $name).'.php';
}
}

/**
* Resolve the expected path for a class based on Composer autoload mappings.
*
* @throws \RuntimeException if multiple base paths match or none can be resolved
*/
protected function resolvePathForClass(string $class): string
{
$namespaceRoots = [];

// Collect valid PSR-4 and PSR-0 namespace mappings
foreach (ClassLoader::getRegisteredLoaders() as $loader) {
foreach ([$loader->getPrefixesPsr4(), $loader->getPrefixes()] as $prefixes) {
foreach ($prefixes as $namespace => $paths) {
foreach ($paths as $path) {
$real = realpath($path);
if ($real !== false) {
$namespaceRoots[rtrim($namespace, '\\')][] = $real;
}
}
}
}
}

// Sort by namespace depth (deepest first)
uksort($namespaceRoots, fn ($a, $b) => Str::substrCount($b, '\\') <=> Str::substrCount($a, '\\'));

return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'.php';
foreach ($namespaceRoots as $prefix => $paths) {
if (! Str::startsWith($class, $prefix)) {
continue;
}

// Filter duplicates and invalid entries
$paths = array_unique(array_filter($paths));

if (count($paths) > 1) {
throw new RuntimeException(sprintf(
'Multiple base paths found for namespace [%s]: %s',
$prefix,
implode(', ', $paths)
));
}

if (empty($paths)) {
continue;
}

$basePath = reset($paths);
$relative = ltrim(Str::after($class, $prefix), '\\');
$relativePath = str_replace(['\\', '_'], DIRECTORY_SEPARATOR, $relative).'.php';

return rtrim($basePath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$relativePath;
}

throw new RuntimeException(sprintf(
'Unable to resolve a base path for class [%s]',
$class
));
}

/**
Expand Down Expand Up @@ -433,7 +508,20 @@ protected function getNameInput()
*/
protected function rootNamespace()
{
return $this->laravel->getNamespace();
if (! empty($this->rootNamespace)) {
return $this->rootNamespace;
}

$in = $this->option('in') ?? $this->laravel->getNamespace();
if (empty($in)) {
$in = text(
label: 'What namespace would you like to generate the '.$this->type.' in?',
placeholder: 'App',
validate: fn ($value) => empty($value) ? 'The in option is required when the application namespace is empty.' : null,
);
}

return $this->rootNamespace = trim($in, '\\').'\\';
}

/**
Expand Down
Loading