Developer corner

Introduction

This extension provides classes and interfaces to implement feeds in different formats. You don't have to worry about the details of the feeds and how they are build, you only have to provide the data from your concrete model.

Example

Let's start with an example to warm up.

EXT:your_extension/Classes/Feed/YourFeed.php
<?php
declare(strict_types=1);

namespace YourVender\YourExtension\Feed;

use Brotkrueml\FeedGenerator\Attributes\Feed;
use Brotkrueml\FeedGenerator\Contract\FeedInterface;
use Brotkrueml\FeedGenerator\Contract\ImageInterface
use Brotkrueml\FeedGenerator\Entity\Author;
use Brotkrueml\FeedGenerator\Entity\Item;
use Brotkrueml\FeedGenerator\Format\FeedFormat;

#[Feed('/your-feed.atom', FeedFormat::ATOM)]
final class YourFeed implements FeedInterface
{
   public function getId(): string
   {
      return '';
   }

   public function getTitle(): string
   {
      return 'Your website title';
   }

   public function getDescription(): string
   {
      return 'Here comes the Atom feed for your website.';
   }

   public function getLink(): string
   {
      return 'https://example.com/';
   }

   public function getAuthors(): array
   {
      return [new Author('Your Company')];
   }

   public function getDatePublished(): ?\DateTimeInterface
   {
      return null;
   }

   // If the method returns an implementation of the DateTimeInterface it is
   // also used for the "Last-Modified" header in the HTTP response.
   public function getDateModified(): ?\DateTimeInterface
   {
      return new \DateTimeImmutable();
   }

   public function getLastBuildDate(): ?\DateTimeInterface
   {
      return null;
   }

   public function getLanguage(): string
   {
      return 'en';
   }

   public function getCopyright(): string
   {
      return '';
   }

   public function getImage(): ?ImageInterface;
   {
      return new Image('https://example.com/fileadmin/your-logo.png');
   }

   public function getItems(): array
   {
      return [
         (new Item())
            ->setTitle('Another awesome article')
            ->setDateModified(new \DateTimeImmutable('2022-06-07T18:22:00+02:00'))
            ->setLink('https://example.com/another-awesome-article'),
         (new Item())
            ->setTitle('Some awesome article'),
            ->setDateModified(new \DateTimeImmutable('2022-02-20T20:06:00+01:00')),
            ->setLink('https://example.com/some-awesome-article'),
      ];
   }
}

First, a class which provides the data for one or more feeds must implement the Brotkrueml\FeedGenerator\Contract\FeedInterface interface. This marks the class as a feed data provider and requires some methods to be implemented. Of course, you can use dependency injection to inject service classes, for example, a repository that provides the needed items.

To define under which URL a feed is available and which format should be used you have to provide at least one Brotkrueml\FeedGenerator\Attributes\Feed class attribute. As format a name of the Brotkrueml\FeedGenerator\Attributes\Feed enum is used which defines the according format.

The getItems() method returns an array of Brotkrueml\FeedGenerator\Entity\Item entities.

Note

Based on the FeedInterface the feed is automatically registered if autoconfigure is enabled in Configuration/Services.yaml. Alternatively, you can manually tag a feed:

EXT:your_extension/Configuration/Services.yaml
services:
   YourVender\YourExtension\Feed\YourFeed:
      tags:
         - name: tx_feed_generator.feed

Important

After adding a class which implements FeedInterface or adjusting the class attributes the DI cache has to be flushed.

Note

Not all properties are used in every format. For example, the language of the feed is only available in an RSS feed and not in an Atom feed.

A list of all configured feeds is available in the Configurations module.

Interfaces

Four interfaces are available and of interest:

FeedInterface

The Brotkrueml\FeedGenerator\Contract\FeedInterface marks the feed – well – as a feed and requires the implementation of some methods like in the example above.

FeedFormatAwareInterface

