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.*)\s*$}', $selection, $matches)) {
+ return $matches[1].' '.$matches[2];
+ }
+
+ if (!isset($matches[(int) $selection])) {
+ throw new \Exception('Not a valid selection');
+ }
+
+ $package = $matches[(int) $selection];
+
+ return $package['name'];
+ };
+
+ $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;
}
- if (!is_numeric($selection) && preg_match('{^\s*(\S+) +(\S.*)\s*}', $selection, $matches)) {
- return $matches[1].' '.$matches[2];
- }
-
- if (!isset($matches[(int) $selection])) {
- throw new \Exception('Not a valid selection');
- }
-
- $package = $matches[(int) $selection];
-
- return sprintf('%s %s', $package->getName(), $package->getPrettyVersion());
- };
-
- $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a "[package] [version]" couple if it is not listed', false, ':'), $validator, 3);
+ $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']);
+ $flags = $onlyName ? RepositoryInterface::SEARCH_NAME : RepositoryInterface::SEARCH_FULLTEXT;
+ $results = $repos->search(implode(' ', $input->getArgument('tokens')), $flags);
+
+ foreach ($results as $result) {
+ $output->writeln($result['name'] . (isset($result['description']) ? ' '. $result['description'] : ''));
}
}
-
- public function processPackage($package)
- {
- if ($package instanceof AliasPackage || isset($this->matches[$package->getName()])) {
- return;
- }
-
- 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;
- }
- }
-
- /**
- * 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 ($writeDescription) {
- $description = strtok($package->getDescription(), "\r\n");
- $remaining = $width - $nameLength - $versionLength - 4;
- if (strlen($description) > $remaining) {
- $description = substr($description, 0, $remaining - 3) . '...';
+ if ($writeVersion) {
+ $output->write(' ' . str_pad($this->versionParser->formatVersion($package), $versionLength, ' '), false);
}
- $output->write(' ' . $description);
+
+ 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);
+ }
+ } 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');
+ $policy = new DefaultPolicy();
+ $pool = new Pool('dev');
+ $pool->addRepository($repos);
- if (null === $version) {
- // search for a locally installed version
- foreach ($matches as $package) {
- if ($installedRepo->hasPackage($package)) {
- $match = $package;
- break;
- }
+ $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 true;
+ 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 $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).
*
@@ -51,24 +55,19 @@ 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
- *
- * @param callable $callback
- * @param string $class
- * @return bool false if the process was interrupted, true otherwise
- */
- public function filterPackages($callback, $class = 'Composer\Package\Package');
-
/**
* Returns list of registered packages.
*
* @return array
*/
public function getPackages();
+
+ /**
+ * Searches the repository for packages containing the query
+ *
+ * @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 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));