Browse Source

Merge remote-tracking branch 'rdohms/implementing-feeds'

Conflicts:
	composer.lock
Jordi Boggiano 12 years ago
parent
commit
19d03b4cc8

+ 14 - 3
composer.json

@@ -43,7 +43,12 @@
         "nelmio/solarium-bundle": "dev-master",
         "predis/predis": "0.7.*",
         "snc/redis-bundle": "dev-master",
-        "white-october/pagerfanta-bundle": "dev-master"
+        "white-october/pagerfanta-bundle": "dev-master",
+        "zendframework/zend-feed": "2.0.*",
+
+        "zendframework/zend-servicemanager": "2.0.*",
+        "zendframework/zend-uri": "2.0.*",
+        "zendframework/zend-version": "2.0.*"
     },
     "scripts": {
         "post-install-cmd": [
@@ -60,5 +65,11 @@
     "extra": {
         "symfony-app-dir": "app",
         "symfony-web-dir": "web"
-    }
-}
+    },
+    "repositories": [
+        {
+            "type": "composer",
+            "url": "http://packages.zendframework.com/"
+        }
+    ]
+}

+ 315 - 4
composer.lock

@@ -1,5 +1,5 @@
 {
-    "hash": "935255dfdcb9876ff6d3b91eea1e5842",
+    "hash": "b50cdf1c09be1b7696e6ca2f95a9e5e2",
     "packages": [
         {
             "name": "composer/composer",
@@ -1670,11 +1670,322 @@
                 "page",
                 "paging"
             ]
+        },
+        {
+            "name": "zendframework/zend-escaper",
+            "version": "2.0.2",
+            "target-dir": "Zend/Escaper",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_Escaper-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\Escaper": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": " ",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "escaper"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-feed",
+            "version": "2.0.2",
+            "target-dir": "Zend/Feed",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_Feed-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "zendframework/zend-escaper": "self.version",
+                "zendframework/zend-stdlib": "self.version"
+            },
+            "suggest": {
+                "zendframework/zend-uri": "Zend\\Uri component",
+                "zendframework/zend-validator": "Zend\\Validator component"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\Feed": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "provides functionality for consuming RSS and Atom feeds",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "feed"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-i18n",
+            "version": "2.0.2",
+            "target-dir": "Zend/I18n",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_I18n-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "ext-intl": "*",
+                "zendframework/zend-stdlib": "self.version"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\I18n": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": " ",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "i18n"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-servicemanager",
+            "version": "2.0.2",
+            "target-dir": "Zend/ServiceManager",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_ServiceManager-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "zendframework/zend-di": "Zend\\Di component"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\ServiceManager": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": " ",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "servicemanager"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-stdlib",
+            "version": "2.0.2",
+            "target-dir": "Zend/Stdlib",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_Stdlib-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "pecl-weakref": "Implementation of weak references for Stdlib\\CallbackHandler"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\Stdlib": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": " ",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "stdlib"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-uri",
+            "version": "2.0.2",
+            "target-dir": "Zend/Uri",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_Uri-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "zendframework/zend-escaper": "self.version",
+                "zendframework/zend-validator": "self.version"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\Uri": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "a component that aids in manipulating and validating » Uniform Resource Identifiers (URIs)",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "uri"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-validator",
+            "version": "2.0.2",
+            "target-dir": "Zend/Validator",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_Validator-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "zendframework/zend-i18n": "self.version",
+                "zendframework/zend-stdlib": "self.version"
+            },
+            "require-dev": {
+                "zendframework/zend-math": "self.version"
+            },
+            "suggest": {
+                "zendframework/zend-db": "Zend\\Db component",
+                "zendframework/zend-math": "Zend\\Math component"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\Validator": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "provides a set of commonly needed validators",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "validator"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
+        },
+        {
+            "name": "zendframework/zend-version",
+            "version": "2.0.2",
+            "target-dir": "Zend/Version",
+            "dist": {
+                "type": "zip",
+                "url": "http://packages.zendframework.com/composer/Zend_Version-2.0.2.zip",
+                "reference": null,
+                "shasum": null
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Zend\\Version": ""
+                }
+            },
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": " ",
+            "homepage": "http://packages.zendframework.com/",
+            "keywords": [
+                "zf2",
+                "version"
+            ],
+            "support": {
+                "email": "fw-general-subscribe@lists.zend.com",
+                "irc": "irc://irc.freenode.net/zftalk",
+                "issues": "https://github.com/zendframework/zf2/issues",
+                "source": "https://github.com/zendframework/zf2"
+            }
         }
     ],
