.gitignore 0000666 00000000026 13115630631 0006535 0 ustar 00 /bin/
/vendor/
cache/
.scrutinizer.yml 0000666 00000000151 13115630631 0007726 0 ustar 00 imports:
- php
tools:
external_code_coverage: true
filter:
excluded_paths:
- Tests/*
.travis.yml 0000666 00000000377 13115630631 0006667 0 ustar 00 language: php
php:
- 5.6
- 7.0
script : phpunit --coverage-clover=coverage.clover && wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover
before_script:
- composer install --dev
Controller/StreamController.php 0000666 00000011522 13115630631 0012703 0 ustar 00 attributes->get('_route_params');
$this->setModifiedSince($request);
$options['Since'] = $this->getModifiedSince();
return $this->createStreamResponse(
$options,
$request->get('format', 'rss'),
$request->get('source', self::DEFAULT_SOURCE)
);
}
/**
* Extract the 'If-Modified-Since' value from the headers.
*
* @return \DateTime
*/
protected function getModifiedSince()
{
if (is_null($this->since)) {
$this->since = new \DateTime('@0');
}
return $this->since;
}
/**
* @param Request $request
*
* @return $this
*/
protected function setModifiedSince(Request $request)
{
$this->since = new \DateTime();
if ($request->headers->has('If-Modified-Since')) {
$string = $request->headers->get('If-Modified-Since');
$this->since = \DateTime::createFromFormat(\DateTime::RSS, $string);
} else {
$this->since->setTimestamp(1);
}
return $this;
}
/**
* Generate the HTTP response
* 200 : a full body containing the stream
* 304 : Not modified.
*
* @param array $options
* @param $format
* @param string $source
*
* @return Response
*
* @throws \Exception
*/
protected function createStreamResponse(array $options, $format, $source = self::DEFAULT_SOURCE)
{
$content = $this->getContent($options, $source);
if ($this->mustForceRefresh() || $content->getLastModified() > $this->getModifiedSince()) {
$response = new Response($this->getFeedIo()->format($content, $format)->saveXML());
$response->headers->set('Content-Type', 'application/xhtml+xml');
$this->setFeedHeaders($response, $content);
} else {
$response = new Response();
$response->setNotModified();
}
return $response;
}
/**
* @param Response $response
* @param FeedInterface $feed
* @return $this
*/
protected function setFeedHeaders(Response $response, FeedInterface $feed)
{
$response->headers->set('Content-Type', 'application/xhtml+xml');
if (! $this->isPrivate() ) {
$response->setPublic();
}
$response->setMaxAge(3600);
$response->setLastModified($feed->getLastModified());
return $this;
}
/**
* Get the Stream's content using a FeedContentProviderInterface
* The FeedContentProviderInterface instance is provided as a service
* default : debril.provider.service.
*
* @param array $options
* @param string $source
*
* @return FeedInterface
*
* @throws \Exception
*/
protected function getContent(array $options, $source)
{
$provider = $this->get($source);
if (!$provider instanceof FeedContentProviderInterface) {
throw new \Exception('Provider is not a FeedContentProviderInterface instance');
}
try {
return $provider->getFeedContent($options);
} catch (FeedNotFoundException $e) {
throw $this->createNotFoundException('feed not found');
}
}
/**
* Returns true if the controller must ignore the last modified date.
*
* @return bool
*/
protected function mustForceRefresh()
{
if ($this->container->hasParameter(self::FORCE_PARAM_NAME)) {
return $this->container->getParameter(self::FORCE_PARAM_NAME);
}
return false;
}
/**
* @return boolean true if the feed must be private
*/
protected function isPrivate()
{
return $this->container->getParameter('debril_rss_atom.private_feeds');
}
/**
* @return \FeedIo\FeedIo
*/
protected function getFeedIo()
{
return $this->container->get('feedio');
}
}
DebrilRssAtomBundle.php 0000666 00000000207 13115630631 0011123 0 ustar 00 root('debril_rss_atom')
->children()
->booleanNode('private')
->info('Change cache headers so the RSS feed is not cached by public caches (like reverse-proxies...).')
->defaultValue(false)
->end()
->arrayNode('date_formats')
->prototype('scalar')->end()
->end()
->end()
;
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
}
DependencyInjection/DebrilRssAtomExtension.php 0000666 00000005100 13115630631 0015604 0 ustar 00 processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
$this->setDateFormats($container, $config);
$container->setParameter('debril_rss_atom.private_feeds', $config['private']);
}
/**
* @param ContainerBuilder $container
* @param array $config
* @return $this
*/
protected function setDateFormats(ContainerBuilder $container, array $config)
{
$dateFormats = isset($config['date_formats']) ?
array_merge($this->defaultDateFormats, $config['date_formats']):
$this->defaultDateFormats;
$container->setParameter(
'debril_rss_atom.date_formats',
$dateFormats
);
return $this;
}
/**
* @param ContainerBuilder $builder
*/
public function process(ContainerBuilder $container)
{
$this->setDefinition($container, 'logger', 'Psr\Log\NullLogger');
}
/**
* @param ContainerBuilder $container
* @param $serviceName
* @param $className
* @return $this
*/
protected function setDefinition(ContainerBuilder $container, $serviceName, $className)
{
if ( ! $container->has($serviceName) ) {
$container->setDefinition($serviceName, new Definition($className));
}
return $this;
}
}
Exception/DriverUnreachableResourceException.php 0000666 00000000550 13115630631 0016172 0 ustar 00
*/
class FeedException extends RssAtomException
{
}
Exception/FeedException/FeedCannotBeReadException.php 0000666 00000000556 13115630631 0016676 0 ustar 00
*/
class RssAtomException extends \RuntimeException
{
}
LICENSE 0000666 00000001374 13115630631 0005561 0 ustar 00 RSS and Atom support for Symfony
Copyright (C) 2013 Alexandre Debril
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
For full license :
Provider/DoctrineFeedContentProvider.php 0000666 00000004550 13115630631 0014457 0 ustar 00 doctrine = $doctrine;
}
/**
* Returns the name of the doctrine repository.
*
* @return string
*/
public function getRepositoryName()
{
return $this->repositoryName;
}
/**
* Sets the doctrine's repository name.
*
* @param string $repositoryName
*
* @return DoctrineFeedContentProvider
*/
public function setRepositoryName($repositoryName)
{
$this->repositoryName = $repositoryName;
return $this;
}
/**
* @param array $options
*
* @return FeedInterface
*
* @throws FeedNotFoundException
*/
public function getFeedContent(array $options)
{
// fetch feed from data repository
$feed = $this->getDoctrine()
->getManager()
->getRepository($this->getRepositoryName())
->findOneById($this->getIdFromOptions($options));
// if the feed is an actual FeedInterface instance, then return it
if ($feed instanceof FeedInterface) {
return $feed;
}
// $feed is null, which means no Feed was found with this id.
throw new FeedNotFoundException();
}
/**
* @return Registry
*/
public function getDoctrine()
{
return $this->doctrine;
}
/**
* @param array $options
*
* @return mixed
*/
public function getIdFromOptions(array $options)
{
$optionsResolver = new OptionsResolver();
$optionsResolver->setRequired('id');
$options = $optionsResolver->resolve($options);
return $options['id'];
}
}
Provider/FeedContentProviderInterface.php 0000666 00000001115 13115630631 0014602 0 ustar 00 setPublicId($id);
$feed->setTitle('thank you for using RssAtomBundle');
$feed->setDescription('this is the mock FeedContent');
$feed->setLink('https://raw.github.com/alexdebril/rss-atom-bundle/');
$feed->setLastModified(new \DateTime());
return $this->addItem($feed);
}
/**
* @param Feed $feed
* @return Feed
*/
protected function addItem(Feed $feed)
{
$item = new Item();
$item->setPublicId('1');
$item->setLink('https://raw.github.com/alexdebril/rss-atom-bundle/somelink');
$item->setTitle('This is an item');
$item->setDescription('this stream was generated using the MockProvider class');
$item->setLastModified(new \DateTime());
$feed->add($item);
return $feed;
}
}
README.md 0000666 00000027510 13115630631 0006033 0 ustar 00 # RssAtomBundle - Read and Build Atom/RSS feeds
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/9e0b1301-d7a5-49fd-916b-49da544389ac/big.png)](https://insight.sensiolabs.com/projects/9e0b1301-d7a5-49fd-916b-49da544389ac)
[![Latest Stable Version](https://poser.pugx.org/debril/rss-atom-bundle/v/stable.png)](https://packagist.org/packages/debril/rss-atom-bundle)
[![Download Count](https://poser.pugx.org/debril/rss-atom-bundle/d/total)](https://packagist.org/packages/debril/rss-atom-bundle)
[![Build Status](https://secure.travis-ci.org/alexdebril/rss-atom-bundle.png?branch=master)](http://travis-ci.org/alexdebril/rss-atom-bundle)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/alexdebril/rss-atom-bundle/badges/quality-score.png?s=6e4cc3b9368ddbf14b1066114b6af6d9011894d9)](https://scrutinizer-ci.com/g/alexdebril/rss-atom-bundle/)
[![Code Coverage](https://scrutinizer-ci.com/g/alexdebril/rss-atom-bundle/badges/coverage.png?s=5bbd191f3b9364b8c31d8f1881f4c1fd06829fc3)](https://scrutinizer-ci.com/g/alexdebril/rss-atom-bundle/)
RssAtomBundle is a Bundle for Symfony made to easily access and deliver RSS / Atom feeds. It is built on top of [feed-io](https://github.com/alexdebril/feed-io) and features:
- Detection of the feed format (RSS / Atom)
- enclosures support
- A generic StreamController built to write all your feeds. This controller is able to send a 304 HTTP Code if the feed didn't change since the last visit
- HTTP Headers support when reading feeds in order to save network traffic
- Content filtering to fetch only the newest items
- multiple feeds writing
- Ability to use doctrine as a data source
- PSR compliant logging
- DateTime detection and conversion
- Guzzle Client integration
Keep informed about new releases and incoming features : http://debril.org/category/rss-atom-bundle
You can try rss-atom-bundle through its [Demo](https://rss-atom-demo.herokuapp.com/).
## Version 2.5 is a transitional version
You may notice that a lot of code is now deprecated. It's because feed-io will totally replace most of it in version 3.0, please refer to the [upgrade guide](UPGRADE-3.0.md).
## Installation
### Dependencies
As a Symfony Bundle, RssAtomBundle must be installed using Composer. If you do not know Composer, please refer to its website: http://getcomposer.org/
### Installation in a Symfony project
This is the most common way if you want to add RssAtomBundle into an existing project.
composer require debril/rss-atom-bundle
Edit your app/AppKernel.php to register the bundle in the registerBundles() method as above:
```php
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
// ...
// register the bundle here
new Debril\RssAtomBundle\DebrilRssAtomBundle(),
```
Then add the bundle's routing configuration in app/config/routing.yml :
```yaml
rssatom:
resource: "@DebrilRssAtomBundle/Resources/config/routing.xml"
```
### Fetching the repository
Do this if you want to contribute (and you're welcome to do so):
git clone https://github.com/alexdebril/rss-atom-bundle.git
composer.phar install --dev
## Unit Testing
You can run the unit test suites using the following command in the Bundle's source director:
bin/phpunit
## Usage
rss-atom-bundle is designed to read feeds across the internet and to publish your own using [feed-io](https://github.com/alexdebril/feed-io)
feed-io provides two interfaces, each one being dedicated to feed's consuming and publishing :
- [FeedInterface](https://github.com/alexdebril/feed-io/blob/master/src/FeedIo/FeedInterface.php) to handle the feed
- [ItemInterface](https://github.com/alexdebril/feed-io/blob/master/src/FeedIo/Feed/ItemInterface.php) to handle feed's items
### Feed Reading
To read a feed you need to use the `feedio` service which provides two methods for that : `read()` and `readSince()`. This service is based upon [FeedIo](https://github.com/alexdebril/feed-io/blob/master/src/FeedIo/FeedIo.php).
#### using read()
`read()` is designed to give a brand new Feed instance or any object of your own, as long as it implements the [FeedInterface](https://github.com/alexdebril/feed-io/blob/master/src/FeedIo/FeedInterface.php) interface. It takes three arguments :
- `$url` : URL of the RSS/Atom feed you want to read (eg: http://php.net/feed.atom)
- `$feed` (optional) : a FeedInterface instance. The default is a new `\FeedIo\Feed` instance
- `$modifiedSince` (optional) : the last time you read this feed. This is useful to fetch only the articles which were published after your last hit.
Wherever you have access to the service container :
```php
container->get('feedio');
// this date is used to fetch only the latest items
$modifiedSince = new \DateTime($date);
// the feed you want to read
$url = 'http://host.tld/feed';
// now fetch its (fresh) content
$feed = $feedIo->read($url, new \Acme\Entity\Feed, $modifiedSince)->getFeed();
foreach ( $feed as $item ) {
echo "item title : {$item->getTitle()} \n ";
// getMedias() returns enclosures if any
$medias = $item->getMedias();
}
?>
```
`read()` fetches the feed hosted at `$url` and removes items prior to `$modifiedSince`. If it is the first time you read this feed, then you must specify a date far enough in the past to keep all the items. This method does not loop until the `$modifiedSince` is reached, it justs performs one hit and filters the response to keep only the fresh articles.
#### using readSince()
`readSince()` helps you get a `\FeedIo\Feed` without creating its instance :
```php
container->get('feedio');
// this date is used to fetch only the latest items
$modifiedSince = new \DateTime($date);
// the feed you want to read
$url = 'http://host.tld/feed';
// now fetch its (fresh) content
$feed = $feedIo->readSince($url, $modifiedSince)->getFeed();
?>
```
### Providing feeds
RssAtomBundle offers the ability to provide RSS/Atom feeds. The route will match the following pattern : /{format}/{contentId}
- {format} must be "rss" or "atom" (or whatever you want if you add the good routing rule in routing.yml)
- {contentId} is an optional argument. Use it you have several feeds
The request will be handled by `StreamController`, according to the following steps :
- 1 : grabs the ModifiedSince header if it exists
- 2 : creates an `Options` instance holding the request's parameters (contentId if it exists)
- 3 : gets the provider defined in services.yml and calls the `getFeedContent(Options $options)` method
- 4 : compare the feed's LastModified property with the ModifiedSince header
- 5 : if LastModified is prior or equal to ModifiedSince then the response contains only a "NotModified" header and the 304 code. Otherwise, the stream is built and sent to the client
StreamController expects the getFeedContent()'s return value to be a FeedInterface instance. It can be a `FeedIo\Feed` or a class you wrote and if so, your class MUST implement `\FeedIo\FeedInterface`.
```php
```
Now, how to plug the `StreamController` with the provider of your choice ? The easiest way is to override the `debril.provider.default` service with your own in services.xml :
```xml
```
Your class just needs to implement the `FeedContentProviderInterface` interface :
```php
interface FeedContentProviderInterface
{
/**
* @param \Symfony\Component\OptionsResolver $params
* @return \FeedIo\FeedInterface
* @throws \Debril\RssAtomBundle\Exception\FeedNotFoundException
*/
public function getFeedContent(Options $options);
}
```
If the reclaimed feed does not exist, you just need to throw a FeedNotFoundException to make the StreamController answer with a 404 error. Otherwise, `getFeedContent(Options $options)` must return a `\FeedIo\FeedInterface` instance. Then, the controller properly turns the object into a XML stream.
More information on the FeedContentProviderInterface interface and how to interface rss-atom-bundle directly with doctrine can be found in the [Providing Feeds section](https://github.com/alexdebril/rss-atom-bundle/wiki/Providing-feeds)
## Override tip
It could happen that according to the order of the bundles registered in `AppKernel`, this override procedures do not work properly. This happens when a bundle is registered before `rss-atom-bundle`.
In this case, you should use the Symfony `CompilerPass` as reported in the [documentation](http://symfony.com/doc/current/bundles/override.html#services-configuration).
`Vendor/Bundle/VendorBundle.php`:
```php
use Vendor\Bundle\DependencyInjection\Compiler\OverrideRssAtomBundleProviderCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class VendorBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OverrideRssAtomBundleProviderCompilerPass());
}
}
```
and `Vendor/Bundle/DependencyInjection/Compiler/OverrideRssAtomBundleProviderCompilerPass.php`:
```php
use Vendor\Bundle\Provider\FeedProvider;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class OverrideRssAtomBundleProviderCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('debril.provider.default');
$definition->setClass(FeedProvider::class);
$definition->addArgument(new Reference('my.service1'));
$definition->addArgument(new Reference('my.service2'));
}
}
```
You can follow either `services.xml` or `CompilerPass` but with services, you have to pay attention to bundles registration order.
## Useful Tips
### Skipping 304 HTTP Code
The HTTP cache handling can be annoying during development process, you can skip it through configuration in your app/config/parameters.yml file :
```yml
parameters:
force_refresh: true
```
This way, the `StreamController` will always display your feed's content and return a 200 HTTP code.
### Choosing your own provider
Need to keep the existing routes and add one mapped to a different FeedProvider ? add it own in your routing file :
```xml
DebrilRssAtomBundle:Stream:index
rss
your.provider.service
```
The `source` parameter must contain a valid service name defined in your application.
### Private feeds
You may have private feeds, user-specific or behind some authentication.
In that case, you don't want to `Cache-Control: public` header to be added, not to have your feed cached by a reverse-proxy (such as Symfony AppCache or Varnish).
You can do so by setting `private` parameter to `true` in config:
```yml
debril_rss_atom:
private: true
```
### Adding non-standard date formats
Some feeds use date formats which are not compliant with the specifications. You can fix this by adding the format in your configuration
```yml
# app/config/config.yml
debril_rss_atom:
date_formats:
- 'Y/M/d'
```
Resources/config/routing.xml 0000666 00000002251 13115630631 0012177 0 ustar 00
DebrilRssAtomBundle:Stream:index
null
DebrilRssAtomBundle:Stream:index
atom
null
Resources/config/services.yml 0000666 00000001425 13115630631 0012336 0 ustar 00 parameters:
debril.provider.mock.class: Debril\RssAtomBundle\Provider\MockProvider
debril.provider.default.class: Debril\RssAtomBundle\Provider\MockProvider
debril.provider.doctrine.class: Debril\RssAtomBundle\Provider\DoctrineFeedContentProvider
services:
guzzle.client:
class: GuzzleHttp\Client
feedio.client:
class: FeedIo\Adapter\Guzzle\Client
arguments: ["@guzzle.client"]
feedio:
class: FeedIo\FeedIo
arguments: ["@feedio.client", "@logger"]
calls:
- method: addDateFormats
arguments:
- '%debril_rss_atom.date_formats%'
debril.provider.default:
class: '%debril.provider.default.class%'
debril.provider.mock:
class: '%debril.provider.mock.class%'
Resources/doc/feed-publishing.odg 0000666 00000027512 13115630631 0013035 0 ustar 00 PK PB.+ + mimetypeapplication/vnd.oasis.opendocument.graphicsPK PB}Lׇ Thumbnails/thumbnail.pngPNG
IHDR ?`C NIDATxil白:'8;qu9U6DD(
(-H)EUd!!@4R"}A`aqu$8v|1{=3O:뭏nv]Z&;`B0 @,!
i HC @ 4 !
i HC @ 4 !
i HC @ 4 !
i HC @ 4 !
i HC @ 4 !
i HC @ 4 !
i HC @ 4 !
i HC ²y6k$@ @xԮ* 5]g-iļ5U\K gX^E1}@Դ߸ݏviSSS^<#.f#C4t{W[_z]joW?/5q ۷S?ֳ_X,==ƽyyKX{