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();
+ }
+}