-    "packages-dev": [
-
-    ],
+    "packages-dev": null,
     "aliases": [
 
     ],

+ 238 - 0
src/Packagist/WebBundle/Controller/FeedController.php

@@ -0,0 +1,238 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Doctrine\ORM\QueryBuilder;
+use Pagerfanta\Adapter\DoctrineORMAdapter;
+use Pagerfanta\Pagerfanta;
+use Zend\Feed\Writer\Entry;
+use Zend\Feed\Writer\Feed;
+use Packagist\WebBundle\Entity\Package;
+use Packagist\WebBundle\Entity\Version;
+use Symfony\Component\HttpFoundation\Response;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
+
+/**
+ * @author Rafael Dohms <rafael@doh.ms>
+ *
+ * @Route("/feeds")
+ */
+class FeedController extends Controller
+{
+    /**
+     * @Route(
+     *     "/packages.{_format}",
+     *     name="feed_packages",
+     *     requirements={"_format"="(rss|atom)"}
+     * )
+     * @Method({"GET"})
+     */
+    public function packagesAction()
+    {
+        /** @var $repo \Packagist\WebBundle\Entity\VersionRepository */
+        $repo = $this->getDoctrine()->getRepository('PackagistWebBundle:Version');
+        $packages = $this->getLimitedResults(
+            $repo->getQueryBuilderForLatestVersionWithPackage()
+        );
+
+        $feed = $this->buildFeed(
+            'Latest Packages',
+            'Latest packages updated on Packagist.',
+            $packages
+        );
+
+        return $this->buildResponse($feed);
+    }
+
+    /**
+     * @Route(
+     *     "/releases.{_format}",
+     *     name="feed_releases",
+     *     requirements={"_format"="(rss|atom)"}
+     * )
+     * @Method({"GET"})
+     */
+    public function releasesAction()
+    {
+        /** @var $repo \Packagist\WebBundle\Entity\PackageRepository */
+        $repo = $this->getDoctrine()->getRepository('PackagistWebBundle:Package');
+        $packages = $this->getLimitedResults(
+            $repo->getQueryBuilderForNewestPackages()
+        );
+
+        $feed = $this->buildFeed(
+            'Latest Released Packages',
+            'Latest packages added to Packagist.',
+            $packages
+        );
+
+        return $this->buildResponse($feed);
+    }
+
+    /**
+     * @Route(
+     *     "/vendor.{filter}.{_format}",
+     *     name="feed_vendor",
+     *     requirements={"_format"="(rss|atom)"}
+     * )
+     * @Method({"GET"})
+     */
+    public function vendorAction($filter)
+    {
+        /** @var $repo \Packagist\WebBundle\Entity\PackageRepository */
+        $repo = $this->getDoctrine()->getRepository('PackagistWebBundle:Package');
+        $packages = $this->getLimitedResults(
+            $repo->getQueryBuilderForLatestPackagesByVendor($filter)
+        );
+
+        $feed = $this->buildFeed(
+            "$filter Packages",
+            "Latest packages updated on Packagist for $filter.",
+            $packages
+        );
+
+        return $this->buildResponse($feed);
+    }
+
+    /**
+     * Limits a query to the desired number of results
+     *
+     * @param \Doctrine\ORM\QueryBuilder $queryBuilder
+     *
+     * @return array|\Traversable
+     */
+    protected function getLimitedResults(QueryBuilder $queryBuilder)
+    {
+        $paginator = new Pagerfanta(new DoctrineORMAdapter($queryBuilder));
+        $paginator->setMaxPerPage(
+            $this->container->getParameter('packagist_web.rss_max_items')
+        );
+        $paginator->setCurrentPage(1);
+
+        return $paginator->getCurrentPageResults();
+    }
+
+    /**
+     * Builds the desired feed
+     *
+     * @param string $title
+     * @param string $description
+     * @param array  $items
+     *
+     * @return \Zend\Feed\Writer\Feed
+     */
+    protected function buildFeed($title, $description, $items)
+    {
+        $feed = new Feed();
+        $feed->setTitle($title);
+        $feed->setDescription($description);
+        $feed->setLink($this->getRequest()->getSchemeAndHttpHost());
+
+        foreach ($items as $item) {
+            $entry = $feed->createEntry();
+            $this->populateEntry($entry, $item);
+            $feed->addEntry($entry);
+        }
+
+        if ($this->getRequest()->getRequestFormat() == 'atom') {
+            $feed->setFeedLink(
+                $this->getRequest()->getUri(),
+                $this->getRequest()->getRequestFormat()
+            );
+        }
+
+        $feed->setDateModified($feed->getEntry(0)->getDateModified());
+
+        return $feed;
+    }
+
+    /**
+     * Receives either a Package or a Version and populates a feed entry.
+     *
+     * @param \Zend\Feed\Writer\Entry $entry
+     * @param Package|Version         $item
+     */
+    protected function populateEntry(Entry $entry, $item)
+    {
+        if ($item instanceof Package) {
+            $version = $item->getVersions()->first() ?: new Version();
+
+            $this->populatePackageData($entry, $item);
+            $this->populateVersionData($entry, $version);
+        }
+
+        if ($item instanceof Version) {
+            $this->populatePackageData($entry, $item->getPackage());
+            $this->populateVersionData($entry, $item);
+        }
+    }
+
+    /**
+     * Populates a feed entry with data coming from Package objects.
+     *
+     * @param \Zend\Feed\Writer\Entry $entry
+     * @param Package                 $package
+     */
+    protected function populatePackageData(Entry $entry, Package $package)
+    {
+        $entry->setTitle($package->getPackageName());
+        $entry->setLink(
+            $this->generateUrl(
+                'view_package',
+                array('name' => $package->getName()),
+                true
+            )
+        );
+
+        $entry->setDateModified($package->getUpdatedAt());
+        $entry->setDateCreated($package->getCreatedAt());
+        $entry->setDescription($package->getDescription());
+    }
+
+    /**
+     * Populates a feed entry with data coming from Version objects.
+     *
+     * @param \Zend\Feed\Writer\Entry $entry
+     * @param Version                 $version
+     */
+    protected function populateVersionData(Entry $entry, Version $version)
+    {
+        $entry->setTitle($entry->getTitle()." ({$version->getVersion()})");
+
+        foreach ($version->getAuthors() as $author) {
+            /** @var $author \Packagist\WebBundle\Entity\Author */
+            $entry->addAuthor(array(
+                'name'  => $author->getName()
+            ));
+        }
+    }
+
+    /**
+     * Creates a HTTP Response and exports feed
+     *
+     * @param \Zend\Feed\Writer\Feed $feed
+     *
+     * @return \Symfony\Component\HttpFoundation\Response
+     */
+    protected function buildResponse(Feed $feed)
+    {
+        $content = $feed->export($this->getRequest()->getRequestFormat());
+
+        $response = new Response($content, 200);
+        $response->setSharedMaxAge(3600);
+
+        return $response;
+    }
+}

