* 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\Pcre\Preg; use Composer\Repository\InstalledRepository; use Symfony\Component\Console\Formatter\OutputFormatter; /** * Add suggested packages from different places to output them in the end. * * @author Haralan Dobrev */ class SuggestedPackagesReporter { public const MODE_LIST = 1; public const MODE_BY_PACKAGE = 2; public const MODE_BY_SUGGESTION = 4; /** * @var array */ protected $suggestedPackages = []; /** * @var IOInterface */ private $io; public function __construct(IOInterface $io) { $this->io = $io; } /** * @return array Suggested packages with source, target and reason keys. */ public function getPackages(): array { 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 */ public function addPackage(string $source, string $target, string $reason): SuggestedPackagesReporter { $this->suggestedPackages[] = [ 'source' => $source, 'target' => $target, 'reason' => $reason, ]; return $this; } /** * Add all suggestions from a package. */ public function addSuggestionsFromPackage(PackageInterface $package): SuggestedPackagesReporter { $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 int $mode One of the MODE_* constants from this class * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown */ public function output(int $mode, ?InstalledRepository $installedRepo = null, ?PackageInterface $onlyDependentsOf = null): void { $suggestedPackages = $this->getFilteredSuggestions($installedRepo, $onlyDependentsOf); $suggesters = []; $suggested = []; foreach ($suggestedPackages as $suggestion) { $suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason']; $suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason']; } ksort($suggesters); ksort($suggested); // Simple mode if ($mode & self::MODE_LIST) { foreach (array_keys($suggested) as $name) { $this->io->write(sprintf('%s', $name)); } return; } // Grouped by package if ($mode & self::MODE_BY_PACKAGE) { foreach ($suggesters as $suggester => $suggestions) { $this->io->write(sprintf('%s suggests:', $suggester)); foreach ($suggestions as $suggestion => $reason) { $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason))); } $this->io->write(''); } } // Grouped by suggestion if ($mode & self::MODE_BY_SUGGESTION) { // Improve readability in full mode if ($mode & self::MODE_BY_PACKAGE) { $this->io->write(str_repeat('-', 78)); } foreach ($suggested as $suggestion => $suggesters) { $this->io->write(sprintf('%s is suggested by:', $suggestion)); foreach ($suggesters as $suggester => $reason) { $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason))); } $this->io->write(''); } } if ($onlyDependentsOf) { $allSuggestedPackages = $this->getFilteredSuggestions($installedRepo); $diff = count($allSuggestedPackages) - count($suggestedPackages); if ($diff) { $this->io->write(''.$diff.' additional suggestions by transitive dependencies can be shown with --all'); } } } /** * Output number of new suggested packages and a hint to use suggest command. * * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown */ public function outputMinimalistic(?InstalledRepository $installedRepo = null, ?PackageInterface $onlyDependentsOf = null): void { $suggestedPackages = $this->getFilteredSuggestions($installedRepo, $onlyDependentsOf); if ($suggestedPackages) { $this->io->writeError(''.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.'); } } /** * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown * @return mixed[] */ private function getFilteredSuggestions(?InstalledRepository $installedRepo = null, ?PackageInterface $onlyDependentsOf = null): array { $suggestedPackages = $this->getPackages(); $installedNames = []; if (null !== $installedRepo && !empty($suggestedPackages)) { foreach ($installedRepo->getPackages() as $package) { $installedNames = array_merge( $installedNames, $package->getNames() ); } } $sourceFilter = []; if ($onlyDependentsOf) { $sourceFilter = array_map(static function ($link): string { return $link->getTarget(); }, array_merge($onlyDependentsOf->getRequires(), $onlyDependentsOf->getDevRequires())); $sourceFilter[] = $onlyDependentsOf->getName(); } $suggestions = []; foreach ($suggestedPackages as $suggestion) { if (in_array($suggestion['target'], $installedNames) || ($sourceFilter && !in_array($suggestion['source'], $sourceFilter))) { continue; } $suggestions[] = $suggestion; } return $suggestions; } private function escapeOutput(string $string): string { return OutputFormatter::escape( $this->removeControlCharacters($string) ); } private function removeControlCharacters(string $string): string { return Preg::replace( '/[[:cntrl:]]/', '', str_replace("\n", ' ', $string) ); } }