diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index 6dc1a7e44..5603a17c0 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -73,9 +73,8 @@ EOT }, $input->getOption('link-type')); $messages = array(); - $repo->filterPackages(function ($package) use ($needle, $types, $linkTypes, &$messages) { - static $outputPackages = array(); - + $outputPackages = array(); + foreach ($repo->getPackages() as $package) { foreach ($types as $type) { foreach ($package->{'get'.$linkTypes[$type][0]}() as $link) { if ($link->getTarget() === $needle) { @@ -86,7 +85,7 @@ EOT } } } - }); + } if ($messages) { sort($messages); diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 2c9d53ed7..8541d3c98 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -292,15 +292,7 @@ EOT )); } - $token = strtolower($name); - - $this->repos->filterPackages(function ($package) use ($token, &$packages) { - if (false !== strpos($package->getName(), $token)) { - $packages[] = $package; - } - }); - - return $packages; + return $this->repos->search($name); } protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array()) @@ -339,31 +331,57 @@ EOT '' )); + $exactMatch = null; + $choices = array(); foreach ($matches as $position => $package) { - $output->writeln(sprintf(' %5s %s %s', "[$position]", $package->getPrettyName(), $package->getPrettyVersion())); + $choices[] = sprintf(' %5s %s', "[$position]", $package['name']); + if ($package['name'] === $package) { + $exactMatch = true; + break; + } } - $output->writeln(''); + // no match, prompt which to pick + if (!$exactMatch) { + $output->writeln($choices); + $output->writeln(''); - $validator = function ($selection) use ($matches) { - if ('' === $selection) { - return false; - } + $validator = function ($selection) use ($matches) { + if ('' === $selection) { + return false; + } - if (!is_numeric($selection) && preg_match('{^\s*(\S+) +(\S.*)\s*}', $selection, $matches)) { - return $matches[1].' '.$matches[2]; - } + if (!is_numeric($selection) && preg_match('{^\s*(\S+)\s+(\S.*)\s*$}', $selection, $matches)) { + return $matches[1].' '.$matches[2]; + } - if (!isset($matches[(int) $selection])) { - throw new \Exception('Not a valid selection'); - } + if (!isset($matches[(int) $selection])) { + throw new \Exception('Not a valid selection'); + } - $package = $matches[(int) $selection]; + $package = $matches[(int) $selection]; - return sprintf('%s %s', $package->getName(), $package->getPrettyVersion()); - }; + return $package['name']; + }; - $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a "[package] [version]" couple if it is not listed', false, ':'), $validator, 3); + $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or the complete package name if it is not listed', false, ':'), $validator, 3); + } + + // no constraint yet, prompt user + if (false !== $package && false === strpos($package, ' ')) { + $validator = function ($input) { + $input = trim($input); + + return $input ?: false; + }; + + $constraint = $dialog->askAndValidate($output, $dialog->getQuestion('Enter the version constraint to require', false, ':'), $validator, 3); + if (false === $constraint) { + continue; + } + + $package .= ' '.$constraint; + } if (false !== $package) { $requires[] = $package; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index af417023e..93304479b 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -109,7 +109,7 @@ EOT ->setPreferDist($input->getOption('prefer-dist')) ->setDevMode($input->getOption('dev')) ->setUpdate(true) - ->setUpdateWhitelist($requirements); + ->setUpdateWhitelist(array_keys($requirements)); ; if (!$install->run()) { diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index 061786300..e3aee3744 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\AliasPackage; use Composer\Factory; @@ -66,79 +67,13 @@ EOT $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } - $this->onlyName = $input->getOption('only-name'); - $this->tokens = $input->getArgument('tokens'); - $this->output = $output; - $repos->filterPackages(array($this, 'processPackage'), 'Composer\Package\CompletePackage'); + $onlyName = $input->getOption('only-name'); - foreach ($this->lowMatches as $details) { - $output->writeln($details['name'] . ': '. $details['description']); - } - } - - public function processPackage($package) - { - if ($package instanceof AliasPackage || isset($this->matches[$package->getName()])) { - return; - } + $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT; + $results = $repos->search(implode(' ', $input->getArgument('tokens')), $flags); - foreach ($this->tokens as $token) { - if (!$score = $this->matchPackage($package, $token)) { - continue; - } - - if (false !== ($pos = stripos($package->getName(), $token))) { - $name = substr($package->getPrettyName(), 0, $pos) - . '' . substr($package->getPrettyName(), $pos, strlen($token)) . '' - . substr($package->getPrettyName(), $pos + strlen($token)); - } else { - $name = $package->getPrettyName(); - } - - $description = strtok($package->getDescription(), "\r\n"); - if (false !== ($pos = stripos($description, $token))) { - $description = substr($description, 0, $pos) - . '' . substr($description, $pos, strlen($token)) . '' - . substr($description, $pos + strlen($token)); - } - - if ($score >= 3) { - $this->output->writeln($name . ': '. $description); - $this->matches[$package->getName()] = true; - } else { - $this->lowMatches[$package->getName()] = array( - 'name' => $name, - 'description' => $description, - ); - } - - return; + foreach ($results as $result) { + $output->writeln($result['name'] . (isset($result['description']) ? ' '. $result['description'] : '')); } } - - /** - * tries to find a token within the name/keywords/description - * - * @param CompletePackageInterface $package - * @param string $token - * @return boolean - */ - private function matchPackage(CompletePackageInterface $package, $token) - { - $score = 0; - - if (false !== stripos($package->getName(), $token)) { - $score += 5; - } - - if (!$this->onlyName && false !== stripos(join(',', $package->getKeywords() ?: array()), $token)) { - $score += 3; - } - - if (!$this->onlyName && false !== stripos($package->getDescription(), $token)) { - $score += 1; - } - - return $score; - } } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 8be36ed65..532c064ef 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -13,8 +13,11 @@ namespace Composer\Command; use Composer\Composer; +use Composer\DependencyResolver\Pool; +use Composer\DependencyResolver\DefaultPolicy; use Composer\Factory; use Composer\Package\CompletePackageInterface; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -22,6 +25,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\ArrayRepository; use Composer\Repository\CompositeRepository; +use Composer\Repository\ComposerRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; @@ -122,20 +126,39 @@ EOT // list packages $packages = array(); - $repos->filterPackages(function ($package) use (&$packages, $platformRepo, $installedRepo) { - if ($platformRepo->hasPackage($package)) { + + if ($repos instanceof CompositeRepository) { + $repos = $repos->getRepositories(); + } elseif (!is_array($repos)) { + $repos = array($repos); + } + + foreach ($repos as $repo) { + if ($repo === $platformRepo) { $type = 'platform:'; - } elseif ($installedRepo->hasPackage($package)) { + } elseif ( + $repo === $installedRepo + || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true)) + ) { $type = 'installed:'; } else { $type = 'available:'; } - if (!isset($packages[$type][$package->getName()]) - || version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') - ) { - $packages[$type][$package->getName()] = $package; + if ($repo instanceof ComposerRepository && $repo->hasProviders()) { + foreach ($repo->getProviderNames() as $name) { + $packages[$type][$name] = $name; + } + } else { + foreach ($repo->getPackages() as $package) { + if (!isset($packages[$type][$package->getName()]) + || !is_object($packages[$type][$package->getName()]) + || version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') + ) { + $packages[$type][$package->getName()] = $package; + } + } } - }, 'Composer\Package\CompletePackage'); + } $tree = !$input->getOption('platform') && !$input->getOption('installed') && !$input->getOption('available'); $indent = $tree ? ' ' : ''; @@ -148,8 +171,12 @@ EOT $nameLength = $versionLength = 0; foreach ($packages[$type] as $package) { - $nameLength = max($nameLength, strlen($package->getPrettyName())); - $versionLength = max($versionLength, strlen($this->versionParser->formatVersion($package))); + if (is_object($package)) { + $nameLength = max($nameLength, strlen($package->getPrettyName())); + $versionLength = max($versionLength, strlen($this->versionParser->formatVersion($package))); + } else { + $nameLength = max($nameLength, $package); + } } list($width) = $this->getApplication()->getTerminalDimensions(); if (defined('PHP_WINDOWS_VERSION_BUILD')) { @@ -159,19 +186,23 @@ EOT $writeVersion = !$input->getOption('name-only') && $showVersion && ($nameLength + $versionLength + 3 <= $width); $writeDescription = !$input->getOption('name-only') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width); foreach ($packages[$type] as $package) { - $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); + if (is_object($package)) { + $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); - if ($writeVersion) { - $output->write(' ' . str_pad($this->versionParser->formatVersion($package), $versionLength, ' '), false); - } + if ($writeVersion) { + $output->write(' ' . str_pad($this->versionParser->formatVersion($package), $versionLength, ' '), false); + } - if ($writeDescription) { - $description = strtok($package->getDescription(), "\r\n"); - $remaining = $width - $nameLength - $versionLength - 4; - if (strlen($description) > $remaining) { - $description = substr($description, 0, $remaining - 3) . '...'; + if ($writeDescription) { + $description = strtok($package->getDescription(), "\r\n"); + $remaining = $width - $nameLength - $versionLength - 4; + if (strlen($description) > $remaining) { + $description = substr($description, 0, $remaining - 3) . '...'; + } + $output->write(' ' . $description); } - $output->write(' ' . $description); + } else { + $output->write($indent . $package); } $output->writeln(''); } @@ -195,51 +226,46 @@ EOT protected function getPackage(RepositoryInterface $installedRepo, RepositoryInterface $repos, $name, $version = null) { $name = strtolower($name); + $constraint = null; if ($version) { $version = $this->versionParser->normalize($version); + $constraint = new VersionConstraint('=', $version); } - $match = null; - $matches = array(); - $repos->filterPackages(function ($package) use ($name, $version, &$matches) { - if ($package->getName() === $name) { - $matches[] = $package; - } - }, 'Composer\Package\CompletePackage'); - - if (null === $version) { - // search for a locally installed version - foreach ($matches as $package) { - if ($installedRepo->hasPackage($package)) { - $match = $package; - break; - } + $policy = new DefaultPolicy(); + $pool = new Pool('dev'); + $pool->addRepository($repos); + + $matchedPackage = null; + $matches = $pool->whatProvides($name, $constraint); + foreach ($matches as $index => $package) { + // skip providers/replacers + if ($package->getName() !== $name) { + unset($matches[$index]); + continue; } - if (!$match) { - // fallback to the highest version - foreach ($matches as $package) { - if (null === $match || version_compare($package->getVersion(), $match->getVersion(), '>=')) { - $match = $package; - } - } - } - } else { - // select the specified version - foreach ($matches as $package) { - if ($package->getVersion() === $version) { - $match = $package; - } + // select an exact match if it is in the installed repo and no specific version was required + if (null === $version && $installedRepo->hasPackage($package)) { + $matchedPackage = $package; } + + $matches[$index] = $package->getId(); + } + + // select prefered package according to policy rules + if (!$matchedPackage && $matches && $prefered = $policy->selectPreferedPackages($pool, array(), $matches)) { + $matchedPackage = $pool->literalToPackage($prefered[0]); } // build versions array $versions = array(); foreach ($matches as $package) { + $package = $pool->literalToPackage($package); $versions[$package->getPrettyVersion()] = $package->getVersion(); } - return array($match, $versions); + return array($matchedPackage, $versions); } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 0571e5676..992a7c69f 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -214,13 +214,13 @@ class Installer // output suggestions foreach ($this->suggestedPackages as $suggestion) { $target = $suggestion['target']; - if ($installedRepo->filterPackages(function (PackageInterface $package) use ($target) { + foreach ($installedRepo->getPackages() as $package) { if (in_array($target, $package->getNames())) { - return false; + continue 2; } - })) { - $this->io->write($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')'); } + + $this->io->write($suggestion['source'].' suggests installing '.$suggestion['target'].' ('.$suggestion['reason'].')'); } if (!$this->dryRun) { diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 49ddd99d5..72fe242c7 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -74,6 +74,27 @@ class ArrayRepository implements RepositoryInterface return $packages; } + /** + * {@inheritDoc} + */ + public function search($query, $mode = 0) + { + $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i'; + + $matches = array(); + foreach ($this->getPackages() as $package) { + // TODO implement SEARCH_FULLTEXT handling with keywords/description matching + if (preg_match($regex, $package->getName())) { + $matches[] = array( + 'name' => $package->getName(), + 'description' => $package->getDescription(), + ); + } + } + + return $matches; + } + /** * {@inheritDoc} */ @@ -112,20 +133,6 @@ class ArrayRepository implements RepositoryInterface } } - /** - * {@inheritDoc} - */ - public function filterPackages($callback, $class = 'Composer\Package\Package') - { - foreach ($this->getPackages() as $package) { - if (false === call_user_func($callback, $package)) { - return false; - } - } - - return true; - } - protected function createAliasPackage(PackageInterface $package, $alias = null, $prettyAlias = null) { return new AliasPackage($package, $alias ?: $package->getAlias(), $prettyAlias ?: $package->getPrettyAlias()); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 6a1f09178..a13bbabd1 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -135,24 +135,54 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository /** * {@inheritDoc} */ - public function filterPackages($callback, $class = 'Composer\Package\Package') + public function search($query, $mode = 0) { - if (null === $this->rawData) { - $this->rawData = $this->loadDataFromServer(); + $this->loadRootServerFile(); + + if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { + $url = str_replace('%query%', $query, $this->searchUrl); + + $json = $this->rfs->getContents($url, $url, false); + $results = JsonFile::parseJson($json, $url); + + return $results['results']; } - foreach ($this->rawData as $package) { - if (false === call_user_func($callback, $package = $this->createPackage($package, $class))) { - return false; - } - if ($package->getAlias()) { - if (false === call_user_func($callback, $this->createAliasPackage($package))) { - return false; + if ($this->hasProviders()) { + $results = array(); + $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i'; + + foreach ($this->getProviderNames() as $name) { + if (preg_match($regex, $name)) { + $results[] = array('name' => $name); } } + + return $results; + } + + return parent::search($query, $mode); + } + + public function getProviderNames() + { + $this->loadRootServerFile(); + + if (null === $this->providerListing) { + $this->loadProviderListings($this->loadRootServerFile()); + } + + if ($this->providersUrl) { + return array_keys($this->providerListing); + } + + // BC handling for old providers-includes + $providers = array(); + foreach (array_keys($this->providerListing) as $provider) { + $providers[] = substr($provider, 2, -5); } - return true; + return $providers; } /** @@ -196,15 +226,15 @@ class ComposerRepository extends ArrayRepository implements StreamableRepository public function whatProvides(Pool $pool, $name) { - // skip platform packages - if ($name === 'php' || in_array(substr($name, 0, 4), array('ext-', 'lib-'), true) || $name === '__root__') { - return array(); - } - if (isset($this->providers[$name])) { return $this->providers[$name]; } + // skip platform packages + if (preg_match('{^(?:php(?:-64bit)?|(?:ext|lib)-[^/]+)$}i', $name) || '__root__' === $name) { + return array(); + } + if (null === $this->providerListing) { $this->loadProviderListings($this->loadRootServerFile()); } diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 3467f04d9..7e0b7df8f 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -94,6 +94,20 @@ class CompositeRepository implements RepositoryInterface return call_user_func_array('array_merge', $packages); } + /** + * {@inheritdoc} + */ + public function search($query, $mode = 0) + { + $matches = array(); + foreach ($this->repositories as $repository) { + /* @var $repository RepositoryInterface */ + $matches[] = $repository->search($query, $mode); + } + + return call_user_func_array('array_merge', $matches); + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 69e48e4fb..c38b00653 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -19,9 +19,13 @@ use Composer\Package\PackageInterface; * * @author Nils Adermann * @author Konstantin Kudryashov + * @author Jordi Boggiano */ interface RepositoryInterface extends \Countable { + const SEARCH_FULLTEXT = 0; + const SEARCH_NAME = 1; + /** * Checks if specified package registered (installed). * @@ -52,23 +56,18 @@ interface RepositoryInterface extends \Countable public function findPackages($name, $version = null); /** - * Filters all the packages through a callback - * - * The packages are not guaranteed to be instances in the repository - * and this can only be used for streaming through a list of packages. - * - * If the callback returns false, the process stops + * Returns list of registered packages. * - * @param callable $callback - * @param string $class - * @return bool false if the process was interrupted, true otherwise + * @return array */ - public function filterPackages($callback, $class = 'Composer\Package\Package'); + public function getPackages(); /** - * Returns list of registered packages. + * Searches the repository for packages containing the query * - * @return array + * @param string $query search query + * @param int $mode a set of SEARCH_* constants to search on, implementations should do a best effort only + * @return array[] an array of array('name' => '...', 'description' => '...') */ - public function getPackages(); + public function search($query, $mode = 0); } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 9a2d340af..5061218e2 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -42,7 +42,7 @@ class ComposerRepositoryTest extends TestCase ); $repository - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('loadRootServerFile') ->will($this->returnValue($repoPackages)); @@ -50,7 +50,7 @@ class ComposerRepositoryTest extends TestCase $stubPackage = $this->getPackage('stub/stub', '1.0.0'); $repository - ->expects($this->at($at + 1)) + ->expects($this->at($at + 2)) ->method('createPackage') ->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage')) ->will($this->returnValue($stubPackage));