Browse Source

Remove filterPackages and add RepositoryInterface::search, refactor all commands to use new methods and remove all usage of the full package list for Composer repositories that support providers, fixes #1646

Jordi Boggiano 12 years ago
parent
commit
be861f090a

+ 3 - 4
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);

+ 43 - 25
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(' <info>%5s</info> %s <comment>%s</comment>', "[$position]", $package->getPrettyName(), $package->getPrettyVersion()));
+                    $choices[] = sprintf(' <info>%5s</info> %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;

+ 1 - 1
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()) {

+ 6 - 71
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'] . '<comment>:</comment> '. $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)
-                    . '<highlight>' . substr($package->getPrettyName(), $pos, strlen($token)) . '</highlight>'
-                    . 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)
-                    . '<highlight>' . substr($description, $pos, strlen($token)) . '</highlight>'
-                    . substr($description, $pos + strlen($token));
-            }
-
-            if ($score >= 3) {
-                $this->output->writeln($name . '<comment>:</comment> '. $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;
-    }
 }

+ 76 - 50
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 = '<info>platform</info>:';
-            } elseif ($installedRepo->hasPackage($package)) {
+            } elseif (
+                $repo === $installedRepo
+                || ($installedRepo instanceof CompositeRepository && in_array($repo, $installedRepo->getRepositories(), true))
+            ) {
                 $type = '<info>installed</info>:';
             } else {
                 $type = '<comment>available</comment>:';
             }
-            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);
     }
 
     /**

+ 4 - 4
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) {

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

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

+ 14 - 0
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}
      */

+ 12 - 13
src/Composer/Repository/RepositoryInterface.php

@@ -19,9 +19,13 @@ use Composer\Package\PackageInterface;
  *
  * @author Nils Adermann <naderman@naderman.de>
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
  */
 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);
 }

+ 2 - 2
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));