diff --git a/src/RequireRecipeCommand.php b/src/RequireRecipeCommand.php
index c188545..fec84aa 100644
--- a/src/RequireRecipeCommand.php
+++ b/src/RequireRecipeCommand.php
@@ -10,7 +10,6 @@ use MongoDB\Driver\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Output\OutputInterface;
class RequireRecipeCommand extends BaseCommand
@@ -26,8 +25,8 @@ class RequireRecipeCommand extends BaseCommand
);
$this->addArgument(
'version',
- InputArgument::REQUIRED,
- 'Version to require'
+ InputArgument::OPTIONAL,
+ 'Version or constraint to require'
);
$this->addUsage('silverstripe/recipe-blogging 1.0.0');
$this->setHelp(
@@ -53,60 +52,84 @@ HELP
public function execute(InputInterface $input, OutputInterface $output)
{
+ // Get args and existing composer data
$recipe = $input->getArgument('recipe');
- $version = $input->getArgument('version');
+ $constraint = $input->getArgument('version');
- // Get existing composer data
- $composerData = $this->loadComposer(getcwd());
- if (isset($composerData['provide'][$recipe])) {
- $output->writeln("This recipe is already added to provide");
- return -1;
+ // Check if this is already installed
+ $installedVersion = $this->findInstalledVersion($recipe);
+
+ // Notify users of which version is being updated
+ if ($installedVersion) {
+ if ($constraint) {
+ $output->writeln(
+ "Updating existing recipe from {$installedVersion} to {$constraint}"
+ );
+ } else {
+ // Show a guessed constraint
+ $constraint = $this->findBestConstraint($installedVersion);
+ if ($constraint) {
+ $output->writeln(
+ "Updating existing recipe from {$installedVersion} to {$constraint} "
+ . "(auto-detected constraint)"
+ );
+ } else {
+ $output->writeln(
+ "Updating existing recipe from {$installedVersion} to latest version"
+ );
+ }
+ }
}
// Ensure composer require includes this recipe
- $returnCode = $this->requireRecipe($output, $recipe, $version);
+ $returnCode = $this->requireRecipe($output, $recipe, $constraint);
if ($returnCode) {
return $returnCode;
}
// Get composer data for both root and newly installed recipe
- $composerData = $this->loadComposer(getcwd());
- $recipeData = $this->loadComposer(getcwd().'/vendor/'.$recipe);
+ $composerData = $this->loadComposer(getcwd() .'/composer.json');
+ $recipeData = $this->loadComposer(getcwd().'/vendor/'.$recipe.'/composer.json');
// Promote all dependencies
if (!empty($recipeData['require'])) {
- $output->writeln("Inlining all dependencies for recipe $recipe:");
+ $output->writeln("Inlining all dependencies for recipe {$recipe}:");
foreach ($recipeData['require'] as $dependencyName => $dependencyVersion) {
- $output->writeln(" * Inline dependency $dependencyName as $dependencyVersion");
+ $output->writeln(
+ " * Inline dependency {$dependencyName} as {$dependencyVersion}"
+ );
$composerData['require'][$dependencyName] = $dependencyVersion;
}
}
// Move recipe from 'require' to 'provide'
+ $installedVersion = $this->findInstalledVersion($recipe) ?: $installedVersion;
unset($composerData['require'][$recipe]);
if (!isset($composerData['provide'])) {
$composerData['provide'] = [];
}
- $composerData['provide'][$recipe] = $version;
+ $composerData['provide'][$recipe] = $installedVersion;
// Update composer.json and synchronise composer.lock
$this->saveComposer(getcwd(), $composerData);
- $this->updateProject($output);
-
- return $returnCode;
+ return $this->updateProject($output);
}
/**
* Load composer data from the given directory
*
- * @param string $directory
+ * @param string $path
+ * @param array|null $default If file doesn't exist use this default. If null, file is mandatory and there is
+ * no default
* @return array
*/
- protected function loadComposer($directory)
+ protected function loadComposer($path, $default = null)
{
- $path = $directory.'/composer.json';
if (!file_exists($path)) {
- throw new BadMethodCallException("Could not find composer.json");
+ if (isset($default)) {
+ return $default;
+ }
+ throw new BadMethodCallException("Could not find " . basename($path));
}
$data = json_decode(file_get_contents($path), true);
if (json_last_error() !== JSON_ERROR_NONE) {
@@ -137,18 +160,21 @@ HELP
/**
* @param OutputInterface $output
- * @param $recipe
- * @param $version
+ * @param string $recipe
+ * @param string $constraint
* @return int
*/
- protected function requireRecipe(OutputInterface $output, $recipe, $version)
+ protected function requireRecipe(OutputInterface $output, $recipe, $constraint = null)
{
/** @var RequireCommand $command */
$command = $this->getApplication()->find('require');
- $package = $recipe . ':' . $version;
+ $packages = [$recipe];
+ if ($constraint) {
+ $packages[] = $constraint;
+ }
$arguments = [
'command' => 'require',
- 'packages' => [$package],
+ 'packages' => $packages,
];
$requireInput = new ArrayInput($arguments);
$returnCode = $command->run($requireInput, $output);
@@ -170,4 +196,73 @@ HELP
$returnCode = $command->run($requireInput, $output);
return $returnCode;
}
+
+ /**
+ * @param string $recipe
+ * @return string
+ */
+ protected function findInstalledVersion($recipe)
+ {
+ // Check composer.lock file
+ $lockData = $this->loadComposer(getcwd() . '/composer.lock', []);
+ if (isset($lockData['packages'])) {
+ foreach ($lockData['packages'] as $package) {
+ // Get version of installed file
+ if (isset($package['name']) && isset($package['version']) && $package['name'] === $recipe) {
+ $version = $package['version'];
+ // Trim leading `v` from `v1.0.0`
+ if (preg_match('#v([\d.]+)#i', $version)) {
+ return substr($version, 1);
+ }
+ return $version;
+ }
+ }
+ }
+
+ // Check composer.json
+ $composerData = $this->loadComposer(getcwd() . '/composer.json');
+
+ // Check provide for previously inlined recipe
+ if (isset($composerData['provide'][$recipe])) {
+ return $composerData['provide'][$recipe];
+ }
+
+ // Check existing constraints, or installed version
+ if (isset($composerData['require'][$recipe])) {
+ return $composerData['require'][$recipe];
+ }
+ return null;
+ }
+
+ /**
+ * Guess constraint to use if not provided
+ *
+ * @param string $existingVersion Known installed version
+ * @return string
+ */
+ protected function findBestConstraint($existingVersion)
+ {
+ // Cannot guess without existing version
+ if (!$existingVersion) {
+ return null;
+ }
+
+ // Existing version is already a ^1.0.0 or ~1.0.0 constraint
+ if (preg_match('#^[~^]#', $existingVersion)) {
+ return $existingVersion;
+ }
+
+ // Existing version is already a dev constraint
+ if (stristr($existingVersion, 'dev') !== false) {
+ return $existingVersion;
+ }
+
+ // Numeric-only version maps to semver constraint
+ if (preg_match('#^([\d.]+)$#', $existingVersion)) {
+ return "^{$existingVersion}";
+ }
+
+ // Cannot guess; Let composer choose (equivalent to `composer require vendor/library`)
+ return null;
+ }
}