When implementing the Brotkrueml\FeedGenerator\Contract\FeedFormatAwareInterface, you can access the feed format of the current request. This is helpful if you define a feed implementation with different formats and want to adjust some values according to the format.

EXT:your_extension/Classes/Feed/YourFeed.php
// use Brotkrueml\FeedGenerator\Contract\FeedFormatAwareInterface
// use Brotkrueml\FeedGenerator\Format\FeedFormat

#[Feed('/your-feed.atom', FeedFormat::ATOM)]
#[Feed('/your-feed.json', FeedFormat::JSON)]
#[Feed('/your-feed.rss', FeedFormat::RSS)]
final class YourFeed implements FeedInterface, FeedFormatAwareInterface
{
   private FeedFormat $format;

   public function setFormat(FeedFormat $format): void
   {
      $this->format = $format;
   }

   public function getDescription(): string
   {
       return match ($this->format) {
           FeedFormat::ATOM => 'Here comes the Atom feed for your website.',
           FeedFormat::JSON => 'Here comes the JSON feed for your website.',
           FeedFormat::RSS => 'Here comes the RSS feed for your website.'
       };
   }

   // ... the other methods from the introduction example are untouched
}

FeedCategoryInterface

The Brotkrueml\FeedGenerator\Contract\FeedCategoryInterface can be added when one or more categories should be applied to a feed. It requires the implementation of a getCategories() method that returns an array of categories.

EXT:your_extension/Classes/Feed/YourFeed.php
// use Brotkrueml\FeedGenerator\Contract\CategoryInterface;
// use Brotkrueml\FeedGenerator\Contract\FeedCategoryInterface;
// use Brotkrueml\FeedGenerator\Entity\Category;

#[Feed('/your-feed.atom', FeedFormat::ATOM)]
final class YourFeed implements FeedInterface, FeedCategoryInterface
{
   /**
    * @return CategoryInterface[]
    */
   public function getCategories(): array
   {
      return [
         new Category('some-term', 'https://example.org/some-term'),
         new Category('another-term', 'https://example.org/another-term', 'Another term'),
      ];
   }

   // ... the other methods from the introduction example are untouched
}

RequestAwareInterface

The Brotkrueml\FeedGenerator\Contract\RequestAwareInterface injects the PSR-7 request object via a setRequest() method, which must be implemented by yourself.

This way you have access to request attributes, such as normalised parameters, the site or the language information:

EXT:your_extension/Classes/Feed/YourFeed.php
// use Brotkrueml\FeedGenerator\Contract\RequestAwareInterface;
// use Psr\Http\Message\ServerRequestInterface;

#[Feed('/your-feed.atom', FeedFormat::ATOM)]
final class YourFeed implements FeedInterface, RequestAwareInterface
{
   private ServerRequestInterface $request;

   public function setRequest(ServerRequestInterface $request): void
   {
      $this->request = $request;
   }

   public function getLink(): string
   {
      return $this->request->getAttribute('normalizedParams')->getSiteUrl();
   }

   public function getItems(): array
   {
      $router = $this->request->getAttribute('site')->getRouter();

      return [
         (new Item())
            ->setTitle('Another awesome article')
            ->setDateModified(new \DateTimeImmutable('2022-06-07T18:22:00+02:00'))
            ->setLink((string)$router->generateUri(43)),
         (new Item())
            ->setTitle('Some awesome article')
            ->setDateModified(new \DateTimeImmutable('2022-02-20T20:06:00+01:00'))
            ->setLink((string)$router->generateUri(42)),
         ),
      ];
   }

   // ... the other methods from the introduction example are untouched
}

StyleSheetInterface

The Brotkrueml\FeedGenerator\Contract\StyleSheetInterface requires the implementation of a getStyleSheet() method that returns the path to an XSL stylesheet. In this way, the appearance of an Atom or RSS feed can be customised in a browser.

EXT:your_extension/Classes/Feed/YourFeed.php
// use Brotkrueml\FeedGenerator\Contract\StyleSheetInterface;