+ 30 - 0
src/Packagist/WebBundle/DependencyInjection/Configuration.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Packagist\WebBundle\DependencyInjection;
+
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+/**
+ * This is the class that validates and merges configuration from your app/config files
+ *
+ * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
+ */
+class Configuration implements ConfigurationInterface
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function getConfigTreeBuilder()
+    {
+        $treeBuilder = new TreeBuilder();
+        $rootNode = $treeBuilder->root('packagist_web');
+
+        $rootNode
+            ->children()
+                ->scalarNode('rss_max_items')->defaultValue(40)->end()
+            ->end();
+
+        return $treeBuilder;
+    }
+}

+ 6 - 1
src/Packagist/WebBundle/DependencyInjection/PackagistWebExtension.php

@@ -24,7 +24,12 @@ class PackagistWebExtension extends Extension
 {
     public function load(array $configs, ContainerBuilder $container)
     {
+        $configuration = new Configuration();
+        $config = $this->processConfiguration($configuration, $configs);
+
         $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
         $loader->load('services.yml');
+
+        $container->setParameter('packagist_web.rss_max_items', $config['rss_max_items']);
     }
-}
+}

+ 34 - 0
src/Packagist/WebBundle/Entity/PackageRepository.php

@@ -214,4 +214,38 @@ class PackageRepository extends EntityRepository
 
         return $qb;
     }
