Ver código fonte

Adds download and favers fields to schema.
Adds download and favers fields when indexing packages.
Modified WebController to support multiple orderBys : allows for the combination the combination of both downloads and favers.
Added tests.

Benjamin Michalski 10 anos atrás
pai
commit
fd7a3a3b1d

+ 2 - 0
doc/schema.xml

@@ -237,6 +237,8 @@
    <field name="tags" type="text_general_rev" indexed="true" stored="true" multiValued="true"/>
    <field name="type" type="text_general_rev" indexed="true" stored="true"/>
    <field name="trendiness" type="float" indexed="true" stored="true" />
+   <field name="downloads" type="int" indexed="true" stored="true" />
+   <field name="favers" type="int" indexed="true" stored="true" />
    <field name="repository" type="string" indexed="false" stored="true" />
    <field name="abandoned" type="int" indexed="false" stored="true" />
    <field name="replacementPackage" type="string" indexed="false" stored="true" />

+ 21 - 8
src/Packagist/WebBundle/Command/IndexPackagesCommand.php

@@ -12,12 +12,16 @@
 
 namespace Packagist\WebBundle\Command;
 
+use DateTime;
+use Exception;
 use Packagist\WebBundle\Entity\Package;
-
+use Packagist\WebBundle\Model\DownloadManager;
+use Packagist\WebBundle\Model\FavoriteManager;
+use Solarium_Document_ReadWrite;
 use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Output\OutputInterface;
 
 /**
@@ -62,6 +66,8 @@ class IndexPackagesCommand extends ContainerAwareCommand
         $doctrine = $this->getContainer()->get('doctrine');
         $solarium = $this->getContainer()->get('solarium.client');
         $redis = $this->getContainer()->get('snc_redis.default');
+        $downloadManager = $this->getContainer()->get('packagist.download_manager');
+        $favoriteManager = $this->getContainer()->get('packagist.favorite_manager');
 
         $lock = $this->getContainer()->getParameter('kernel.cache_dir').'/composer-indexer.lock';
         $timeout = 600;
@@ -119,11 +125,11 @@ class IndexPackagesCommand extends ContainerAwareCommand
 
                 try {
                     $document = $update->createDocument();
-                    $this->updateDocumentFromPackage($document, $package, $redis);
+                    $this->updateDocumentFromPackage($document, $package, $redis, $downloadManager, $favoriteManager);
                     $update->addDocument($document);
 
-                    $package->setIndexedAt(new \DateTime);
-                } catch (\Exception $e) {
+                    $package->setIndexedAt(new DateTime);
+                } catch (Exception $e) {
                     $output->writeln('<error>Exception: '.$e->getMessage().', skipping package '.$package->getName().'.</error>');
                 }
 
@@ -145,7 +151,7 @@ class IndexPackagesCommand extends ContainerAwareCommand
                                 $document->setField('abandoned', 0);
                                 $document->setField('replacementPackage', '');
                                 $update->addDocument($document);
-                            } catch (\Exception $e) {
+                            } catch (Exception $e) {
                                 $output->writeln('<error>Exception: '.$e->getMessage().', skipping package '.$package->getName().':provide:'.$provide->getPackageName().'</error>');
                             }
                         }
@@ -164,13 +170,20 @@ class IndexPackagesCommand extends ContainerAwareCommand
         unlink($lock);
     }
 
-    private function updateDocumentFromPackage(\Solarium_Document_ReadWrite $document, Package $package, $redis)
-    {
+    private function updateDocumentFromPackage(
+        Solarium_Document_ReadWrite $document,
+        Package $package,
+        $redis,
+        DownloadManager $downloadManager,
+        FavoriteManager $favoriteManager
+    ) {
         $document->setField('id', $package->getId());
         $document->setField('name', $package->getName());
         $document->setField('description', $package->getDescription());
         $document->setField('type', $package->getType());
         $document->setField('trendiness', $redis->zscore('downloads:trending', $package->getId()));
+        $document->setField('downloads', $downloadManager->getTotalDownloads($package));
+        $document->setField('favers', $favoriteManager->getFaverCount($package));
         $document->setField('repository', $package->getRepository());
         if ($package->isAbandoned()) {
             $document->setField('abandoned', 1);

+ 36 - 1
src/Packagist/WebBundle/Controller/WebController.php

@@ -262,6 +262,7 @@ class WebController extends Controller
 
         $typeFilter = $req->query->get('type');
         $tagsFilter = $req->query->get('tags');
+        $orderBys = $req->query->get('orderBys');
 
         if ($req->query->has('search_query') || $typeFilter || $tagsFilter) {
             /** @var $solarium \Solarium_Client */
