From bb1bb022e4625291a6be5b985b53cf6177ba6b71 Mon Sep 17 00:00:00 2001 From: Andreas Scheibel Date: Tue, 20 Apr 2021 21:58:38 +0200 Subject: [PATCH] "composer init --autoload" - Interactive generates PSR-4 autoloader in composer.json (#9829) - Generates PSR-4 autoload entry in composer.json. - Run dump-autoload, if no dependencies are set --- doc/03-cli.md | 1 + src/Composer/Command/InitCommand.php | 103 +++++++++++++++++- .../Composer/Test/Command/InitCommandTest.php | 22 ++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index cf4dc6aac..6e7de1333 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -65,6 +65,7 @@ php composer.phar init the list of requires. Every repository can be either an HTTP URL pointing to a `composer` repository or a JSON string which similar to what the [repositories](04-schema.md#repositories) key accepts. +* **--autoload (-a):** Add a PSR-4 autoload mapping to the composer.json. Automatically maps your package's namespace to the provided directory. (Expects a relative path, e.g. src/) See also [PSR-4 autoload](04-schema.md#psr-4). ## install / i diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index e9ac03978..8b73c5e26 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -24,6 +24,7 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; +use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Composer\Semver\Constraint\Constraint; use Symfony\Component\Console\Input\ArrayInput; @@ -69,6 +70,7 @@ class InitCommand extends BaseCommand new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), + new InputOption('autoload', 'a', InputOption::VALUE_REQUIRED, 'Add PSR-4 autoload mapping. Maps your package\'s namespace to the provided directory. (Expects a relative path, e.g. src/)'), )) ->setHelp( <<getIO(); - $allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); + $allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload'); $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist))); if (isset($options['author'])) { @@ -123,6 +125,18 @@ EOT } } + // --autoload - create autoload object + $autoloadPath = null; + if (isset($options['autoload'])) { + $autoloadPath = $options['autoload']; + $namespace = $this->namespaceFromPackageName($input->getOption('name')); + $options['autoload'] = (object) array( + 'psr-4' => array( + $namespace . '\\' => $autoloadPath, + ) + ); + } + $file = new JsonFile(Factory::getComposerFile()); $json = JsonFile::encode($options); @@ -143,6 +157,17 @@ EOT $file->write($options); + // --autoload - Create src folder + if ($autoloadPath) { + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($autoloadPath); + + // dump-autoload only for projects without added dependencies. + if (!$this->hasDependencies($options)) { + $this->runDumpAutoloadCommand($output); + } + } + if ($input->isInteractive() && is_dir('.git')) { $ignoreFile = realpath('.gitignore'); @@ -164,6 +189,14 @@ EOT $this->installDependencies($output); } + // --autoload - Show post-install configuration info + if ($autoloadPath) { + $namespace = $this->namespaceFromPackageName($input->getOption('name')); + + $io->writeError('PSR-4 autoloading configured. Use "namespace '.$namespace.';" in '.$autoloadPath); + $io->writeError('Include the Composer autoloader with: require \'vendor/autoload.php\';'); + } + return 0; } @@ -381,6 +414,37 @@ EOT $devRequirements = $this->determineRequirements($input, $output, $requireDev, $platformRepo, $preferredStability); } $input->setOption('require-dev', $devRequirements); + + // --autoload - input and validation + $autoload = $input->getOption('autoload') ?: 'src/'; + $namespace = $this->namespaceFromPackageName($input->getOption('name')); + $autoload = $io->askAndValidate( + 'Add PSR-4 autoload mapping? Maps namespace "'.$namespace.'" to the entered relative path. ['.$autoload.', n to skip]: ', + function ($value) use ($autoload) { + if (null === $value) { + return $autoload; + } + + if ($value === 'n' || $value === 'no') { + return; + } + + $value = $value ?: $autoload; + + if (!preg_match('{^[^/][A-Za-z0-9\-_/]+/$}', $value)) { + throw new \InvalidArgumentException(sprintf( + 'The src folder name "%s" is invalid. Please add a relative path with tailing forward slash. [A-Za-z0-9_-/]+/', + $value + )); + } + + return $value; + }, + null, + $autoload + ); + $input->setOption('autoload', $autoload); + } /** @@ -593,6 +657,33 @@ EOT return array($this->parseAuthorString($author)); } + /** + * Extract namespace from package's vendor name. + * + * new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" + * + * @param string $packageName + * + * @return string|null + */ + public function namespaceFromPackageName($packageName) + { + if (!$packageName || strpos($packageName, '/') === false) { + return null; + } + + $namespace = array_map( + function($part) { + $part = preg_replace('/[^a-z0-9]/i', ' ', $part); + $part = ucwords($part); + return str_replace(' ', '', $part); + }, + explode('/', $packageName) + ); + + return join('\\', $namespace); + } + protected function getGitConfig() { if (null !== $this->gitConfig) { @@ -882,6 +973,16 @@ EOT } } + private function runDumpAutoloadCommand($output) + { + try { + $command = $this->getApplication()->find('dump-autoload'); + $command->run(new ArrayInput(array()), $output); + } catch (\Exception $e) { + $this->getIO()->writeError('Could not run dump-autoload.'); + } + } + private function hasDependencies($options) { $requires = (array) $options['require']; diff --git a/tests/Composer/Test/Command/InitCommandTest.php b/tests/Composer/Test/Command/InitCommandTest.php index d1c3b62fd..d8c7e50eb 100644 --- a/tests/Composer/Test/Command/InitCommandTest.php +++ b/tests/Composer/Test/Command/InitCommandTest.php @@ -92,4 +92,26 @@ class InitCommandTest extends TestCase $this->setExpectedException('InvalidArgumentException'); $command->parseAuthorString('John Smith '); } + + public function testNamespaceFromValidPackageName() + { + $command = new InitCommand; + $namespace = $command->namespaceFromPackageName('new_projects.acme-extra/package-name'); + $this->assertEquals('NewProjectsAcmeExtra\PackageName', $namespace); + } + + public function testNamespaceFromInvalidPackageName() + { + $command = new InitCommand; + $namespace = $command->namespaceFromPackageName('invalid-package-name'); + $this->assertNull($namespace); + } + + public function testNamespaceFromMissingPackageName() + { + $command = new InitCommand; + $namespace = $command->namespaceFromPackageName(null); + $this->assertNull($namespace); + } + }