Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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"
));
Comment on lines +140 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only needs to prompt for the argument in the case where the root namespace provided by the Laravel application container is empty, all other cases it should default to that value instead without prompting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what getOptions() is for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you're proposing exactly, can you do it as a code suggestion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure: https://github.com/laravel/framework/pull/57578/files#r2478427777

This looks like a lot but it really is just the first and last few lines. And GitHub seems to struggle with rendering the "before"/"old" part of the diff correctly


$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) {
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
12 changes: 12 additions & 0 deletions tests/Integration/Generators/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

namespace Illuminate\Tests\Integration\Generators;

use Composer\Autoload\ClassLoader;
use Orchestra\Testbench\Concerns\InteractsWithPublishedFiles;

abstract class TestCase extends \Orchestra\Testbench\TestCase
{
use InteractsWithPublishedFiles;

protected function setUp(): void
{
parent::setUp();

// Register the App namespace with Composer's autoloader for the testbench laravel app
$appPath = $this->app->path();
foreach (ClassLoader::getRegisteredLoaders() as $loader) {
$loader->addPsr4('App\\', [$appPath]);
}
}
}
Loading