+
+    /**
+     * Gets the most recent packages created
+     *
+     * @return QueryBuilder
+     */
+    public function getQueryBuilderForNewestPackages()
+    {
+        $qb = $this->getBaseQueryBuilder();
+
+        $qb->orderBy('p.createdAt', 'DESC');
+        $qb->addOrderBy('v.releasedAt', 'DESC');
+
+        return $qb;
+    }
+
+    /**
+     * Gets the latest packages/versions released by a selected vendor
+     *
+     * @param string $vendor
+     *
+     * @return QueryBuilder
+     */
+    public function getQueryBuilderForLatestPackagesByVendor($vendor)
+    {
+        $qb = $this->getBaseQueryBuilder();
+
+        $qb->orderBy('v.releasedAt', 'DESC');
+
+        $qb->where('p.name LIKE ?0');
+        $qb->setParameter(0, $vendor.'/%');
+
+        return $qb;
+    }
 }

+ 18 - 0
src/Packagist/WebBundle/Entity/VersionRepository.php

@@ -67,4 +67,22 @@ class VersionRepository extends EntityRepository
 
         return $qb->getQuery()->getSingleResult();
     }
+
+    /**
+     * Returns the latest versions released
+     *
+     * @return \Doctrine\ORM\QueryBuilder
+     */
+    public function getQueryBuilderForLatestVersionWithPackage()
+    {
+        $qb = $this->getEntityManager()->createQueryBuilder();
+        $qb->select('v', 't', 'a', 'p')
+            ->from('Packagist\WebBundle\Entity\Version', 'v')
+            ->leftJoin('v.tags', 't')
+            ->leftJoin('v.authors', 'a')
+            ->leftJoin('v.package', 'p')
+            ->orderBy('v.releasedAt', 'DESC');
+
+        return $qb;
+    }
 }

+ 46 - 0
src/Packagist/WebBundle/Tests/Controller/FeedControllerTest.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Packagist\WebBundle\Tests\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class FeedControllerTest extends WebTestCase
+{
+    /**
+     * @param string $feed
+     * @param string $format
+     * @param string|null $filter
+     *
+     * @dataProvider provideForFeed
+     */
+    public function testFeedAction($feed, $format, $filter = null)
+    {
+        $client = self::createClient();
+
+        $url = $client->getContainer()->get('router')->generate($feed, array('_format' => $format, 'filter' => $filter));
+
+        $crawler = $client->request('GET', $url);
+
+        $this->assertEquals(200, $client->getResponse()->getStatusCode());
+        $this->assertContains($format, $client->getResponse()->getContent());
+
+        if ($filter !== null) {
+            $this->assertContains($filter, $client->getResponse()->getContent());
+        }
+
+    }
+
+
+    public function provideForFeed()
+    {
+        return array(
+            array('feed_packages', 'rss'),
+            array('feed_packages', 'atom'),
+            array('feed_releases', 'rss'),
+            array('feed_releases', 'atom'),
+            array('feed_vendor', 'rss', 'symfony'),
+            array('feed_vendor', 'atom', 'symfony'),
+        );
+    }
+
+}