From cc389d6c1a42a343a1d835a0e049d5f04ccf4bc9 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Wed, 2 Mar 2016 23:13:06 +0200 Subject: [PATCH] List project suggestions in create-project command Resolves https://github.com/composer/composer/issues/2752 --- src/Composer/Command/CreateProjectCommand.php | 14 +- src/Composer/Installer.php | 40 +-- .../Installer/SuggestedPackagesReporter.php | 126 ++++++++++ .../SuggestedPackagesReporterTest.php | 227 ++++++++++++++++++ 4 files changed, 386 insertions(+), 21 deletions(-) create mode 100644 src/Composer/Installer/SuggestedPackagesReporter.php create mode 100644 tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index e74e2e56f..2755f9913 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -17,6 +17,7 @@ use Composer\Factory; use Composer\Installer; use Composer\Installer\ProjectInstaller; use Composer\Installer\InstallationManager; +use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\DependencyResolver\Pool; @@ -47,6 +48,11 @@ use Composer\Package\Version\VersionParser; */ class CreateProjectCommand extends BaseCommand { + /** + * @var SuggestedPackagesReporter + */ + protected $suggestedPackagesReporter; + protected function configure() { $this @@ -142,6 +148,8 @@ EOT // we need to manually load the configuration to pass the auth credentials to the io interface! $io->loadConfiguration($config); + $this->suggestedPackagesReporter = new SuggestedPackagesReporter($io); + if ($packageName !== null) { $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repository, $disablePlugins, $noScripts, $keepVcs, $noProgress); } else { @@ -168,7 +176,8 @@ EOT ->setPreferDist($preferDist) ->setDevMode($installDevPackages) ->setRunScripts(!$noScripts) - ->setIgnorePlatformRequirements($ignorePlatformReqs); + ->setIgnorePlatformRequirements($ignorePlatformReqs) + ->setSuggestedPackagesReporter($this->suggestedPackagesReporter); if ($disablePlugins) { $installer->disablePlugins(); @@ -318,6 +327,9 @@ EOT $im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package)); $im->notifyInstalls($io); + // collect suggestions + $this->suggestedPackagesReporter->addSuggestionsFromPackage($package); + $installedFromVcs = 'source' === $package->getInstallationSource(); $io->writeError('Created project in ' . $directory . ''); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index ace3fa17b..0676c91c6 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -29,6 +29,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallationManager; use Composer\Installer\InstallerEvents; use Composer\Installer\NoopInstaller; +use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\CompletePackage; @@ -120,9 +121,9 @@ class Installer protected $whitelistDependencies = false; /** - * @var array + * @var SuggestedPackagesReporter */ - protected $suggestedPackages; + protected $suggestedPackagesReporter; /** * @var RepositoryInterface @@ -214,8 +215,11 @@ class Installer $aliases = $this->getRootAliases(); $this->aliasPlatformPackages($platformRepo, $aliases); + if (!$this->suggestedPackagesReporter) { + $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); + } + try { - $this->suggestedPackages = array(); $res = $this->doInstall($localRepo, $installedRepo, $platformRepo, $aliases, $this->devMode); if ($res !== 0) { return $res; @@ -233,16 +237,7 @@ class Installer // output suggestions if we're in dev mode if ($this->devMode) { - foreach ($this->suggestedPackages as $suggestion) { - $target = $suggestion['target']; - foreach ($installedRepo->getPackages() as $package) { - if (in_array($target, $package->getNames())) { - continue 2; - } - } - - $this->io->writeError($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')'); - } + $this->suggestedPackagesReporter->output($installedRepo); } # Find abandoned packages and warn user @@ -538,13 +533,7 @@ class Installer foreach ($operations as $operation) { // collect suggestions if ('install' === $operation->getJobType()) { - foreach ($operation->getPackage()->getSuggests() as $target => $reason) { - $this->suggestedPackages[] = array( - 'source' => $operation->getPackage()->getPrettyName(), - 'target' => $target, - 'reason' => $reason, - ); - } + $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); } // not installing from lock, force dev packages' references if they're in root package refs @@ -1508,4 +1497,15 @@ class Installer return $this; } + + /** + * @param SuggestedPackagesReporter $suggestedPackagesReporter + * @return Installer + */ + public function setSuggestedPackagesReporter(SuggestedPackagesReporter $suggestedPackagesReporter) + { + $this->suggestedPackagesReporter = $suggestedPackagesReporter; + + return $this; + } } diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php new file mode 100644 index 000000000..d56628ceb --- /dev/null +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -0,0 +1,126 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Installer; + +use Composer\IO\IOInterface; +use Composer\Package\PackageInterface; +use Composer\Repository\RepositoryInterface; + +/** + * Add suggested packages from different places to output them in the end. + * + * @author Haralan Dobrev + */ +class SuggestedPackagesReporter +{ + /** + * @var array + */ + protected $suggestedPackages = array(); + + /** + * @var Composer\IO\IOInterface + */ + private $io; + + public function __construct(IOInterface $io) + { + $this->io = $io; + } + + /** + * @return array Suggested packages with source, target and reason keys. + */ + public function getPackages() + { + return $this->suggestedPackages; + } + + /** + * Add suggested packages to be listed after install + * + * Could be used to add suggested packages both from the installer + * or from CreateProjectCommand. + * + * @param string $source Source package which made the suggestion + * @param string $target Target package to be suggested + * @param string $reason Reason the target package to be suggested + * @return SuggestedPackagesReporter + */ + public function addPackage($source, $target, $reason) + { + $this->suggestedPackages[] = array( + 'source' => $source, + 'target' => $target, + 'reason' => $reason, + ); + + return $this; + } + + /** + * Add all suggestions from a package. + * + * @param PackageInterface $package + * @return SuggestedPackagesReporter + */ + public function addSuggestionsFromPackage(PackageInterface $package) + { + $source = $package->getPrettyName(); + foreach ($package->getSuggests() as $target => $reason) { + $this->addPackage( + $source, + $target, + $reason + ); + } + + return $this; + } + + /** + * Output suggested packages. + * Do not list the ones already installed if installed repository provided. + * + * @param RepositoryInterface $installedRepo Installed packages + * @return SuggestedPackagesReporter + */ + public function output(RepositoryInterface $installedRepo = null) + { + $suggestedPackages = $this->getPackages(); + $installedPackages = array(); + if (null !== $installedRepo && ! empty($suggestedPackages)) { + foreach ($installedRepo->getPackages() as $package) { + $installedPackages = array_merge( + $installedPackages, + $package->getNames() + ); + } + } + + foreach ($suggestedPackages as $suggestion) { + if (in_array($suggestion['target'], $installedPackages)) { + continue; + } + + $this->io->writeError(sprintf( + '%s suggests installing %s (%s)', + $suggestion['source'], + $suggestion['target'], + $suggestion['reason'] + )); + } + + return $this; + } +} diff --git a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php new file mode 100644 index 000000000..c4cbd72c5 --- /dev/null +++ b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php @@ -0,0 +1,227 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Installer; + +use Composer\Installer\SuggestedPackagesReporter; + +/** + * @coversDefaultClass Composer\Installer\SuggestedPackagesReporter + */ +class SuggestedPackagesReporterTest extends \PHPUnit_Framework_TestCase +{ + private $io; + private $suggestedPackagesReporter; + + protected function setUp() + { + $this->io = $this->getMock('Composer\IO\IOInterface'); + + $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); + } + + /** + * @covers ::__construct + */ + public function testContrsuctor() + { + $this->io->expects($this->once()) + ->method('writeError'); + + $suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); + $suggestedPackagesReporter->addPackage('a', 'b', 'c'); + $suggestedPackagesReporter->output(); + } + + /** + * @covers ::getPackages + */ + public function testGetPackagesEmptyByDefault() + { + $this->assertSame( + array(), + $this->suggestedPackagesReporter->getPackages() + ); + } + + /** + * @covers ::getPackages + * @covers ::addPackage + */ + public function testGetPackages() + { + $suggestedPackage = $this->getSuggestedPackageArray(); + $this->suggestedPackagesReporter->addPackage( + $suggestedPackage['source'], + $suggestedPackage['target'], + $suggestedPackage['reason'] + ); + $this->assertSame( + array($suggestedPackage), + $this->suggestedPackagesReporter->getPackages() + ); + } + + /** + * Test addPackage appends packages. + * Also test targets can be duplicated. + * + * @covers ::addPackage + */ + public function testAddPackageAppends() + { + $suggestedPackageA = $this->getSuggestedPackageArray(); + $suggestedPackageB = $this->getSuggestedPackageArray(); + $suggestedPackageB['source'] = 'different source'; + $suggestedPackageB['reason'] = 'different reason'; + $this->suggestedPackagesReporter->addPackage( + $suggestedPackageA['source'], + $suggestedPackageA['target'], + $suggestedPackageA['reason'] + ); + $this->suggestedPackagesReporter->addPackage( + $suggestedPackageB['source'], + $suggestedPackageB['target'], + $suggestedPackageB['reason'] + ); + $this->assertSame( + array($suggestedPackageA, $suggestedPackageB), + $this->suggestedPackagesReporter->getPackages() + ); + } + + /** + * @covers ::addSuggestionsFromPackage + */ + public function testAddSuggestionsFromPackage() + { + $package = $this->createPackageMock(); + $package->expects($this->once()) + ->method('getSuggests') + ->will($this->returnValue(array( + 'target-a' => 'reason-a', + 'target-b' => 'reason-b', + ))); + $package->expects($this->once()) + ->method('getPrettyName') + ->will($this->returnValue('package-pretty-name')); + + $this->suggestedPackagesReporter->addSuggestionsFromPackage($package); + $this->assertSame(array( + array( + 'source' => 'package-pretty-name', + 'target' => 'target-a', + 'reason' => 'reason-a', + ), + array( + 'source' => 'package-pretty-name', + 'target' => 'target-b', + 'reason' => 'reason-b', + ), + ), $this->suggestedPackagesReporter->getPackages()); + } + + /** + * @covers ::output + */ + public function testOutput() + { + $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); + + $this->io->expects($this->once()) + ->method('writeError') + ->with('a suggests installing b (c)'); + + $this->suggestedPackagesReporter->output(); + } + + /** + * @covers ::output + */ + public function testOutputMultiplePackages() + { + $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); + $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); + + $this->io->expects($this->at(0)) + ->method('writeError') + ->with('a suggests installing b (c)'); + + $this->io->expects($this->at(1)) + ->method('writeError') + ->with('source package suggests installing target (because reasons)'); + + $this->suggestedPackagesReporter->output(); + } + + /** + * @covers ::output + */ + public function testOutputSkipInstalledPackages() + { + $repository = $this->getMock('Composer\Repository\RepositoryInterface'); + $package1 = $this->getMock('Composer\Package\PackageInterface'); + $package2 = $this->getMock('Composer\Package\PackageInterface'); + + $package1->expects($this->once()) + ->method('getNames') + ->will($this->returnValue(array('x', 'y'))); + + $package2->expects($this->once()) + ->method('getNames') + ->will($this->returnValue(array('b'))); + + $repository->expects($this->once()) + ->method('getPackages') + ->will($this->returnValue(array( + $package1, + $package2, + ))); + + $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); + $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); + + $this->io->expects($this->once()) + ->method('writeError') + ->with('source package suggests installing target (because reasons)'); + + $this->suggestedPackagesReporter->output($repository); + } + + /** + * @covers ::output + */ + public function testOutputNotGettingInstalledPackagesWhenNoSuggestions() + { + $repository = $this->getMock('Composer\Repository\RepositoryInterface'); + $repository->expects($this->exactly(0)) + ->method('getPackages'); + + $this->suggestedPackagesReporter->output($repository); + } + + private function getSuggestedPackageArray() + { + return array( + 'source' => 'a', + 'target' => 'b', + 'reason' => 'c', + ); + } + + private function createPackageMock() + { + return $this->getMockBuilder('Composer\Package\Package') + ->setConstructorArgs(array(md5(mt_rand()), '1.0.0.0', '1.0.0')) + ->getMock(); + } +}