@@ -294,6 +295,33 @@ class WebController extends Controller
                 $select->addFilterQuery($filterQuery);
             }
 
+            if ($orderBys) {
+                $allowedSorts = array(
+                    'downloads' => 1,
+                    'orders' => 1
+                );
+
+                $allowedOrders = array(
+                    'asc' => 1,
+                    'desc' => 1,
+                    'ASC' => 1,
+                    'DESC' => 1
+                );
+
+                $filteredSorts = array();
+
+                foreach ($orderBys as $orderBy) {
+                    if (isset($orderBy['sort'])
+                        && isset($allowedSorts[$orderBy['sort']])
+                        && isset($orderBy['order'])
+                        && isset($allowedOrders[$orderBy['order']])) {
+                        $filteredSorts[$orderBy['sort']] = $orderBy['order'];
+                    }
+                }
+
+                $select->addSorts($filteredSorts);
+            }
+
             if ($req->query->has('search_query')) {
                 $form->bind($req);
                 if ($form->isValid()) {
@@ -324,7 +352,14 @@ class WebController extends Controller
 
             $paginator->setCurrentPage($req->query->get('page', 1), false, true);
 
-            $metadata = $this->getPackagesMetadata($paginator);
+            $metadata = array();
+
+            foreach ($paginator as $package) {
+                if (is_numeric($package->id)) {
+                    $metadata['downloads'][$package->id] = $package->downloads;
+                    $metadata['favers'][$package->id] = $package->favers;
+                }
+            }
 
             if ($req->getRequestFormat() === 'json') {
                 try {

+ 244 - 3
src/Packagist/WebBundle/Tests/Controller/WebControllerTest.php

@@ -2,7 +2,11 @@
 
 namespace Packagist\WebBundle\Tests\Controller;
 
+use Exception;
+use Packagist\WebBundle\Entity\Package;
+use Packagist\WebBundle\Model\DownloadManager;
 use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 class WebControllerTest extends WebTestCase
 {
@@ -13,7 +17,7 @@ class WebControllerTest extends WebTestCase
         $crawler = $client->request('GET', '/');
         $this->assertEquals('Getting Started', $crawler->filter('.getting-started h1')->text());
     }
-    
+
     public function testPackages()
     {
         $client = self::createClient();
@@ -21,15 +25,252 @@ class WebControllerTest extends WebTestCase
         $crawler = $client->request('GET', '/packages/');
         $this->assertTrue($crawler->filter('.packages li')->count() > 0);
     }
-    
+
     public function testPackage()
     {
         $client = self::createClient();
         //we expect package to be clickable and showing at least 'package' div
         $crawler = $client->request('GET', '/packages/');
         $link = $crawler->filter('.packages li h1 a')->first()->attr('href');
-        
+
         $crawler = $client->request('GET', $link);
         $this->assertTrue($crawler->filter('.package')->count() > 0);
     }
+
+    /**
+     * @covers ::nothing
+     */
+    public function testSearchNoOrderBysAction()
+    {
+        $json = $this->commonTestSearchActionOrderBysDownloads();
+
+        $this->assertSame(
+            $this->getJsonResults(
+                array(
+                    $this->getJsonResult('twig/twig', 25, 0),
+                    $this->getJsonResult('composer/packagist', 12, 0),
+                    $this->getJsonResult('symfony/symfony', 42, 0),
+                )
+            ),
+            $json
+        );
+    }
+
+    /**
+     * @covers ::nothing
+     */
+    public function testSearchOrderByDownloadsAscAction()
+    {
+        $json = $this->commonTestSearchActionOrderBysDownloads(
+            array(
+                array(
+                    'sort' => 'downloads',
+                    'order' => 'asc',
+                ),
+            )
+        );
+
+        $this->assertSame(
+            $this->getJsonResults(
+                array(
+                    $this->getJsonResult('composer/packagist', 12, 0),
+                    $this->getJsonResult('twig/twig', 25, 0),
+                    $this->getJsonResult('symfony/symfony', 42, 0),
+                )
+            ),
+            $json
+        );
+    }
+
+    /**
+     * @covers ::nothing
+     */
+    public function testSearchOrderByDownloadsDescAction()
+    {
+        $json = $this->commonTestSearchActionOrderBysDownloads(
+            array(
+                array(
+                    'sort' => 'downloads',
+                    'order' => 'desc',
+                ),
+            )
+        );
+
+        $this->assertSame(
+            $this->getJsonResults(
+                array(
+                    $this->getJsonResult('symfony/symfony', 42, 0),
+                    $this->getJsonResult('twig/twig', 25, 0),
+                    $this->getJsonResult('composer/packagist', 12, 0),
+                )
+            ),
+            $json
+        );
+    }
+
+    /**
+     * @param callable $onBeforeIndex TODO Add typehint when migrating to 5.4+
+     * @param array $orderBys
+     *
+     * @return array
+     */
+    protected function commonTestSearchActionOrderBysAction(
+        $onBeforeIndex,
+        array $orderBys = array()
+    ) {
+        $client = self::createClient();
+
+        $container = $client->getContainer();
+
+        $kernelRootDir = $container->getParameter('kernel.root_dir');
+
+        $this->executeCommand($kernelRootDir . '/console doctrine:database:drop --env=test --force');
+        $this->executeCommand($kernelRootDir . '/console doctrine:database:create --env=test');
+        $this->executeCommand($kernelRootDir . '/console doctrine:schema:create --env=test');
+        $this->executeCommand($kernelRootDir . '/console redis:flushall --env=test -n');
+
+        $lock = $container->getParameter('kernel.cache_dir').'/composer-indexer.lock';
+
+        $this->executeCommand('rm -f ' . $lock);
+
+        $em = $container->get('doctrine')->getManager();
+
+        if (!empty($orderBys)) {
+            $orderBysQryStrPart = '&' . http_build_query(
+                array(
+                    'orderBys' => $orderBys
+                )
+            );
+        } else {
+            $orderBysQryStrPart = '';
+        }
+
+        $client->request('GET', '/search.json?q=' . $orderBysQryStrPart);
+
+        $response = $client->getResponse();
+
+        $content = $client->getResponse()->getContent();
+
+        $this->assertSame(200, $response->getStatusCode(), $content);
+
+        $package = new Package();
+
+        $package->setName('twig/twig');
+        $package->setRepository('https://github.com/twig/twig');
+
+        $package1 = new Package();
+
+        $package1->setName('composer/packagist');
+        $package1->setRepository('https://github.com/composer/packagist');
+
+        $package2 = new Package();
+
+        $package2->setName('symfony/symfony');
+        $package2->setRepository('https://github.com/symfony/symfony');
+
+        $em->persist($package);
+        $em->persist($package1);
+        $em->persist($package2);
+
+        $em->flush();
+
+        $onBeforeIndex($container, $package, $package1, $package2);
+
+        $this->executeCommand($kernelRootDir . '/console packagist:index --env=test --force');
+
+        return json_decode($content, true);
+    }
+
+    /**
+     * @param array $orderBys
+     *
+     * @return array
+     */
+    protected function commonTestSearchActionOrderBysDownloads(
+        array $orderBys = array()
+    ) {
+        return $this->commonTestSearchActionOrderBysAction(
+            function (
+                ContainerInterface $container,
+                Package $package,
+                Package $package1,
+                Package $package2
+            ) {
+                $downloadManager = $container->get('packagist.download_manager');
+
+                /* @var $downloadManager DownloadManager */
+
+                for ($i = 0; $i < 25; $i += 1) {
+                    $downloadManager->addDownload($package->getId(), 25);
+                }
+                for ($i = 0; $i < 12; $i += 1) {
+                    $downloadManager->addDownload($package1->getId(), 12);
+                }
+                for ($i = 0; $i < 42; $i += 1) {
+                    $downloadManager->addDownload($package2->getId(), 42);
+                }
+            },
+            $orderBys
+        );
+    }
+
+    /**
+     * Executes a given command.
+     *
+     * @param string $command a command to execute
+     *
+     * @throws Exception when the return code is not 0.
+     */
+    protected function executeCommand(
+        $command
+    ) {
+        $output = array();
+
+        $returnCode = null;;
+
+        exec($command, $output, $returnCode);
+
+        if ($returnCode !== 0) {
+            throw new Exception(
+                sprintf(
+                    'Error executing command "%s", return code was "%s".',
+                    $command,
+                    $returnCode
+                )
+            );
+        }
+    }
+
+    /**
+     * @param string $package
+     * @param int $downloads
+     * @param int $favers
+     *
+     * @return array
+     */
+    protected function getJsonResult($package, $downloads, $favers)
+    {
+        return array(
+            'name' => $package,
+            'description' => '',
+            'url' => 'http://localhost/packages/' . $package,
+            'repository' => 'https://github.com/' . $package,
+            'downloads' => $downloads,
+            'favers' => $favers,
+        );
+    }
+
+    /**
+     * @param array $results
+     *
+     * @return array
+     */
+    protected function getJsonResults(
+        array $results
+    ) {
+        return array(
+            'results' => $results,
+            'total' => count($results)
+        );
+    }
 }