diff --git a/src/Plugin.php b/src/Plugin.php index 1872a7b..4439d40 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -4,7 +4,6 @@ use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; -use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Pool; use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventSubscriberInterface; @@ -37,6 +36,9 @@ class Plugin implements PluginInterface, EventSubscriberInterface /** @var Composer */ private $composer; + /** @var string[] */ + private $installedPackages; + /** @var IOInterface */ private $io; @@ -58,6 +60,11 @@ public function activate(Composer $composer, IOInterface $io) { $this->composer = $composer; $this->io = $io; + + $installedPackages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(); + foreach ($installedPackages as $package) { + $this->installedPackages[$package->getName()] = $package->getPrettyVersion(); + } } public function onPostPackage(PackageEvent $event) @@ -145,14 +152,28 @@ private function runInstaller(RootPackageInterface $rootPackage, array $packages private function promptForPackageVersion($name) { - $validator = function ($input) { - $input = trim($input); - return $input ?: false; - }; + // Package is currently installed. Add it to root composer.json + if (isset($this->installedPackages[$name])) { + $this->io->write(sprintf( + 'Added package %s to composer.json with constraint %s;' + . ' to upgrade, run composer require %s:VERSION', + $name, + '^' . $this->installedPackages[$name], + $name + )); + + return '^' . $this->installedPackages[$name]; + } $constraint = $this->io->askAndValidate( - sprintf('Enter the version of %s to require (or leave blank to use the latest version): ', $name), - $validator + sprintf( + 'Enter the version of %s to require (or leave blank to use the latest version): ', + $name + ), + function ($input) { + $input = trim($input); + return $input ?: false; + } ); if ($constraint === false) { @@ -184,11 +205,11 @@ private function createInstaller(Composer $composer, IOInterface $io, RootPackag ); } - private function hasPackage($name) + private function hasPackage($package) { $requires = $this->composer->getPackage()->getRequires(); - foreach ($requires as $link) { - if (strtolower($link->getTarget()) === strtolower($name)) { + foreach ($requires as $name => $link) { + if (strtolower($name) === strtolower($package)) { return true; } } diff --git a/test/PluginTest.php b/test/PluginTest.php index ba3a38e..288a8d3 100644 --- a/test/PluginTest.php +++ b/test/PluginTest.php @@ -18,6 +18,7 @@ use Composer\Package\RootPackageInterface; use Composer\Package\Version\VersionSelector; use Composer\Repository\RepositoryManager; +use Composer\Repository\WritableRepositoryInterface; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -37,11 +38,24 @@ class PluginTest extends TestCase /** @var IOInterface|ObjectProphecy */ private $io; + /** @var WritableRepositoryInterface */ + private $localRepository; + protected function setUp() { parent::setUp(); + $this->localRepository = $this->prophesize(WritableRepositoryInterface::class); + $this->localRepository->getPackages()->willReturn([]); + + $repositoryManager = $this->prophesize(RepositoryManager::class); + $repositoryManager->getLocalRepository()->willReturn($this->localRepository->reveal()); + $this->composer = $this->prophesize(Composer::class); + $this->composer->getRepositoryManager() + ->willReturn($repositoryManager->reveal()) + ->shouldBeCalled(); + $this->io = $this->prophesize(IOInterface::class); $this->plugin = new Plugin(); @@ -244,7 +258,8 @@ public function testInstallSingleDependencyOnPackageUpdate() $this->composer->getConfig()->willReturn($config->reveal()); $this->io->askAndValidate( - 'Enter the version of extra-dependency-foo to require (or leave blank to use the latest version): ', + 'Enter the version of extra-dependency-foo to require' + . ' (or leave blank to use the latest version): ', Argument::type('callable') )->willReturn('17.0.1-dev'); @@ -316,7 +331,8 @@ public function testInstallSingleDependencyOnPackageInstall() $this->composer->getConfig()->willReturn($config->reveal()); $this->io->askAndValidate( - 'Enter the version of extra-dependency-foo to require (or leave blank to use the latest version): ', + 'Enter the version of extra-dependency-foo to require' + . ' (or leave blank to use the latest version): ', Argument::type('callable') )->willReturn('17.0.1-dev'); @@ -396,7 +412,8 @@ public function testInstallOneDependenciesWhenOneIsAlreadyInstalled() $this->composer->getConfig()->willReturn($config->reveal()); $this->io->askAndValidate( - 'Enter the version of extra-dependency-foo to require (or leave blank to use the latest version): ', + 'Enter the version of extra-dependency-foo to require' + . ' (or leave blank to use the latest version): ', Argument::type('callable') )->willReturn('17.0.1-dev'); @@ -474,7 +491,8 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersion() $this->composer->getConfig()->willReturn($config->reveal()); $this->io->askAndValidate( - 'Enter the version of extra-dependency-foo to require (or leave blank to use the latest version): ', + 'Enter the version of extra-dependency-foo to require' + . ' (or leave blank to use the latest version): ', Argument::type('callable') )->willReturn(false); @@ -500,6 +518,11 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersion() $this->setUpPool(); $this->assertNull($this->plugin->onPostPackage($event->reveal())); + + $json = file_get_contents(vfsStream::url('project/composer.json')); + $composer = json_decode($json, true); + $this->assertTrue(isset($composer['require']['extra-dependency-foo'])); + $this->assertSame('13.4.2', $composer['require']['extra-dependency-foo']); } public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersionNotFoundMatchingPackage() @@ -532,7 +555,8 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersionNo $this->composer->getConfig()->willReturn($config->reveal()); $this->io->askAndValidate( - 'Enter the version of extra-dependency-foo to require (or leave blank to use the latest version): ', + 'Enter the version of extra-dependency-foo to require' + . ' (or leave blank to use the latest version): ', Argument::type('callable') )->willReturn(false); @@ -551,6 +575,82 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersionNo $this->plugin->onPostPackage($event->reveal()); } + public function testUpdateComposerWithCurrentlyInstalledVersion() + { + $installedPackage = $this->prophesize(PackageInterface::class); + $installedPackage->getName()->willReturn('extra-dependency-foo'); + $installedPackage->getPrettyVersion()->willReturn('0.5.1'); + + $this->localRepository->getPackages()->willReturn([$installedPackage->reveal()]); + $this->plugin->activate($this->composer->reveal(), $this->io->reveal()); + + /** @var PackageInterface|ObjectProphecy $package */ + $package = $this->prophesize(PackageInterface::class); + $package->getName()->willReturn('some/component'); + $package->getExtra()->willReturn([ + 'dependency' => [ + 'extra-dependency-foo', + ], + ]); + + $operation = $this->prophesize(InstallOperation::class); + $operation->getPackage()->willReturn($package->reveal()); + + $event = $this->prophesize(PackageEvent::class); + $event->isDevMode()->willReturn(true); + $event->getOperation()->willReturn($operation->reveal()); + + $config = $this->prophesize(Config::class); + $config->get('sort-packages')->willReturn(true); + $config->get(Argument::any())->willReturn(null); + + $rootPackage = $this->prophesize(RootPackageInterface::class); + $rootPackage->getRequires()->willReturn([]); + $rootPackage->setRequires(Argument::that(function ($arguments) { + if (! is_array($arguments)) { + return false; + } + + if (! isset($arguments['extra-dependency-foo'])) { + return false; + } + + $argument = $arguments['extra-dependency-foo']; + + if (! $argument instanceof Link) { + return false; + } + + if ($argument->getTarget() !== 'extra-dependency-foo') { + return false; + } + + if ($argument->getConstraint()->getPrettyString() !== '^0.5.1') { + return false; + } + + if ($argument->getDescription() !== 'requires') { + return false; + } + + return true; + }))->shouldBeCalled(); + $rootPackage->getMinimumStability()->willReturn('stable'); + + $this->composer->getPackage()->willReturn($rootPackage); + $this->composer->getConfig()->willReturn($config->reveal()); + + $this->setUpComposerInstaller(['extra-dependency-foo']); + $this->setUpComposerJson(); + + $this->assertNull($this->plugin->onPostPackage($event->reveal())); + + $json = file_get_contents(vfsStream::url('project/composer.json')); + $composer = json_decode($json, true); + $this->assertTrue(isset($composer['require']['extra-dependency-foo'])); + $this->assertSame('^0.5.1', $composer['require']['extra-dependency-foo']); + } + public function testComposerInstallerFactory() { $r = new ReflectionProperty($this->plugin, 'installerFactory'); @@ -563,9 +663,6 @@ public function testComposerInstallerFactory() $this->composer->getDownloadManager() ->willReturn($this->prophesize(DownloadManager::class)) ->shouldBeCalled(); - $this->composer->getRepositoryManager() - ->willReturn($this->prophesize(RepositoryManager::class)) - ->shouldBeCalled(); $this->composer->getLocker() ->willReturn($this->prophesize(Locker::class)) ->shouldBeCalled();