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

662 lines
24 KiB
PHTML

<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Command;
use Composer\Factory;
use Composer\Json\JsonFile;
use Composer\Json\JsonValidationException;
use Composer\Package\BasePackage;
use Composer\Package\Package;
use Composer\Pcre\Preg;
use Composer\Repository\CompositeRepository;
use Composer\Repository\PlatformRepository;
use Composer\Repository\RepositoryFactory;
use Composer\Util\Filesystem;
use Composer\Util\Silencer;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Helper\FormatterHelper;
/**
* @author Justin Rainbow <justin.rainbow@gmail.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class InitCommand extends BaseCommand
{
use PackageDiscoveryTrait;
/** @var array<string, string> */
private $gitConfig;
/**
* @inheritDoc
*
* @return void
*/
protected function configure()
{
$this
->setName('init')
->setDescription('Creates a basic composer.json file in current directory.')
->setDefinition(array(
new InputOption('name', null, InputOption::VALUE_REQUIRED, 'Name of the package'),
new InputOption('description', null, InputOption::VALUE_REQUIRED, 'Description of package'),
new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'),
new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'),
new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'),
new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'),
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/)'),
))
6 years ago
->setHelp(
<<<EOT
The <info>init</info> command creates a basic composer.json file
in the current directory.
<info>php composer.phar init</info>
Read more at https://getcomposer.org/doc/03-cli.md#init
EOT
)
;
}
/**
* @throws \Seld\JsonLint\ParsingException
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = $this->getIO();
$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['name']) && !Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $options['name'])) {
throw new \InvalidArgumentException(
'The package name '.$options['name'].' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+'
);
}
if (isset($options['author'])) {
$options['authors'] = $this->formatAuthors($options['author']);
unset($options['author']);
}
$repositories = $input->getOption('repository');
if (count($repositories) > 0) {
$config = Factory::createConfig($io);
foreach ($repositories as $repo) {
$options['repositories'][] = RepositoryFactory::configFromString($io, $config, $repo, true);
}
}
if (isset($options['stability'])) {
$options['minimum-stability'] = $options['stability'];
unset($options['stability']);
}
$options['require'] = isset($options['require']) ? $this->formatRequirements($options['require']) : new \stdClass;
if (array() === $options['require']) {
$options['require'] = new \stdClass;
}
if (isset($options['require-dev'])) {
10 years ago
$options['require-dev'] = $this->formatRequirements($options['require-dev']);
if (array() === $options['require-dev']) {
$options['require-dev'] = new \stdClass;
}
}
// --autoload - create autoload object
$autoloadPath = null;
if (isset($options['autoload'])) {
$autoloadPath = $options['autoload'];
$namespace = $this->namespaceFromPackageName((string) $input->getOption('name'));
$options['autoload'] = (object) array(
'psr-4' => array(
$namespace . '\\' => $autoloadPath,
3 years ago
),
);
}
$file = new JsonFile(Factory::getComposerFile());
$json = JsonFile::encode($options);
if ($input->isInteractive()) {
$io->writeError(array('', $json, ''));
if (!$io->askConfirmation('Do you confirm generation [<comment>yes</comment>]? ')) {
$io->writeError('<error>Command aborted</error>');
return 1;
}
} else {
if (json_encode($options) === '{"require":{}}') {
throw new \RuntimeException('You have to run this command in interactive mode, or specify at least some data using --name, --require, etc.');
}
$io->writeError('Writing '.$file->getPath());
}
$file->write($options);
try {
$file->validateSchema(JsonFile::LAX_SCHEMA);
} catch (JsonValidationException $e) {
$io->writeError('<error>Schema validation error, aborting</error>');
$errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors());
$io->writeError($e->getMessage() . ':' . PHP_EOL . $errors);
Silencer::call('unlink', $file->getPath());
return 1;
}
// --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');
if (false === $ignoreFile) {
$ignoreFile = realpath('.') . '/.gitignore';
}
if (!$this->hasVendorIgnore($ignoreFile)) {
$question = 'Would you like the <info>vendor</info> directory added to your <info>.gitignore</info> [<comment>yes</comment>]? ';
if ($io->askConfirmation($question)) {
$this->addVendorIgnore($ignoreFile);
}
}
}
$question = 'Would you like to install dependencies now [<comment>yes</comment>]? ';
if ($input->isInteractive() && $this->hasDependencies($options) && $io->askConfirmation($question)) {
$this->updateDependencies($output);
}
// --autoload - Show post-install configuration info
if ($autoloadPath) {
$namespace = $this->namespaceFromPackageName((string) $input->getOption('name'));
$io->writeError('PSR-4 autoloading configured. Use "<comment>namespace '.$namespace.';</comment>" in '.$autoloadPath);
$io->writeError('Include the Composer autoloader with: <comment>require \'vendor/autoload.php\';</comment>');
}
return 0;
}
/**
* @inheritDoc
*
* @return void
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
$git = $this->getGitConfig();
$io = $this->getIO();
/** @var FormatterHelper $formatter */
$formatter = $this->getHelperSet()->get('formatter');
// initialize repos if configured
$repositories = $input->getOption('repository');
if (count($repositories) > 0) {
$config = Factory::createConfig($io);
$repos = array(new PlatformRepository);
$createDefaultPackagistRepo = true;
foreach ($repositories as $repo) {
$repoConfig = RepositoryFactory::configFromString($io, $config, $repo, true);
if (
(isset($repoConfig['packagist']) && $repoConfig === array('packagist' => false))
|| (isset($repoConfig['packagist.org']) && $repoConfig === array('packagist.org' => false))
) {
$createDefaultPackagistRepo = false;
continue;
}
$repos[] = RepositoryFactory::createRepo($io, $config, $repoConfig);
}
if ($createDefaultPackagistRepo) {
$repos[] = RepositoryFactory::createRepo($io, $config, array(
'type' => 'composer',
'url' => 'https://repo.packagist.org',
));
}
$this->repos = new CompositeRepository($repos);
unset($repos, $config, $repositories);
}
$io->writeError(array(
'',
$formatter->formatBlock('Welcome to the Composer config generator', 'bg=blue;fg=white', true),
9 years ago
'',
));
// namespace
$io->writeError(array(
'',
'This command will guide you through creating your composer.json config.',
'',
));
$cwd = realpath(".");
$name = $input->getOption('name');
if (null === $name) {
$name = basename($cwd);
$name = Preg::replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $name);
$name = strtolower($name);
7 years ago
if (!empty($_SERVER['COMPOSER_DEFAULT_VENDOR'])) {
$name = $_SERVER['COMPOSER_DEFAULT_VENDOR'] . '/' . $name;
} elseif (isset($git['github.user'])) {
$name = $git['github.user'] . '/' . $name;
} elseif (!empty($_SERVER['USERNAME'])) {
$name = $_SERVER['USERNAME'] . '/' . $name;
} elseif (!empty($_SERVER['USER'])) {
$name = $_SERVER['USER'] . '/' . $name;
} elseif (get_current_user()) {
$name = get_current_user() . '/' . $name;
} else {
// package names must be in the format foo/bar
$name .= '/' . $name;
}
$name = strtolower($name);
}
$name = $io->askAndValidate(
'Package name (<vendor>/<name>) [<comment>'.$name.'</comment>]: ',
function ($value) use ($name) {
if (null === $value) {
return $name;
}
if (!Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $value)) {
throw new \InvalidArgumentException(
'The package name '.$value.' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+'
);
}
return $value;
},
null,
$name
);
$input->setOption('name', $name);
$description = $input->getOption('description') ?: null;
$description = $io->ask(
'Description [<comment>'.$description.'</comment>]: ',
$description
);
$input->setOption('description', $description);
if (null === $author = $input->getOption('author')) {
7 years ago
if (!empty($_SERVER['COMPOSER_DEFAULT_AUTHOR'])) {
$author_name = $_SERVER['COMPOSER_DEFAULT_AUTHOR'];
7 years ago
} elseif (isset($git['user.name'])) {
$author_name = $git['user.name'];
}
7 years ago
if (!empty($_SERVER['COMPOSER_DEFAULT_EMAIL'])) {
$author_email = $_SERVER['COMPOSER_DEFAULT_EMAIL'];
7 years ago
} elseif (isset($git['user.email'])) {
$author_email = $git['user.email'];
}
if (isset($author_name, $author_email)) {
$author = sprintf('%s <%s>', $author_name, $author_email);
}
}
$author = $io->askAndValidate(
'Author ['.(is_string($author) ? '<comment>'.$author.'</comment>, ' : '') . 'n to skip]: ',
function ($value) use ($author) {
if ($value === 'n' || $value === 'no') {
return;
}
$value = $value ?: $author;
$author = $this->parseAuthorString($value);
if ($author['email'] === null) {
return $author['name'];
}
return sprintf('%s <%s>', $author['name'], $author['email']);
},
null,
$author
);
$input->setOption('author', $author);
$minimumStability = $input->getOption('stability') ?: null;
$minimumStability = $io->askAndValidate(
'Minimum Stability [<comment>'.$minimumStability.'</comment>]: ',
function ($value) use ($minimumStability) {
if (null === $value) {
return $minimumStability;
}
if (!isset(BasePackage::$stabilities[$value])) {
throw new \InvalidArgumentException(
'Invalid minimum stability "'.$value.'". Must be empty or one of: '.
implode(', ', array_keys(BasePackage::$stabilities))
);
}
return $value;
},
null,
$minimumStability
);
$input->setOption('stability', $minimumStability);
$type = $input->getOption('type') ?: false;
$type = $io->ask(
'Package Type (e.g. library, project, metapackage, composer-plugin) [<comment>'.$type.'</comment>]: ',
$type
);
$input->setOption('type', $type);
if (null === $license = $input->getOption('license')) {
7 years ago
if (!empty($_SERVER['COMPOSER_DEFAULT_LICENSE'])) {
$license = $_SERVER['COMPOSER_DEFAULT_LICENSE'];
}
}
$license = $io->ask(
'License [<comment>'.$license.'</comment>]: ',
$license
);
$input->setOption('license', $license);
$io->writeError(array('', 'Define your dependencies.', ''));
// prepare to resolve dependencies
$repos = $this->getRepos();
$preferredStability = $minimumStability ?: 'stable';
$platformRepo = null;
if ($repos instanceof CompositeRepository) {
foreach ($repos->getRepositories() as $candidateRepo) {
if ($candidateRepo instanceof PlatformRepository) {
$platformRepo = $candidateRepo;
break;
}
}
}
$question = 'Would you like to define your dependencies (require) interactively [<comment>yes</comment>]? ';
$require = $input->getOption('require');
$requirements = array();
if (count($require) > 0 || $io->askConfirmation($question)) {
$requirements = $this->determineRequirements($input, $output, $require, $platformRepo, $preferredStability);
}
$input->setOption('require', $requirements);
$question = 'Would you like to define your dev dependencies (require-dev) interactively [<comment>yes</comment>]? ';
$requireDev = $input->getOption('require-dev');
$devRequirements = array();
if (count($requireDev) > 0 || $io->askConfirmation($question)) {
$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((string) $input->getOption('name'));
$autoload = $io->askAndValidate(
'Add PSR-4 autoload mapping? Maps namespace "'.$namespace.'" to the entered relative path. [<comment>'.$autoload.'</comment>, n to skip]: ',
function ($value) use ($autoload) {
if (null === $value) {
return $autoload;
}
if ($value === 'n' || $value === 'no') {
return;
}
$value = $value ?: $autoload;
if (!Preg::isMatch('{^[^/][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);
}
9 years ago
/**
9 years ago
* @param string $author
* @return array{name: string, email: string|null}
9 years ago
*/
private function parseAuthorString(string $author): array
9 years ago
{
if (Preg::isMatch('/^(?P<name>[- .,\p{L}\p{N}\p{Mn}\'"()]+)(?:\s+<(?P<email>.+?)>)?$/u', $author, $match)) {
$hasEmail = isset($match['email']) && '' !== $match['email'];
if ($hasEmail && !$this->isValidEmail($match['email'])) {
throw new \InvalidArgumentException('Invalid email "'.$match['email'].'"');
9 years ago
}
return array(
'name' => trim($match['name']),
'email' => $hasEmail ? $match['email'] : null,
);
9 years ago
}
throw new \InvalidArgumentException(
'Invalid author string. Must be in the formats: '.
'Jane Doe or John Smith <john@example.com>'
9 years ago
);
}
3 years ago
/**
* @param string $author
*
2 years ago
* @return array<int, array{name: string, email?: string}>
3 years ago
*/
2 years ago
protected function formatAuthors(string $author): array
{
2 years ago
$author = $this->parseAuthorString($author);
if (null === $author['email']) {
unset($author['email']);
}
return array($author);
}
/**
* Extract namespace from package's vendor name.
*
* new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName"
*
* @param string $packageName
*
* @return string|null
*/
2 years ago
public function namespaceFromPackageName(string $packageName): ?string
{
if (!$packageName || strpos($packageName, '/') === false) {
return null;
}
$namespace = array_map(
function ($part): string {
$part = Preg::replace('/[^a-z0-9]/i', ' ', $part);
$part = ucwords($part);
3 years ago
return str_replace(' ', '', $part);
},
explode('/', $packageName)
);
return join('\\', $namespace);
}
/**
* @return array<string, string>
*/
2 years ago
protected function getGitConfig(): array
{
if (null !== $this->gitConfig) {
return $this->gitConfig;
}
$finder = new ExecutableFinder();
$gitBin = $finder->find('git');
$cmd = new Process(array($gitBin, 'config', '-l'));
$cmd->run();
if ($cmd->isSuccessful()) {
$this->gitConfig = array();
Preg::matchAll('{^([^=]+)=(.*)$}m', $cmd->getOutput(), $matches);
foreach ($matches[1] as $key => $match) {
$this->gitConfig[$match] = $matches[2][$key];
}
return $this->gitConfig;
}
return $this->gitConfig = array();
}
/**
* Checks the local .gitignore file for the Composer vendor directory.
*
* Tested patterns include:
* "/$vendor"
* "$vendor"
* "$vendor/"
* "/$vendor/"
* "/$vendor/*"
* "$vendor/*"
*
* @param string $ignoreFile
* @param string $vendor
*
12 years ago
* @return bool
*/
2 years ago
protected function hasVendorIgnore(string $ignoreFile, string $vendor = 'vendor'): bool
{
if (!file_exists($ignoreFile)) {
return false;
}
$pattern = sprintf('{^/?%s(/\*?)?$}', preg_quote($vendor));
$lines = file($ignoreFile, FILE_IGNORE_NEW_LINES);
foreach ($lines as $line) {
if (Preg::isMatch($pattern, $line)) {
return true;
}
}
return false;
}
/**
* @param string $ignoreFile
* @param string $vendor
*
* @return void
*/
2 years ago
protected function addVendorIgnore(string $ignoreFile, string $vendor = '/vendor/'): void
{
$contents = "";
if (file_exists($ignoreFile)) {
$contents = file_get_contents($ignoreFile);
if (strpos($contents, "\n") !== 0) {
$contents .= "\n";
}
}
file_put_contents($ignoreFile, $contents . $vendor. "\n");
}
3 years ago
/**
* @param string $email
*
3 years ago
* @return bool
*/
2 years ago
protected function isValidEmail(string $email): bool
{
11 years ago
// assume it's valid if we can't validate it
if (!function_exists('filter_var')) {
return true;
}
return false !== filter_var($email, FILTER_VALIDATE_EMAIL);
}
3 years ago
/**
* @return void
*/
private function updateDependencies(OutputInterface $output): void
{
try {
$updateCommand = $this->getApplication()->find('update');
$this->getApplication()->resetComposer();
$updateCommand->run(new ArrayInput(array()), $output);
} catch (\Exception $e) {
$this->getIO()->writeError('Could not update dependencies. Run `composer update` to see more information.');
}
}
3 years ago
/**
* @return void
*/
private function runDumpAutoloadCommand(OutputInterface $output): void
{
try {
$command = $this->getApplication()->find('dump-autoload');
$this->getApplication()->resetComposer();
$command->run(new ArrayInput(array()), $output);
} catch (\Exception $e) {
$this->getIO()->writeError('Could not run dump-autoload.');
}
}
3 years ago
/**
* @param array<string, string|array<string>> $options
3 years ago
* @return bool
*/
private function hasDependencies(array $options): bool
{
$requires = (array) $options['require'];
$devRequires = isset($options['require-dev']) ? (array) $options['require-dev'] : array();
return !empty($requires) || !empty($devRequires);
}
}