diff --git a/README.md b/README.md index 07671aa..6dcd251 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,18 @@ you'd like to install with your library: "dependency": [ "package/to-require", ... - ] + ], + "dependency-or": { + "Question": [ + "package/to-choose", + "package/or-this", + ... + ] + } }, "require": { "php": "^5.6 || ^7.0", - "webimpress/composer-extra-dependency": "^0.1 || ^1.0", + "webimpress/composer-extra-dependency": "^0.3 || ^1.0", ... } ... diff --git a/src/Plugin.php b/src/Plugin.php index e8c31b1..b020657 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -21,6 +21,9 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; +use Composer\Script\Event; +use InvalidArgumentException; +use RuntimeException; class Plugin implements PluginInterface, EventSubscriberInterface { @@ -48,11 +51,16 @@ class Plugin implements PluginInterface, EventSubscriberInterface /** @var CompositeRepository */ private $repos; + /** @var string[] */ + private $packagesToInstall = []; + public static function getSubscribedEvents() { return [ 'post-package-install' => 'onPostPackage', 'post-package-update' => 'onPostPackage', + 'post-install-cmd' => 'onPostCommand', + 'post-update-cmd' => 'onPostCommand', ]; } @@ -63,7 +71,27 @@ public function activate(Composer $composer, IOInterface $io) $installedPackages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(); foreach ($installedPackages as $package) { - $this->installedPackages[$package->getName()] = $package->getPrettyVersion(); + $this->installedPackages[strtolower($package->getName())] = $package->getPrettyVersion(); + } + } + + public function onPostCommand(Event $event) + { + if (! $event->isDevMode()) { + // Do nothing in production mode. + return; + } + + if (! $this->io->isInteractive()) { + // Do nothing in no-interactive mode + return; + } + + if ($this->packagesToInstall) { + $this->updateComposerJson($this->packagesToInstall); + + $rootPackage = $this->updateRootPackage($this->composer->getPackage(), $this->packagesToInstall); + $this->runInstaller($rootPackage, array_keys($this->packagesToInstall)); } } @@ -85,36 +113,89 @@ public function onPostPackage(PackageEvent $event) } else { $package = $operation->getTargetPackage(); } - $extra = $this->getExtraMetadata($package->getExtra()); - if (empty($extra)) { - // Package does not define anything of interest; do nothing. - return; + + $extra = $package->getExtra(); + + $this->packagesToInstall += $this->andDependencies($extra); + $this->packagesToInstall += $this->orDependencies($extra); + } + + private function andDependencies(array $extra) + { + $deps = isset($extra['dependency']) && is_array($extra['dependency']) + ? $extra['dependency'] + : []; + + if (! $deps) { + // No defined any packages to install + return []; } - $packages = array_flip($extra); + $packages = array_flip($deps); foreach ($packages as $package => &$constraint) { + if ($this->isPackageReadyToInstall($package)) { + unset($packages[$package]); + continue; + } + if ($this->hasPackage($package)) { unset($packages[$package]); continue; } + // Check if package is currently installed and use installed version. + if ($constraint = $this->getInstalledPackageConstraint($package)) { + continue; + } + + // Package is not installed, then prompt user for the version. $constraint = $this->promptForPackageVersion($package); } - if ($packages) { - $this->updateComposerJson($packages); - - $rootPackage = $this->updateRootPackage($this->composer->getPackage(), $packages); - $this->runInstaller($rootPackage, array_keys($packages)); - } + return $packages; } - private function getExtraMetadata(array $extra) + private function orDependencies(array $extra) { - return isset($extra['dependency']) && is_array($extra['dependency']) - ? $extra['dependency'] + $deps = isset($extra['dependency-or']) && is_array($extra['dependency-or']) + ? $extra['dependency-or'] : []; + + if (! $deps) { + // No any dependencies to choose defined in the package. + return []; + } + + $packages = []; + foreach ($deps as $question => $options) { + if (! is_array($options) || count($options) < 2) { + throw new RuntimeException('You must provide at least two optional dependencies.'); + } + + foreach ($options as $package) { + if ($this->isPackageReadyToInstall($package)) { + // Package has been already prepared to be installed, skipping. + continue 2; + } + + if ($this->hasPackage($package)) { + // Package from this group has been found in root composer, skipping. + continue 2; + } + + // Check if package is currently installed, if so, use installed constraint and skip question. + if ($constraint = $this->getInstalledPackageConstraint($package)) { + $packages[$package] = $constraint; + continue 2; + } + } + + $package = $this->promptForPackageSelection($question, $options); + $packages[$package] = $this->promptForPackageVersion($package); + } + + return $packages; } private function updateRootPackage(RootPackageInterface $rootPackage, array $packages) @@ -157,21 +238,55 @@ private function runInstaller(RootPackageInterface $rootPackage, array $packages return $installer->run(); } - private function promptForPackageVersion($name) + private function getInstalledPackageConstraint($package) { + $lower = strtolower($package); + // 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 - )); + if (! isset($this->installedPackages[$lower])) { + return null; + } + + $constraint = '^' . $this->installedPackages[$lower]; + $this->io->write(sprintf( + 'Added package %s to composer.json with constraint %s;' + . ' to upgrade, run composer require %s:VERSION', + $package, + $constraint, + $package + )); + + return $constraint; + } - return '^' . $this->installedPackages[$name]; + private function promptForPackageSelection($question, array $packages) + { + $ask = [sprintf('%s' . "\n", $question)]; + foreach ($packages as $i => $name) { + $ask[] = sprintf(' [%d] %s' . "\n", $i + 1, $name); } + $ask[] = ' Make your selection: '; + + do { + $package = $this->io->askAndValidate( + $ask, + function ($input) use ($packages) { + $input = is_numeric($input) ? (int) trim($input) : 0; + + if (isset($packages[$input - 1])) { + return $packages[$input - 1]; + } + + return null; + } + ); + } while (! $package); + return $package; + } + + private function promptForPackageVersion($name) + { $constraint = $this->io->askAndValidate( sprintf( 'Enter the version of %s to require (or leave blank to use the latest version): ', @@ -214,10 +329,25 @@ private function createInstaller(Composer $composer, IOInterface $io, RootPackag private function hasPackage($package) { + $lower = strtolower($package); + $rootPackage = $this->composer->getPackage(); $requires = $rootPackage->getRequires() + $rootPackage->getDevRequires(); foreach ($requires as $name => $link) { - if (strtolower($name) === strtolower($package)) { + if (strtolower($name) === $lower) { + return true; + } + } + + return false; + } + + private function isPackageReadyToInstall($package) + { + $lower = strtolower($package); + + foreach ($this->packagesToInstall as $name => $version) { + if (strtolower($name) === $lower) { return true; } } @@ -282,7 +412,7 @@ private function findBestVersionForPackage($name) $package = $versionSelector->findBestCandidate($name, null, null, 'stable'); if (! $package) { - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'Could not find package %s at any version for your minimum-stability (%s).' . ' Check the package spelling or your minimum-stability', $name, diff --git a/test/PluginTest.php b/test/PluginTest.php index fff6b98..e10ee58 100644 --- a/test/PluginTest.php +++ b/test/PluginTest.php @@ -19,12 +19,15 @@ use Composer\Package\Version\VersionSelector; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; +use Composer\Script\Event; use org\bovigo\vfs\vfsStream; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use ReflectionMethod; use ReflectionProperty; +use RuntimeException; use Webimpress\ComposerExtraDependency\Plugin; class PluginTest extends TestCase @@ -62,7 +65,24 @@ protected function setUp() $this->plugin->activate($this->composer->reveal(), $this->io->reveal()); } - protected function setUpComposerInstaller(array $expectedPackages, $expectedReturn = 0) + private function setUpRootPackage( + array $dependencies = [], + array $devDependencies = [], + $minimumStability = null + ) { + $rootPackage = $this->prophesize(RootPackageInterface::class); + $rootPackage->getRequires()->willReturn($dependencies)->shouldBeCalled(); + $rootPackage->getDevRequires()->willReturn($devDependencies)->shouldBeCalled(); + if ($minimumStability) { + $rootPackage->getMinimumStability()->willReturn($minimumStability)->shouldBeCalled(); + } else { + $rootPackage->getMinimumStability()->shouldNotBeCalled(); + } + + $this->composer->getPackage()->willReturn($rootPackage->reveal())->shouldBeCalled(); + } + + private function setUpComposerInstaller(array $expectedPackages, $expectedReturn = 0) { $installer = $this->prophesize(Installer::class); $installer->setRunScripts(false)->shouldBeCalled(); @@ -78,7 +98,7 @@ protected function setUpComposerInstaller(array $expectedPackages, $expectedRetu }); } - protected function setUpVersionSelector(VersionSelector $versionSelector) + private function setUpVersionSelector(VersionSelector $versionSelector) { $r = new ReflectionProperty($this->plugin, 'versionSelectorFactory'); $r->setAccessible(true); @@ -87,7 +107,7 @@ protected function setUpVersionSelector(VersionSelector $versionSelector) }); } - protected function setUpPool() + private function setUpPool() { $pool = $this->prophesize(Pool::class); @@ -96,7 +116,7 @@ protected function setUpPool() $r->setValue($this->plugin, $pool->reveal()); } - protected function setUpComposerJson($data = null) + private function setUpComposerJson($data = null) { $project = vfsStream::setup('project'); vfsStream::newFile('composer.json') @@ -110,13 +130,13 @@ protected function setUpComposerJson($data = null) }); } - protected function createComposerJson($data) + private function createComposerJson($data) { $data = $data ?: $this->getDefaultComposerData(); return json_encode($data); } - protected function getDefaultComposerData() + private function getDefaultComposerData() { return [ 'name' => 'test/project', @@ -128,6 +148,46 @@ protected function getDefaultComposerData() ]; } + private function getCommandEvent($isDevMode = true) + { + $event = $this->prophesize(Event::class); + $event->isDevMode()->willReturn($isDevMode); + + return $event->reveal(); + } + + private function getPackageEvent( + $packageName, + array $extra, + $operationClass = InstallOperation::class + ) { + /** @var PackageInterface|ObjectProphecy $package */ + $package = $this->prophesize(PackageInterface::class); + $package->getName()->willReturn($packageName); + $package->getExtra()->willReturn($extra); + + $operation = $this->prophesize($operationClass); + if ($operationClass === InstallOperation::class) { + $operation->getPackage()->willReturn($package->reveal()); + } else { + $operation->getTargetPackage()->willReturn($package->reveal()); + } + + $event = $this->prophesize(PackageEvent::class); + $event->isDevMode()->willReturn(true); + $event->getOperation()->willReturn($operation->reveal())->shouldBeCalled(); + + return $event->reveal(); + } + + private function injectPackages(array $packagesToInstall) + { + $p = new ReflectionProperty($this->plugin, 'packagesToInstall'); + $p->setAccessible(true); + + $p->setValue($this->plugin, $packagesToInstall); + } + public function testActivateSetsComposerAndIoProperties() { $plugin = new Plugin(); @@ -142,111 +202,162 @@ public function testSubscribesToExpectedEvents() $subscribers = Plugin::getSubscribedEvents(); $this->assertArrayHasKey('post-package-install', $subscribers); $this->assertArrayHasKey('post-package-update', $subscribers); + $this->assertArrayHasKey('post-install-cmd', $subscribers); + $this->assertArrayHasKey('post-update-cmd', $subscribers); $this->assertEquals('onPostPackage', $subscribers['post-package-install']); $this->assertEquals('onPostPackage', $subscribers['post-package-update']); + $this->assertEquals('onPostCommand', $subscribers['post-install-cmd']); + $this->assertEquals('onPostCommand', $subscribers['post-update-cmd']); } - public function testDoNothingIfItIsNotInDevMode() + public function testPostPackageDoNothingInNoDevMode() { $event = $this->prophesize(PackageEvent::class); $event->isDevMode()->willReturn(false); + $event->getOperation()->shouldNotBeCalled(); $this->assertNull($this->plugin->onPostPackage($event->reveal())); } - public function testDoNothingInNoInteractionMode() + public function testPostCommandDoNothingInNoDevMode() { - /** @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()); + $this->assertNull($this->plugin->onPostCommand($this->getCommandEvent(false))); + } + public function testPostPackageDoNothingInNoInteractionMode() + { $event = $this->prophesize(PackageEvent::class); $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); - - $rootPackage = $this->prophesize(RootPackageInterface::class); - $rootPackage->getRequires()->willReturn([]); - $rootPackage->getDevRequires()->willReturn([]); - - $this->composer->getPackage()->willReturn($rootPackage); + $event->getOperation()->shouldNotBeCalled(); $this->io->isInteractive()->willReturn(false); $this->assertNull($this->plugin->onPostPackage($event->reveal())); } - public function testDoNothingWhenThereIsNoExtraDependencies() + public function testPostCommandDoNothingInNoInteractionMode() { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([]); - - $operation = $this->prophesize(InstallOperation::class); - $operation->getPackage()->willReturn($package->reveal()); + $this->injectPackages([ + 'my-package-foo' => '2.37.1', + 'other-package' => 'dev-feature/branch', + ]); - $event = $this->prophesize(PackageEvent::class); - $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); + $this->io->isInteractive()->willReturn(false); - $this->assertNull($this->plugin->onPostPackage($event->reveal())); + $this->assertNull($this->plugin->onPostCommand($this->getCommandEvent())); } - public function testDependencyAlreadyIsInRequiredSection() + public function sortPackages() { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ - 'dependency' => [ - 'extra-dependency-foo', + return [ + [ + true, + [ + 'zoo/bar' => '1.2.4', + 'foo/baz' => '2.7.3', + ], + '{"foo\/baz":"2.7.3","webimpress\/my-package":"^1.0.0-dev@dev","zoo\/bar":"1.2.4"}', ], - ]); + [ + false, + [ + 'zoo/bar' => '1.2.4', + 'foo/baz' => '2.7.3', + ], + '{"webimpress\/my-package":"^1.0.0-dev@dev","zoo\/bar":"1.2.4","foo\/baz":"2.7.3"}', + ], + ]; + } - $operation = $this->prophesize(InstallOperation::class); - $operation->getPackage()->willReturn($package->reveal()); + /** + * @dataProvider sortPackages + * + * @param bool $sortPackages + * @param array $packages + * @param string $result + */ + public function testPostCommandInstallPackagesAndUpdateComposer($sortPackages, array $packages, $result) + { + $this->injectPackages($packages); - $event = $this->prophesize(PackageEvent::class); - $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); + $this->io->isInteractive()->willReturn(true); + $this->io->write(' Updating composer.json')->shouldBeCalled(); + $this->io->write('Updating root package')->shouldBeCalled(); + $this->io->write(' Running an update to install dependent packages')->shouldBeCalled(); - $link = $this->prophesize(Link::class); - $link->getTarget()->willReturn('extra-dependency-foo'); + $config = $this->prophesize(Config::class); + $config->get('sort-packages')->willReturn($sortPackages)->shouldBeCalled(); $rootPackage = $this->prophesize(RootPackageInterface::class); - $rootPackage->getDevRequires()->willReturn(['extra-dependency-foo' => $link->reveal()]); + $rootPackage->getRequires()->willReturn([])->shouldBeCalled(); + $rootPackage->getDevRequires()->willReturn([])->shouldNotBeCalled(); + $rootPackage->setRequires(Argument::that(function (array $arguments) use ($packages) { + if (count($arguments) !== count($packages)) { + return false; + } - $this->composer->getPackage()->willReturn($rootPackage); + foreach ($packages as $package => $version) { + if (! $this->assertSetRequiresArgument($package, $version, $arguments)) { + return false; + } + } - $this->assertNull($this->plugin->onPostPackage($event->reveal())); + return true; + }))->shouldBeCalled(); + + $this->composer->getPackage()->willReturn($rootPackage)->shouldBeCalled(); + $this->composer->getConfig()->willReturn($config->reveal())->shouldBeCalled(); + + $this->setUpComposerJson(); + $this->setUpComposerInstaller(array_keys($packages)); + + $this->assertNull($this->plugin->onPostCommand($this->getCommandEvent())); + + $json = file_get_contents(vfsStream::url('project/composer.json')); + $composer = json_decode($json, true); + foreach ($packages as $package => $version) { + $this->assertTrue(isset($composer['require'][$package])); + $this->assertSame($version, $composer['require'][$package]); + } + $this->assertSame($result, json_encode($composer['require'])); } - public function testDependencyAlreadyIsInRequiredDevSection() + public function operation() { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ + return [ + 'install' => [InstallOperation::class], + 'update' => [UpdateOperation::class], + ]; + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDoNothingWhenThereIsNoExtraDependencies($operation) + { + $event = $this->getPackageEvent('some/component', [], $operation); + + $this->io->isInteractive()->willReturn(true)->shouldBeCalled(); + + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([]); + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDependencyAlreadyIsInRequireSection($operation) + { + $event = $this->getPackageEvent('some/component', [ '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()); + ], $operation); $this->io->isInteractive()->willReturn(true)->shouldBeCalled(); $this->io->askAndValidate(Argument::any())->shouldNotBeCalled(); @@ -255,109 +366,59 @@ public function testDependencyAlreadyIsInRequiredDevSection() $link->getTarget()->willReturn('extra-dependency-foo'); $rootPackage = $this->prophesize(RootPackageInterface::class); - $rootPackage->getRequires()->willReturn([]); - $rootPackage->getDevRequires()->willReturn(['extra-dependency-foo' => $link->reveal()]); + $rootPackage->getRequires()->willReturn(['extra-dependency-foo' => $link->reveal()])->shouldBeCalled(); + $rootPackage->getDevRequires()->willReturn([])->shouldBeCalled(); $this->composer->getPackage()->willReturn($rootPackage); - $this->assertNull($this->plugin->onPostPackage($event->reveal())); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([]); } - public function testInstallSingleDependencyOnPackageUpdate() + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDependencyAlreadyIsInRequireDevSection($operation) { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ + $event = $this->getPackageEvent('some/component', [ 'dependency' => [ 'extra-dependency-foo', ], - ]); - - $operation = $this->prophesize(UpdateOperation::class); - $operation->getTargetPackage()->willReturn($package->reveal()); - - $event = $this->prophesize(PackageEvent::class); - $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); + ], $operation); $this->io->isInteractive()->willReturn(true)->shouldBeCalled(); $this->io->askAndValidate(Argument::any())->shouldNotBeCalled(); - $config = $this->prophesize(Config::class); - $config->get('sort-packages')->willReturn(true); - $config->get(Argument::any())->willReturn(null); + $link = $this->prophesize(Link::class); + $link->getTarget()->willReturn('extra-dependency-foo'); $rootPackage = $this->prophesize(RootPackageInterface::class); $rootPackage->getRequires()->willReturn([]); - $rootPackage->getDevRequires()->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() !== '17.0.1-dev') { - return false; - } - - if ($argument->getDescription() !== 'requires') { - return false; - } - - return true; - }))->shouldBeCalled(); + $rootPackage->getDevRequires()->willReturn(['extra-dependency-foo' => $link->reveal()]); $this->composer->getPackage()->willReturn($rootPackage); - $this->composer->getConfig()->willReturn($config->reveal()); - $this->io->isInteractive()->willReturn(true); - $this->io->askAndValidate( - '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'); - - $this->io->write(' Updating composer.json')->shouldBeCalled(); - $this->io->write('Updating root package')->shouldBeCalled(); - $this->io->write(' Running an update to install dependent packages')->shouldBeCalled(); - - $this->setUpComposerInstaller(['extra-dependency-foo']); - $this->setUpComposerJson(); - - $this->assertNull($this->plugin->onPostPackage($event->reveal())); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([]); } - public function testInstallSingleDependencyOnPackageInstall() + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testInstallSingleDependency($operation) { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ + $event = $this->getPackageEvent('some/component', [ 'dependency' => [ 'extra-dependency-foo', ], - ]); - - $operation = $this->prophesize(InstallOperation::class); - $operation->getPackage()->willReturn($package->reveal()); + ], $operation); - $event = $this->prophesize(PackageEvent::class); - $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); + $this->io->isInteractive()->willReturn(true)->shouldBeCalled(); + $this->io->askAndValidate(Argument::any())->shouldNotBeCalled(); $config = $this->prophesize(Config::class); $config->get('sort-packages')->willReturn(true); @@ -366,35 +427,6 @@ public function testInstallSingleDependencyOnPackageInstall() $rootPackage = $this->prophesize(RootPackageInterface::class); $rootPackage->getRequires()->willReturn([]); $rootPackage->getDevRequires()->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() !== '17.0.1-dev') { - return false; - } - - if ($argument->getDescription() !== 'requires') { - return false; - } - - return true; - }))->shouldBeCalled(); $this->composer->getPackage()->willReturn($rootPackage); $this->composer->getConfig()->willReturn($config->reveal()); @@ -403,37 +435,40 @@ public function testInstallSingleDependencyOnPackageInstall() $this->io->askAndValidate( 'Enter the version of extra-dependency-foo to require' . ' (or leave blank to use the latest version): ', - Argument::type('callable') + Argument::that(function ($arg) { + if (! is_callable($arg)) { + return false; + } + + Assert::assertFalse($arg(0)); + Assert::assertFalse($arg('0')); + Assert::assertFalse($arg('')); + Assert::assertFalse($arg(' ')); + Assert::assertSame('1', $arg(' 1 ')); + Assert::assertSame('1', $arg('1')); + Assert::assertSame('0.*', $arg('0.*')); + + return true; + }) )->willReturn('17.0.1-dev'); - $this->io->write(' Updating composer.json')->shouldBeCalled(); - $this->io->write('Updating root package')->shouldBeCalled(); - $this->io->write(' Running an update to install dependent packages')->shouldBeCalled(); - - $this->setUpComposerInstaller(['extra-dependency-foo']); - $this->setUpComposerJson(); - - $this->assertNull($this->plugin->onPostPackage($event->reveal())); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall(['extra-dependency-foo' => '17.0.1-dev']); } - public function testInstallOneDependenciesWhenOneIsAlreadyInstalled() + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testInstallOneDependenciesWhenOneIsAlreadyInstalled($operation) { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ + $event = $this->getPackageEvent('some/component', [ 'dependency' => [ 'extra-dependency-foo', 'extra-dependency-bar', ], - ]); - - $operation = $this->prophesize(InstallOperation::class); - $operation->getPackage()->willReturn($package->reveal()); - - $event = $this->prophesize(PackageEvent::class); - $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); + ], $operation); $config = $this->prophesize(Config::class); $config->get('sort-packages')->willReturn(true); @@ -445,39 +480,6 @@ public function testInstallOneDependenciesWhenOneIsAlreadyInstalled() $rootPackage = $this->prophesize(RootPackageInterface::class); $rootPackage->getRequires()->willReturn(['extra-dependency-bar' => $link->reveal()]); $rootPackage->getDevRequires()->willReturn([]); - $rootPackage->setRequires(Argument::that(function ($arguments) { - if (! is_array($arguments)) { - return false; - } - - if (! isset($arguments['extra-dependency-foo'])) { - return false; - } - - if (! isset($arguments['extra-dependency-bar'])) { - 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() !== '17.0.1-dev') { - return false; - } - - if ($argument->getDescription() !== 'requires') { - return false; - } - - return true; - }))->shouldBeCalled(); $this->composer->getPackage()->willReturn($rootPackage); $this->composer->getConfig()->willReturn($config->reveal()); @@ -489,79 +491,24 @@ public function testInstallOneDependenciesWhenOneIsAlreadyInstalled() Argument::type('callable') )->willReturn('17.0.1-dev'); - $this->io->write(' Updating composer.json')->shouldBeCalled(); - $this->io->write('Updating root package')->shouldBeCalled(); - $this->io->write(' Running an update to install dependent packages')->shouldBeCalled(); - - $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('17.0.1-dev', $composer['require']['extra-dependency-foo']); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall(['extra-dependency-foo' => '17.0.1-dev']); } - public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersion() + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersion($operation) { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ + $event = $this->getPackageEvent('some/component', [ '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->getDevRequires()->willReturn([]); - $rootPackage->setRequires(Argument::that(function ($arguments) { - if (! is_array($arguments)) { - return false; - } - - if (! isset($arguments['extra-dependency-foo'])) { - return false; - } + ], $operation); - $argument = $arguments['extra-dependency-foo']; - - if (! $argument instanceof Link) { - return false; - } - - if ($argument->getTarget() !== 'extra-dependency-foo') { - return false; - } - - if ($argument->getConstraint()->getPrettyString() !== '13.4.2') { - 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->setUpRootPackage(); $this->io->isInteractive()->willReturn(true); $this->io->askAndValidate( @@ -570,14 +517,8 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersion() Argument::type('callable') )->willReturn(false); - $this->io->write(' Updating composer.json')->shouldBeCalled(); - $this->io->write('Updating root package')->shouldBeCalled(); - $this->io->write(' Running an update to install dependent packages')->shouldBeCalled(); $this->io->write('Using version 13.4.2 for extra-dependency-foo')->shouldBeCalled(); - $this->setUpComposerInstaller(['extra-dependency-foo']); - $this->setUpComposerJson(); - $package = $this->prophesize(PackageInterface::class); $versionSelector = $this->prophesize(VersionSelector::class); @@ -591,43 +532,24 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersion() $this->setUpVersionSelector($versionSelector->reveal()); $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']); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall(['extra-dependency-foo' => '13.4.2']); } - public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersionNotFoundMatchingPackage() + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersionNotFoundMatchingPackage($operation) { - /** @var PackageInterface|ObjectProphecy $package */ - $package = $this->prophesize(PackageInterface::class); - $package->getName()->willReturn('some/component'); - $package->getExtra()->willReturn([ + $event = $this->getPackageEvent('some/component', [ 'dependency' => [ 'extra-dependency-foo', ], - ]); + ], $operation); - $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->getDevRequires()->willReturn([]); - $rootPackage->getMinimumStability()->willReturn('stable'); - - $this->composer->getPackage()->willReturn($rootPackage); - $this->composer->getConfig()->willReturn($config->reveal()); + $this->setUpRootPackage([], [], 'stable-foo'); $this->io->isInteractive()->willReturn(true); $this->io->askAndValidate( @@ -636,22 +558,27 @@ public function testInstallSingleDependencyAndAutomaticallyChooseLatestVersionNo Argument::type('callable') )->willReturn(false); - $this->setUpComposerJson(); - $versionSelector = $this->prophesize(VersionSelector::class); - $versionSelector->findBestCandidate('extra-dependency-foo', null, null, 'stable')->willReturn(null); + $versionSelector->findBestCandidate('extra-dependency-foo', null, null, 'stable') + ->willReturn(null) + ->shouldBeCalledTimes(1); $this->setUpVersionSelector($versionSelector->reveal()); $this->setUpPool(); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage( - 'Could not find package extra-dependency-foo at any version for your minimum-stability' + 'Could not find package extra-dependency-foo at any version for your minimum-stability (stable-foo)' ); - $this->plugin->onPostPackage($event->reveal()); + $this->plugin->onPostPackage($event); } - public function testUpdateComposerWithCurrentlyInstalledVersion() + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testUpdateComposerWithCurrentlyInstalledVersion($operation) { $installedPackage = $this->prophesize(PackageInterface::class); $installedPackage->getName()->willReturn('extra-dependency-foo'); @@ -660,83 +587,401 @@ public function testUpdateComposerWithCurrentlyInstalledVersion() $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([ + $event = $this->getPackageEvent('some/component', [ 'dependency' => [ 'extra-dependency-foo', ], + ], $operation); + + $this->setUpRootPackage(); + + $this->io->isInteractive()->willReturn(true); + $this->io + ->write( + 'Added package extra-dependency-foo to composer.json with constraint' + . ' ^0.5.1; to upgrade, run composer require extra-dependency-foo:VERSION' + ) + ->shouldBeCalled(); + + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall(['extra-dependency-foo' => '^0.5.1']); + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDoNotInstallAskTwiceForTheSamePackage($operation) + { + $this->injectPackages([ + 'extra-package' => '^1.0.1', ]); - $operation = $this->prophesize(InstallOperation::class); - $operation->getPackage()->willReturn($package->reveal()); + $event = $this->getPackageEvent('some/component', [ + 'dependency' => [ + 'extra-package', + ], + ], $operation); - $event = $this->prophesize(PackageEvent::class); - $event->isDevMode()->willReturn(true); - $event->getOperation()->willReturn($operation->reveal()); + $this->composer->getPackage()->shouldNotBeCalled(); - $config = $this->prophesize(Config::class); - $config->get('sort-packages')->willReturn(true); - $config->get(Argument::any())->willReturn(null); + $this->io->isInteractive()->willReturn(true); + $this->io + ->askAndValidate(Argument::any(), Argument::type('callable')) + ->shouldNotBeCalled(); - $rootPackage = $this->prophesize(RootPackageInterface::class); - $rootPackage->getRequires()->willReturn([]); - $rootPackage->getDevRequires()->willReturn([]); - $rootPackage->setRequires(Argument::that(function ($arguments) { - if (! is_array($arguments)) { - return false; - } + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([ + 'extra-package' => '^1.0.1', + ]); + } - if (! isset($arguments['extra-dependency-foo'])) { - return false; - } + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDependencyOrChoosePackageToInstall($operation) + { + $event = $this->getPackageEvent('some/component', [ + 'dependency-or' => [ + 'My question foo bar baz' => [ + 'extra-dependency-foo', + 'extra-dependency-bar', + ], + ], + ], $operation); - $argument = $arguments['extra-dependency-foo']; + $this->setUpRootPackage(); - if (! $argument instanceof Link) { - return false; - } + $this->io->isInteractive()->willReturn(true); + $this->io + ->askAndValidate( + Argument::that(function ($arg) { + if (! is_array($arg)) { + return false; + } + + Assert::assertCount(4, $arg); + Assert::assertSame('My question foo bar baz' . "\n", $arg[0]); + Assert::assertSame(' [1] extra-dependency-foo' . "\n", $arg[1]); + Assert::assertSame(' [2] extra-dependency-bar' . "\n", $arg[2]); + Assert::assertSame(' Make your selection: ', $arg[3]); + + return true; + }), + Argument::that(function ($arg) { + if (! is_callable($arg)) { + return false; + } + + Assert::assertSame('extra-dependency-foo', $arg(1)); + Assert::assertSame('extra-dependency-foo', $arg('1')); + Assert::assertSame('extra-dependency-foo', $arg(' 1')); + Assert::assertSame('extra-dependency-foo', $arg('1.0')); + Assert::assertSame('extra-dependency-bar', $arg(2)); + Assert::assertSame('extra-dependency-bar', $arg('2')); + Assert::assertSame('extra-dependency-bar', $arg(' 2')); + Assert::assertSame('extra-dependency-bar', $arg('2.2')); + Assert::assertNull($arg('')); + Assert::assertNull($arg(' ')); + Assert::assertNull($arg('a')); + Assert::assertNull($arg('1a')); + Assert::assertNull($arg(' a')); + Assert::assertNull($arg(0)); + Assert::assertNull($arg(3)); + + return true; + }) + ) + ->willReturn('', 'extra-dependency-bar') + ->shouldBeCalledTimes(2); + $this->io + ->askAndValidate( + 'Enter the version of extra-dependency-bar to require' + . ' (or leave blank to use the latest version): ', + Argument::type('callable') + ) + ->willReturn(false) + ->shouldBeCalledTimes(1); - if ($argument->getTarget() !== 'extra-dependency-foo') { - return false; - } + $this->io->write('Using version 13.4.2 for extra-dependency-bar')->shouldBeCalled(); - if ($argument->getConstraint()->getPrettyString() !== '^0.5.1') { - return false; - } + $package = $this->prophesize(PackageInterface::class); - if ($argument->getDescription() !== 'requires') { - return false; - } + $versionSelector = $this->prophesize(VersionSelector::class); + $versionSelector->findBestCandidate('extra-dependency-bar', null, null, 'stable') + ->willReturn($package->reveal()) + ->shouldBeCalled(); + $versionSelector->findRecommendedRequireVersion($package->reveal()) + ->willReturn('13.4.2') + ->shouldBeCalled(); - return true; - }))->shouldBeCalled(); - $rootPackage->getMinimumStability()->willReturn('stable'); + $this->setUpVersionSelector($versionSelector->reveal()); + $this->setUpPool(); - $this->composer->getPackage()->willReturn($rootPackage); - $this->composer->getConfig()->willReturn($config->reveal()); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall(['extra-dependency-bar' => '13.4.2']); + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDependencyOrOnePackageIsAlreadyInstalledAndShouldBeAddedIntoRootComposer($operation) + { + $installedPackage = $this->prophesize(PackageInterface::class); + $installedPackage->getName()->willReturn('extra-dependency-baz'); + $installedPackage->getPrettyVersion()->willReturn('3.7.1'); + + $this->localRepository->getPackages()->willReturn([$installedPackage->reveal()]); + $this->plugin->activate($this->composer->reveal(), $this->io->reveal()); + + $event = $this->getPackageEvent('some/component', [ + 'dependency-or' => [ + 'Choose something' => [ + 'extra-dependency-bar', + 'extra-dependency-baz', + ], + ], + ], $operation); + + $this->setUpRootPackage(); $this->io->isInteractive()->willReturn(true); $this->io ->write( - 'Added package extra-dependency-foo to composer.json with constraint' - . ' ^0.5.1; to upgrade, run composer require extra-dependency-foo:VERSION' + 'Added package extra-dependency-baz to composer.json with constraint' + . ' ^3.7.1; to upgrade, run composer require extra-dependency-baz:VERSION' ) ->shouldBeCalled(); - $this->io->write(' Updating composer.json')->shouldBeCalled(); - $this->io->write('Updating root package')->shouldBeCalled(); - $this->io->write(' Running an update to install dependent packages')->shouldBeCalled(); + $this->io + ->askAndValidate(Argument::any(), Argument::any()) + ->shouldNotBeCalled(); - $this->setUpComposerInstaller(['extra-dependency-foo']); - $this->setUpComposerJson(); + $this->setUpPool(); - $this->assertNull($this->plugin->onPostPackage($event->reveal())); + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall(['extra-dependency-baz' => '^3.7.1']); + } - $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']); + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDependencyOrOnePackageIsAlreadyInRootComposer($operation) + { + $event = $this->getPackageEvent('some/component', [ + 'dependency-or' => [ + 'Choose something' => [ + 'extra-dependency-foo', + 'extra-dependency-baz', + ], + ], + ], $operation); + + $link = $this->prophesize(Link::class); + $link->getTarget()->willReturn('extra-dependency-bar'); + + $this->setUpRootPackage(['extra-dependency-foo' => $link]); + + $this->io->isInteractive()->willReturn(true)->shouldBeCalledTimes(1); + $this->io + ->askAndValidate(Argument::any(), Argument::any()) + ->shouldNotBeCalled(); + + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([]); + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testDependencyOrWrongDefinitionThrowsException($operation) + { + $event = $this->getPackageEvent('some/component', [ + 'dependency-or' => [ + 'extra-dependency-foo', + 'extra-dependency-baz', + ], + ], $operation); + + $this->io->isInteractive()->willReturn(true)->shouldBeCalledTimes(1); + $this->io + ->askAndValidate(Argument::any(), Argument::any()) + ->shouldNotBeCalled(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('You must provide at least two optional dependencies.'); + $this->assertNull($this->plugin->onPostPackage($event)); + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testIntegrationHandleDependencyAndDependencyOr($operation) + { + $event = $this->getPackageEvent('some/component', [ + 'dependency' => [ + 'extra-package-required', + ], + 'dependency-or' => [ + 'Choose something' => [ + 'extra-choose-one', + 'extra-choose-two', + 'extra-choose-three', + ], + ], + ], $operation); + + $this->setUpRootPackage(); + + $this->io->isInteractive()->willReturn(true); + $this->io + ->askAndValidate( + Argument::that(function ($arg) { + if (! is_array($arg)) { + return false; + } + + Assert::assertCount(5, $arg); + Assert::assertSame('Choose something' . "\n", $arg[0]); + Assert::assertSame(' [1] extra-choose-one' . "\n", $arg[1]); + Assert::assertSame(' [2] extra-choose-two' . "\n", $arg[2]); + Assert::assertSame(' [3] extra-choose-three' . "\n", $arg[3]); + Assert::assertSame(' Make your selection: ', $arg[4]); + + return true; + }), + Argument::type('callable') + ) + ->willReturn('extra-choose-two') + ->shouldBeCalledTimes(1); + $this->io + ->askAndValidate( + 'Enter the version of extra-package-required to require' + . ' (or leave blank to use the latest version): ', + Argument::type('callable') + ) + ->willReturn(false) + ->shouldBeCalledTimes(1); + $this->io + ->askAndValidate( + 'Enter the version of extra-choose-two to require' + . ' (or leave blank to use the latest version): ', + Argument::type('callable') + ) + ->willReturn(false) + ->shouldBeCalledTimes(1); + + $this->io->write('Using version 3.9.1 for extra-package-required')->shouldBeCalled(); + $this->io->write('Using version 2.1.5 for extra-choose-two')->shouldBeCalled(); + + $package1 = $this->prophesize(PackageInterface::class)->reveal(); + $package2 = $this->prophesize(PackageInterface::class)->reveal(); + + $versionSelector = $this->prophesize(VersionSelector::class); + $versionSelector->findBestCandidate('extra-package-required', null, null, 'stable') + ->willReturn($package1) + ->shouldBeCalledTimes(1); + $versionSelector->findBestCandidate('extra-choose-two', null, null, 'stable') + ->willReturn($package2) + ->shouldBeCalledTimes(1); + $versionSelector->findRecommendedRequireVersion($package1) + ->willReturn('3.9.1') + ->shouldBeCalledTimes(1); + $versionSelector->findRecommendedRequireVersion($package2) + ->willReturn('2.1.5') + ->shouldBeCalledTimes(1); + + $this->setUpVersionSelector($versionSelector->reveal()); + $this->setUpPool(); + + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([ + 'extra-package-required' => '3.9.1', + 'extra-choose-two' => '2.1.5', + ]); + } + + /** + * @dataProvider operation + * + * @param string $operation + */ + public function testIntegrationDoNotAskWhenAlreadyChosen($operation) + { + $event = $this->getPackageEvent('some/component', [ + 'dependency' => [ + 'extra-package-required', + ], + 'dependency-or' => [ + 'Choose something' => [ + 'extra-choose-one', + 'extra-choose-two', + 'extra-choose-three', + 'extra-package-required', + ], + ], + ], $operation); + + $this->setUpRootPackage(); + + $this->io->isInteractive()->willReturn(true); + $this->io + ->askAndValidate( + 'Enter the version of extra-package-required to require' + . ' (or leave blank to use the latest version): ', + Argument::type('callable') + ) + ->willReturn('1.8.3') + ->shouldBeCalledTimes(1); + + $this->assertNull($this->plugin->onPostPackage($event)); + $this->assertPackagesToInstall([ + 'extra-package-required' => '1.8.3', + ]); + } + + private function assertSetRequiresArgument($name, $version, array $arguments) + { + if (! isset($arguments[$name])) { + return false; + } + + $argument = $arguments[$name]; + + if (! $argument instanceof Link) { + return false; + } + + if ($argument->getTarget() !== $name) { + return false; + } + + if ($argument->getConstraint()->getPrettyString() !== $version) { + return false; + } + + if ($argument->getDescription() !== 'requires') { + return false; + } + + return true; + } + + private function assertPackagesToInstall(array $packagesToInstall) + { + $p = new ReflectionProperty($this->plugin, 'packagesToInstall'); + $p->setAccessible(true); + self::assertSame($packagesToInstall, $p->getValue($this->plugin)); } public function testComposerInstallerFactory()