0xe1f/grouch-rss

RSS generators for Grouch

★ 0Forks 0PHPGitHub ↗Compare

README

grouch-rss

A lightweight PHP application that generates RSS feeds for providers that don't generate their own.

For supported list, see parsers.

Local development

All local work runs inside Docker.

# Start a live dev server at http://localhost:8080
docker compose -f docker/docker-compose.yml up server

# Run golden (fixture-driven) tests
docker compose -f docker/docker-compose.yml run --rm test-golden

# Run live (network) sanity tests
docker compose -f docker/docker-compose.yml run --rm test-live

The dev server reads the token from the FEED_TOKEN environment variable, which is set to dev in docker-compose.yml. Visit the feed index or pull a specific feed:

http://localhost:8080/?token=dev          # HTML index listing all feeds
http://localhost:8080/ac-movies?token=dev # specific feed

Configuration

Copy config.php.example to config.php and set a secret token:

define('FEED_TOKEN', 'your-secret-here');

config.php is never committed. In the Docker dev environment, FEED_TOKEN=dev is injected automatically.

Authentication

Every request must include the token via either:

  • Authorization header: Authorization: Bearer <token>
  • Query parameter: ?token=<token> (URL-encode the value)

Deployment

# Deploy to a remote server (copies config.php by default)
./deploy.sh [email protected]:public_html/feeds

# Deploy without overwriting an existing config.php on the server
./deploy.sh --skip-config [email protected]:public_html/feeds

# Deploy to a local directory
./deploy.sh /var/www/html/feeds

Requires SSH key-based access. Sets 755 on directories and 644 on files after transfer.

Writing a new parser

1. Create the parser class

Add src/parsers/YourParser.php implementing Grouch\Contract\ParserInterface. Declare a ROUTE constant for the URL path segment the feed will be served at:

<?php

declare(strict_types=1);

namespace Grouch\parsers;

use DateTimeImmutable;
use Grouch\Contract\ParseEntry;
use Grouch\Contract\ParserInterface;
use Grouch\Contract\ParseResult;

class YourParser implements ParserInterface
{
    public const string ROUTE = 'your-feed';

    public function parse(string $feedUrl, callable $fetch): ParseResult
    {
        // $fetch is fn(string $url): string
        // Use it to retrieve remote content — tests will inject a mock.
        $body = $fetch('https://example.com/api/events.json');
        $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);

        $entries = [];
        foreach ($data['events'] as $event) {
            $entries[] = new ParseEntry(
                guid:        $event['id'],
                title:       $event['title'],
                url:         $event['url'],
                publishedAt: new DateTimeImmutable($event['date']),
                html:        '<p>' . htmlspecialchars($event['body'], ENT_XML1) . '</p>',
                summary:     $event['excerpt'],
                author:      '',
            );
        }

        return new ParseResult(
            title:       'Your Feed Title',
            feedUrl:     $feedUrl,
            siteUrl:     'https://example.com',
            description: '',
            entries:     $entries,
        );
    }
}

ParseEntry fields:

Field Type Required Notes
guid string Yes Stable, unique identifier for the item
title string Yes
url string Yes Canonical permalink
publishedAt DateTimeInterface Yes
html string No Rich body; takes precedence over summary in XML
summary string No Plain-text fallback when html is empty
author string No

2. Register the parser

Add an entry to the $parsers map in index.php:

$parsers = [
    // existing parsers …
    'your-feed' => ['class' => \Grouch\parsers\YourParser::class, 'name' => 'Your Feed Name'],
];

name is the human-readable label shown in the HTML index and in feed reader subscription dialogs. The feed will be available at /your-feed?token=….

3. Create the source fixture

Capture a representative API response and save it under tests/fixtures/:

tests/fixtures/yourparser_source.json   (or .html, depending on format)

4. Generate the expected fixture

docker compose -f docker/docker-compose.yml run --rm test-golden \
    php tests/generate_fixtures.php

Inspect tests/fixtures/yourparser_expected.json, then commit both fixtures.

5. Write the golden test

// tests/YourParserGoldenTest.php
namespace Grouch\Tests;

use Grouch\parsers\YourParser;

class YourParserGoldenTest extends GoldenTestCase
{
    protected function getResult(): \Grouch\Contract\ParseResult
    {
        $source = file_get_contents(__DIR__ . '/fixtures/yourparser_source.json');
        $parser = new YourParser();
        return $parser->parse('https://example.com/your-feed', fn($_) => $source);
    }

    protected function getExpectedFile(): string
    {
        return __DIR__ . '/fixtures/yourparser_expected.json';
    }
}

Register it in phpunit.xml:

<testsuite name="golden">
    <!-- existing entries … -->
    <file>tests/YourParserGoldenTest.php</file>
</testsuite>

6. Write the live test

// tests/YourParserLiveTest.php
namespace Grouch\Tests;

use Grouch\Contract\ParserInterface;
use Grouch\parsers\YourParser;

class YourParserLiveTest extends LiveTestCase
{
    protected function getParser(): ParserInterface
    {
        return new YourParser();
    }

    protected function getFeedUrl(): string
    {
        return 'https://example.com/your-feed';
    }
}

Register it in phpunit.xml under the live testsuite.

Architecture

See [docs/plans/architecture.md](docs/plans/architecture.md).

License

Copyright 2026 Akop Karapetyan. Licensed under the Apache License, Version 2.0.

Contributors

0xe1f

Issues