#[Feed('/your-feed.atom', FeedFormat::ATOM)]
final class YourFeed implements FeedInterface, StyleSheetInterface
{
   public function getStyleSheet(): string
   {
      return 'EXT:your_extension/Resources/Public/Xsl/Atom.xsl';
   }

   // ... the other methods from the introduction example are untouched
}

An XSL stylesheet is only useful for XML feeds (Atom and RSS). When providing a stylesheet for a JSON feed, it is ignored.

This extension comes with two XSL stylesheets, one for an Atom feed and one for an RSS feed, which can be used directly or copied and adapted to your needs:

  • Atom: EXT:feed_generator/Resources/Public/Xsl/Atom.xsl

  • RSS: EXT:feed_generator/Resources/Public/Xsl/Rss.xsl

Note

When adding an XSL stylesheet to an Atom or RSS feed, the content type of the HTTP response is changed to application/xml. This way Chrome and some other browsers apply the stylesheet correctly.

Multiple feeds

It is possible to define several feed formats for a class. In this case, it may be useful to implement the FeedFormatAwareInterface.

EXT:your_extension/Classes/Feed/YourFeed.php
#[Feed('/your-feed.atom', FeedFormat::ATOM)]
#[Feed('/your-feed.json', FeedFormat::JSON)]
#[Feed('/your-feed.rss', FeedFormat::RSS)]
final class YourFeed implements FeedInterface
{
   // ...
}

But it is also possible to add different paths with the same format:

EXT:your_extension/Classes/Feed/YourFeed.php
#[Feed('/en/your-feed.atom', FeedFormat::ATOM)]
#[Feed('/de/dein-feed.atom', FeedFormat::ATOM)]
#[Feed('/nl/je-feed.atom', FeedFormat::ATOM)]
final class YourFeed implements FeedInterface
{
   // ...
}

If the paths of a feed match the entry point configured in the site configuration, the PSR-7 request object attribute site is populated with the corresponding information (such as base path and language).

Multiple sites

For a multi-site installation, it may be necessary to restrict a feed to one or more sites. Simply add the site identifier(s) as a third argument to the Feed class attribute:

EXT:your_extension/Classes/Feed/YourFeed.php
#[Feed('/your-feed.atom', FeedFormat::ATOM, ['website', 'blog'])]
final class YourFeed implements FeedInterface
{
   // ...
}

Control of cache headers

The cache headers for a feed request can be influenced with the class attribute Brotkrueml\FeedGenerator\Attributes\Cache. One can pass the number in seconds that a feed should be cached by a browser or feed reader:

EXT:your_extension/Classes/Feed/YourFeed.php
// use Brotkrueml\FeedGenerator\Attributes\Cache

#[Feed('/your-feed.atom', FeedFormat::ATOM])]
#[Cache(3600)] // Cache for one hour
final class YourFeed implements FeedInterface
{
   // ...
}

To clarify that the number stands for seconds, you can also use a named argument:

EXT:your_extension/Classes/Feed/YourFeed.php
// use Brotkrueml\FeedGenerator\Attributes\Cache

#[Feed('/your-feed.atom', FeedFormat::ATOM)]
#[Cache(seconds: 3600)] // Cache for one hour
final class YourFeed implements FeedInterface
{
   // ...
}

Both examples will result in the following HTTP headers:

Cache-Control: max-age=3600
Expires: Mon, 20 Jun 2022 08:06:03 GMT

Hint

When developing with ddev, the nginx proxy used overwrites the Cache-Control and Expires headers set by the middleware. Use the URL 127.0.0.1:<port> to see the correct headers, for example:

curl -I https://127.0.0.1:49155/your-feed.atom

You can find out the current port number with the command ddev describe.

If you want to define the cache headers for all available feeds, you can also set them directly in the configuration of your Apache web server according to the content type:

.htaccess
# Apache module "mod_expires" has to be active

