From fb72c0fcc4732d3dd3bd226c0d7b934dfd68e50b Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 10 Jul 2017 12:10:18 +1200 Subject: [PATCH] Update to use composer root object Move shared commands to trait --- src/RecipeCommandBehaviour.php | 185 ++++++++++++++++++++++++++++++++ src/RequireRecipeCommand.php | 186 ++++----------------------------- 2 files changed, 204 insertions(+), 167 deletions(-) create mode 100644 src/RecipeCommandBehaviour.php diff --git a/src/RecipeCommandBehaviour.php b/src/RecipeCommandBehaviour.php new file mode 100644 index 0000000..e7a3202 --- /dev/null +++ b/src/RecipeCommandBehaviour.php @@ -0,0 +1,185 @@ +getApplication()->find('require'); + $packages = [$recipe]; + if ($constraint) { + $packages[] = $constraint; + } + $arguments = [ + 'command' => 'require', + 'packages' => $packages, + ]; + $requireInput = new ArrayInput($arguments); + $returnCode = $command->run($requireInput, $output); + + // Flush modified composer object + $this->resetComposer(); + return $returnCode; + } + + /** + * Update the project + * + * @param OutputInterface $output + * @return int + */ + protected function updateProject(OutputInterface $output) + { + /** @var UpdateCommand $command */ + $command = $this->getApplication()->find('update'); + $arguments = [ 'command' => 'update' ]; + $requireInput = new ArrayInput($arguments); + $returnCode = $command->run($requireInput, $output); + + // Flush modified composer object + $this->resetComposer(); + return $returnCode; + } + + /** + * Find installed version or constraint + * + * @param string $recipe + * @return string + */ + protected function findInstalledVersion($recipe) + { + // Check locker + $installed = $this->getComposer()->getLocker()->getLockedRepository()->findPackage($recipe, '*'); + if ($installed) { + return $installed->getPrettyVersion(); + } + + // Check provides + $provides = $this->getComposer()->getPackage()->getProvides(); + if (isset($provides[$recipe])) { + return $provides[$recipe]->getPrettyConstraint(); + } + + // Check requires + $requires = $this->getComposer()->getPackage()->getRequires(); + if (isset($requires[$recipe])) { + return $provides[$recipe]->getPrettyConstraint(); + } + + // No existing version + 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; + } +} diff --git a/src/RequireRecipeCommand.php b/src/RequireRecipeCommand.php index fec84aa..f514cfd 100644 --- a/src/RequireRecipeCommand.php +++ b/src/RequireRecipeCommand.php @@ -2,18 +2,19 @@ namespace SilverStripe\RecipePlugin; -use BadMethodCallException; use Composer\Command\BaseCommand; -use Composer\Command\RequireCommand; -use Composer\Command\UpdateCommand; -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\OutputInterface; +/** + * Provides the 'require-recipe' command which allows a new recipe to be installed, but also + * soft-updates any existing recipe. + */ class RequireRecipeCommand extends BaseCommand { + use RecipeCommandBehaviour; + public function configure() { $this->setName('require-recipe'); @@ -52,14 +53,11 @@ HELP public function execute(InputInterface $input, OutputInterface $output) { - // Get args and existing composer data $recipe = $input->getArgument('recipe'); $constraint = $input->getArgument('version'); - // Check if this is already installed + // Check if this is already installed and notify users $installedVersion = $this->findInstalledVersion($recipe); - - // Notify users of which version is being updated if ($installedVersion) { if ($constraint) { $output->writeln( @@ -87,18 +85,23 @@ HELP return $returnCode; } - // Get composer data for both root and newly installed recipe + // Begin modification of composer.json $composerData = $this->loadComposer(getcwd() .'/composer.json'); - $recipeData = $this->loadComposer(getcwd().'/vendor/'.$recipe.'/composer.json'); - // Promote all dependencies - if (!empty($recipeData['require'])) { + // Get composer data for both root and newly installed recipe + $installedRecipe = $this + ->getComposer() + ->getRepositoryManager() + ->getLocalRepository() + ->findPackage($recipe, '*'); + if ($installedRecipe) { $output->writeln("Inlining all dependencies for recipe {$recipe}:"); - foreach ($recipeData['require'] as $dependencyName => $dependencyVersion) { + foreach ($installedRecipe->getRequires() as $requireName => $require) { + $requireVersion = $require->getPrettyConstraint(); $output->writeln( - " * Inline dependency {$dependencyName} as {$dependencyVersion}" + " * Inline dependency {$requireName} as {$requireVersion}" ); - $composerData['require'][$dependencyName] = $dependencyVersion; + $composerData['require'][$requireName] = $requireVersion; } } @@ -114,155 +117,4 @@ HELP $this->saveComposer(getcwd(), $composerData); return $this->updateProject($output); } - - /** - * Load composer data from the given 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($path, $default = null) - { - if (!file_exists($path)) { - 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) { - throw new \LogicException("Invalid composer.json with error: " . json_last_error_msg()); - } - return $data; - } - - /** - * Save the given data to the composer file in the given directory - * - * @param string $directory - * @param array $data - */ - protected function saveComposer($directory, $data) - { - $path = $directory.'/composer.json'; - if (!file_exists($path)) { - throw new BadMethodCallException("Could not find composer.json"); - } - $content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - // Make sure errors are reported - if (json_last_error()) { - throw new InvalidArgumentException("Invalid composer.json data with error: " . json_last_error_msg()); - } - file_put_contents($path, $content); - } - - /** - * @param OutputInterface $output - * @param string $recipe - * @param string $constraint - * @return int - */ - protected function requireRecipe(OutputInterface $output, $recipe, $constraint = null) - { - /** @var RequireCommand $command */ - $command = $this->getApplication()->find('require'); - $packages = [$recipe]; - if ($constraint) { - $packages[] = $constraint; - } - $arguments = [ - 'command' => 'require', - 'packages' => $packages, - ]; - $requireInput = new ArrayInput($arguments); - $returnCode = $command->run($requireInput, $output); - return $returnCode; - } - - /** - * Update the project - * - * @param OutputInterface $output - * @return int - */ - protected function updateProject(OutputInterface $output) - { - /** @var UpdateCommand $command */ - $command = $this->getApplication()->find('update'); - $arguments = [ 'command' => 'update' ]; - $requireInput = new ArrayInput($arguments); - $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; - } }