fastfeed/fastfeed-bundle
Symfony bundle wrapping the FastFeed library for fast, simple RSS and Atom handling. Parse incoming feeds, build and serve your own, and manage feed items through a lightweight API, making it easy to add syndication to Symfony applications.
1. Install and check compatibility first
composer require fastfeed/fastfeed-bundle
Before writing a line of code, open vendor/fastfeed/fastfeed-bundle/composer.json and read the require block. This bundle is archived and was written against the Symfony 2.x era, so it will almost certainly list constraints like symfony/framework-bundle: ~2.3 and php: >=5.4. If you are on Symfony 5/6/7 you will get a Composer solver conflict; you have three options, in order of preference:
composer require --ignore-platform-reqs (fragile; the bundle will load but any ContainerAware, getDefinition, or DefinitionDecorator-style API it uses may throw at container compile time).2. Register the bundle
There is no Flex recipe. Add it manually:
// config/bundles.php (Symfony 4+)
return [
// ...
FastFeed\Bundle\FastFeedBundle::class => ['all' => true],
];
3. Where to look first
Two files tell you everything about how to use this bundle:
DependencyInjection/FastFeedExtension.php — reveals the config tree (usually just reader/parser toggles and maybe a cache service).Resources/config/services.xml (or .yml) — reveals the service IDs you will inject. Expect something like fastfeed.aggregator, fastfeed.reader.rss, fastfeed.parser.Run bin/console debug:container fastfeed --show-private to confirm the real IDs and their class names before guessing.
4. First use case: fetch one feed
The bundle's whole value proposition is that it wires FastFeed's Aggregator for you. Inject it and go:
use FastFeed\Aggregator;
final class FeedPreviewCommand extends Command
{
protected static $defaultName = 'app:feed:preview';
public function __construct(private Aggregator $aggregator)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$feed = $this->aggregator->getFeed('https://example.com/rss.xml');
foreach ($feed->getItems() as $item) {
$output->writeln(sprintf('%s — %s', $item->getName(), $item->getLink()));
}
return Command::SUCCESS;
}
}
If the service ID is not autowirable (likely, given its age), alias it explicitly:
# config/services.yaml
services:
FastFeed\Aggregator:
alias: fastfeed.aggregator
That is the single most useful line you will write for this bundle: it converts an old string-ID service into something modern Symfony autowiring can consume.
Wrap the Aggregator, don't sprinkle it
Do not inject Aggregator into controllers or message handlers directly. The library has no caching, no retry, no logging, and no timeout policy. Create one thin application service that owns those concerns:
final class FeedFetcher
{
public function __construct(
private Aggregator $aggregator,
private CacheInterface $cache,
private LoggerInterface $logger,
) {}
/** @return iterable<\FastFeed\Item> */
public function fetch(string $url): iterable
{
$key = 'feed.'.sha1($url);
return $this->cache->get($key, function (ItemInterface $item) use ($url) {
$item->expiresAfter(900);
try {
return $this->aggregator->getFeed($url)->getItems();
} catch (\Throwable $e) {
$this->logger->warning('Feed fetch failed', ['url' => $url, 'e' => $e]);
return [];
}
});
}
}
Feed sources are slow, flaky, and rate-limited by third parties. Caching in front of the aggregator is not an optimization here, it is a correctness requirement.
Poll → dedupe → dispatch, as three separate steps
The pattern that scales is: a console command iterates over a table of feed subscriptions, calls FeedFetcher, and for each Item computes a stable identity (getGuid() if present, else the link URL, else a hash of name + date) and inserts into a feed_item table with a unique constraint. Only newly inserted rows get dispatched as a Messenger message:
// inside the polling command
$conn->transactional(function () use ($items, $bus, $source) {
foreach ($items as $item) {
if (!$this->repository->existsByExternalId($item->getGuid() ?? $item->getLink())) {
$this->repository->persist(FeedItemRecord::fromFastFeed($item, $source));
$bus->dispatch(new IngestFeedItem($item->getLink()));
}
}
});
The unique-constraint-plus-swallowed-violation approach is more robust than a pre-check under concurrency; use it if the poller can run in parallel.
Map to domain immediately
FastFeed's Item is a mutable value object exposing array-access and magic getters. Do not let it leak past your fetch layer — the moment you upgrade or replace the library (likely, given the archive status), every consumer breaks. Convert to a small readonly DTO in FeedFetcher::fetch().
Register custom readers/parsers via the container, not by new
If the bundle exposes a compiler pass or a tagged-service convention for adding ReaderInterface / ParserInterface implementations (check for addCompilerPass in FastFeedBundle::build()), use it — that is the intended extension point. If it does not, define your own compiler pass adding method calls to the fastfeed.aggregator definition:
$container->getDefinition('fastfeed.aggregator')
->addMethodCall('addReader', [new Reference(MyReader::class)]);
Testing
Because the aggregator is a normal service, override it in config/services_test.yaml with a stub returning fixture Item objects. That keeps every downstream test offline and fast, which matters a lot here since the library's HTTP layer predates HttpClient mocks.
The archive status is the headline. No security patches, no PHP 8.2+ fixes, no Symfony compatibility work — ever. Treat the dependency as frozen code you now own. Budget for a fork on day one rather than discovering the problem at your next Symfony major upgrade. For greenfield work, use debril/rss-atom-bundle or laminas/laminas-feed instead; they are maintained and handle the same problem space with better modern Symfony integration.
HTTP behaviour is inherited from the library, not Symfony. FastFeed does its own fetching. That means no framework.http_client defaults, no proxy env-var support, no PSR-18 client, and possibly no configurable timeout. Verify by reading the reader class. If there is no timeout, a single dead feed host will hang your poller — isolate fetches in Messenger workers with your own pcntl_alarm/SIGALRM guard or a Process wrapper, and cap the batch size per run.
Service IDs are string-based and private-by-default on modern Symfony. The classic failure is ServiceNotFoundException: fastfeed.aggregator — usually because the ID is actually fastfeed or fastfeed.aggregator.default. Always resolve via debug:container rather than copying IDs from a nine-year-old README. Then alias to the class name so your code never depends on the ID again.
Encoding and date parsing are the two most common data bugs. Feeds in the wild are frequently non-UTF-8 (Windows-1252, GB2312) and carry malformed RFC 822 dates. Sanitize at the boundary: run mb_convert_encoding($name, 'UTF-8', 'auto') and new \DateTimeImmutable($item->getDate() ?: 'now') inside a try/catch, and persist the raw payload alongside the parsed fields so you can re-parse without re-fetching when a bug shows up.
Debugging tips. bin/console debug:container --tag=fastfeed and debug:container --show-private fastfeed reveal what the extension actually registered. If the bundle fails at compile time with a DefinitionDecorator or ContainerAware error, you are on a Symfony version the bundle predates — stop there and fork rather than patching the vendor directory.
Config quirks. The extension likely has no semantic config at all (many bundles of this vintage just register services unconditionally). If config/packages/fastfeed.yaml gives "unrecognized option", that is expected — you configure by overriding service definitions, not by extension config.
Extension points, in order of preference: (1) alias/override the aggregator service definition, (2) your own compiler pass to add readers/parsers, (3) decorate the aggregator in your own class and alias the original ID to your decorator so third-party code still works. Avoid subclassing the bundle's classes; there is no upstream to merge your fixes into.
How can I help you explore Laravel packages today?