# For Atom feeds
ExpiresByType application/atom+xml "access plus 1 hour"
# For JSON feeds
ExpiresByType application/feed+json "access plus 1 hour"
# For RSS feeds
ExpiresByType application/rss+xml "access plus 1 hour"

Translations

When configuring a feed for different languages, it may be convenient to use translations from locallang.xlf files. One possible implementation could be:

EXT:your_extension/Classes/Feed/YourFeed.php
// use TYPO3\CMS\Core\Localization\LanguageService;
// use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

#[Feed('/en/your-feed.atom', FeedFormat::ATOM)]
#[Feed('/de/dein-feed.atom', FeedFormat::ATOM)]
#[Feed('/nl/je-feed.atom', FeedFormat::ATOM)]
final class YourFeed implements FeedInterface, RequestAwareInterface
{
   private ?LanguageService $languageService = null;

   public function __construct(
      private readonly LanguageServiceFactory $languageServiceFactory,
   ) {
   }

   public function getDescription(): string
   {
      // feed.description is defined in your extension's locallang.xlf
      return $this->translate('feed.description');
   }

   public function getTitle(): string
   {
      // feed.title is defined in your extension's locallang.xlf
      return $this->translate('feed.title');
   }

   private function translate(string $key): string
   {
      if ($this->languageService === null) {
         $this->languageService = $this->languageServiceFactory->createFromSiteLanguage(
            $this->request->getAttribute('language')
               ?? $this->request->getAttribute('site')->getDefaultLanguage();
         );
      }

      return $this->languageService->sL(
         'LLL:EXT:your_extension/Resources/Private/Language/locallang.xlf:' . $key
      );
   }

   // ... the other methods from the introduction example are untouched
}

Note

To get the correct language, the configured feed path must be in the defined entry point of the language in the site configuration.

Overview of used properties by feed type

Brotkrueml\FeedGenerator\Contract\AuthorInterface

Interface method

Atom tag

JSON property

RSS tag

getEmail()

<author><email>

getName()

<author><name>

author.name

<author>

getUri()

<author><uri>

author.url

<author>

Brotkrueml\FeedGenerator\Contract\CategoryInterface

Interface method

Atom tag

JSON property

RSS tag

getLabel()

<category label="...">

getScheme()

<category scheme="...">

<category domain="...">

getTerm()

<category term="...">

<category>

Brotkrueml\FeedGenerator\Contract\FeedInterface

Interface method

Atom tag

JSON property

RSS tag

getAuthors()

<author>

author (only one)

<author>

getCopyright()

<rights>

<copyright>

getDatePublished()

<pubDate>

getDateModified()

<updated>

getDescription()

<subtitle>

description

<description>

getId()

<id>

getImage()

<logo>

<image>

getItems()

<entry>

items

<item>

getLastBuildDate()

<lastBuildDate>

getLanguage()

<feed xml:lang="...">

<language>

getLink()

<link rel="alternate">

home_page_url

<link>

getTitle()

<title>

title

<title>

Brotkrueml\FeedGenerator\Contract\ImageInterface

Interface method

Atom tag

JSON property

RSS tag

getDescription()

<feed><image><description>

getHeight()

<feed><image><height>

getLink()

<feed><image><link>

getTitle()

<feed><image><title>

getUri()

<feed><logo>

<feed><image><url>

getWidth()

<feed><image><width>

Brotkrueml\FeedGenerator\Contract\ItemInterface

Interface method

Atom tag

JSON property

RSS tag

getAuthors()

<author>

author (only one)

<author>

getContent()

<content>

content_html

<content:encoded>

getDatePublished()

<published>

date_published

<pubDate>

getDateModified()

<updated>

date_modified

getDescription()

<summary>

summary

<description>

getEnclosure()

<link rel="enclosure" type="..." length="..." href="..."/>

<enclosure type="..." length="..." url="..."/>

getId()

<id>

id

<guid isPermaLink="false">

getLink()

<link rel="alternate" type="text/html" href="..."/>

url

<link>

getTitle()

<title>

title

<title>