-
Notifications
You must be signed in to change notification settings - Fork 94
Caching
Rumen Damyanov edited this page Jul 29, 2025
·
1 revision
This guide shows how to implement caching strategies to improve feed performance and reduce server load.
Feed generation can be resource-intensive, especially when:
- Fetching data from databases
- Processing large datasets
- Generating complex feeds with many items
- Handling high traffic volumes
Caching helps by:
- Reducing database queries
- Improving response times
- Lowering server resource usage
- Providing better user experience
Basic file caching implementation:
<?php
require 'vendor/autoload.php';
use Rumenx\Feed\FeedFactory;
class FileCachedFeed
{
private string $cacheDir;
private int $cacheTime;
public function __construct(string $cacheDir = './cache', int $cacheTime = 3600)
{
$this->cacheDir = $cacheDir;
$this->cacheTime = $cacheTime;
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
}
public function getFeed(string $feedType = 'main'): string
{
$cacheFile = $this->cacheDir . '/' . md5($feedType) . '.xml';
// Check if cache exists and is still valid
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $this->cacheTime) {
return file_get_contents($cacheFile);
}
// Generate new feed
$feedXml = $this->generateFeed($feedType);
// Save to cache
file_put_contents($cacheFile, $feedXml);
return $feedXml;
}
public function clearCache(string $feedType = null): void
{
if ($feedType) {
$cacheFile = $this->cacheDir . '/' . md5($feedType) . '.xml';
if (file_exists($cacheFile)) {
unlink($cacheFile);
}
} else {
// Clear all cache files
$files = glob($this->cacheDir . '/*.xml');
foreach ($files as $file) {
unlink($file);
}
}
}
private function generateFeed(string $feedType): string
{
$feed = FeedFactory::create();
$feed->setTitle('My Blog - ' . ucfirst($feedType));
$feed->setDescription('Latest content from my blog');
$feed->setLink('https://example.com');
// Simulate data fetching (replace with actual data source)
$items = $this->fetchData($feedType);
foreach ($items as $item) {
$feed->addItem($item);
}
return $feed->render('rss');
}
private function fetchData(string $feedType): array
{
// Simulate database call
return [
[
'title' => 'Sample Post - ' . $feedType,
'author' => 'Author Name',
'link' => 'https://example.com/sample-post',
'pubdate' => date('c'),
'description' => 'This is a sample post for ' . $feedType
]
];
}
}
// Usage
$cachedFeed = new FileCachedFeed('./cache', 1800); // 30 minutes cache
$feedType = $_GET['type'] ?? 'main';
header('Content-Type: application/rss+xml; charset=utf-8');
echo $cachedFeed->getFeed($feedType);
Using Redis for distributed caching:
<?php
require 'vendor/autoload.php';
use Rumenx\Feed\FeedFactory;
use Redis;
class RedisCachedFeed
{
private Redis $redis;
private int $cacheTime;
private string $keyPrefix;
public function __construct(Redis $redis, int $cacheTime = 3600, string $keyPrefix = 'feed:')
{
$this->redis = $redis;
$this->cacheTime = $cacheTime;
$this->keyPrefix = $keyPrefix;
}
public function getFeed(string $feedType, array $params = []): string
{
$cacheKey = $this->buildCacheKey($feedType, $params);
// Try to get from cache
$cachedFeed = $this->redis->get($cacheKey);
if ($cachedFeed !== false) {
return $cachedFeed;
}
// Generate new feed
$feedXml = $this->generateFeed($feedType, $params);
// Store in cache
$this->redis->setex($cacheKey, $this->cacheTime, $feedXml);
return $feedXml;
}
public function clearCache(string $feedType = null, array $params = []): void
{
if ($feedType) {
$cacheKey = $this->buildCacheKey($feedType, $params);
$this->redis->del($cacheKey);
} else {
// Clear all feed caches
$keys = $this->redis->keys($this->keyPrefix . '*');
if (!empty($keys)) {
$this->redis->del($keys);
}
}
}
public function warmCache(array $feedConfigs): void
{
foreach ($feedConfigs as $config) {
$this->getFeed($config['type'], $config['params'] ?? []);
}
}
private function buildCacheKey(string $feedType, array $params = []): string
{
$keyParts = [$this->keyPrefix, $feedType];
if (!empty($params)) {
ksort($params);
$keyParts[] = md5(serialize($params));
}
return implode(':', $keyParts);
}
private function generateFeed(string $feedType, array $params = []): string
{
$feed = FeedFactory::create();
// Configure feed based on type and params
switch ($feedType) {
case 'blog':
$this->configureBlogFeed($feed, $params);
break;
case 'news':
$this->configureNewsFeed($feed, $params);
break;
default:
$this->configureDefaultFeed($feed, $params);
break;
}
$items = $this->fetchData($feedType, $params);
foreach ($items as $item) {
$feed->addItem($item);
}
return $feed->render('rss');
}
private function configureBlogFeed(object $feed, array $params): void
{
$category = $params['category'] ?? null;
$feed->setTitle('My Blog' . ($category ? " - {$category}" : ''));
$feed->setDescription('Latest blog posts');
$feed->setLink('https://example.com/blog');
}
private function configureNewsFeed(object $feed, array $params): void
{
$feed->setTitle('My Website - News');
$feed->setDescription('Latest news updates');
$feed->setLink('https://example.com/news');
}
private function configureDefaultFeed(object $feed, array $params): void
{
$feed->setTitle('My Website');
$feed->setDescription('Latest updates');
$feed->setLink('https://example.com');
}
private function fetchData(string $feedType, array $params): array
{
// Implement actual data fetching logic here
return [
[
'title' => 'Sample Post',
'author' => 'Author',
'link' => 'https://example.com/sample',
'pubdate' => date('c'),
'description' => 'Sample description'
]
];
}
}
// Usage
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$cachedFeed = new RedisCachedFeed($redis, 1800); // 30 minutes
$feedType = $_GET['type'] ?? 'blog';
$params = [
'category' => $_GET['category'] ?? null,
'limit' => $_GET['limit'] ?? 20
];
header('Content-Type: application/rss+xml; charset=utf-8');
echo $cachedFeed->getFeed($feedType, array_filter($params));
Using Laravel's cache system:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Rumenx\Feed\FeedFactory;
use App\Models\Post;
class CachedFeedController extends Controller
{
public function posts(string $category = null): Response
{
$cacheKey = $this->buildCacheKey('posts', compact('category'));
$feedXml = Cache::remember($cacheKey, now()->addHours(1), function () use ($category) {
return $this->generatePostsFeed($category);
});
return response($feedXml, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=3600'
]);
}
public function clearCache(): Response
{
// Clear all feed caches
Cache::tags(['feeds'])->flush();
return response()->json(['message' => 'Feed cache cleared successfully']);
}
public function warmCache(): Response
{
// Pre-generate popular feeds
$feedConfigs = [
['type' => 'posts', 'params' => []],
['type' => 'posts', 'params' => ['category' => 'php']],
['type' => 'posts', 'params' => ['category' => 'laravel']],
['type' => 'news', 'params' => []],
];
foreach ($feedConfigs as $config) {
$this->generateFeed($config['type'], $config['params']);
}
return response()->json(['message' => 'Feed cache warmed successfully']);
}
private function generatePostsFeed(?string $category = null): string
{
$feed = FeedFactory::create();
$feed->setTitle(config('app.name') . ($category ? " - {$category}" : ''));
$feed->setDescription('Latest blog posts');
$feed->setLink(url('/'));
$query = Post::published()->latest()->limit(20);
if ($category) {
$query->whereHas('categories', function ($q) use ($category) {
$q->where('slug', $category);
});
}
$posts = $query->get();
foreach ($posts as $post) {
$feed->addItem([
'title' => $post->title,
'author' => $post->author->name,
'link' => route('posts.show', $post->slug),
'pubdate' => $post->created_at,
'description' => $post->excerpt,
'category' => $post->categories->pluck('name')->toArray()
]);
}
return $feed->render('rss');
}
private function buildCacheKey(string $type, array $params = []): string
{
$keyParts = ['feed', $type];
if (!empty($params)) {
ksort($params);
$keyParts[] = md5(serialize(array_filter($params)));
}
return implode(':', $keyParts);
}
private function generateFeed(string $type, array $params = []): string
{
$cacheKey = $this->buildCacheKey($type, $params);
return Cache::tags(['feeds'])->remember($cacheKey, now()->addHours(1), function () use ($type, $params) {
switch ($type) {
case 'posts':
return $this->generatePostsFeed($params['category'] ?? null);
case 'news':
return $this->generateNewsFeed();
default:
throw new \InvalidArgumentException("Unknown feed type: {$type}");
}
});
}
private function generateNewsFeed(): string
{
// Implementation for news feed
$feed = FeedFactory::create();
$feed->setTitle('News Feed');
$feed->setDescription('Latest news');
$feed->setLink(url('/news'));
// Add news items...
return $feed->render('rss');
}
}
Using Symfony's cache component:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Rumenx\Feed\FeedFactory;
use App\Repository\PostRepository;
class CachedFeedController extends AbstractController
{
#[Route('/feed/{type}/{category}', name: 'cached_feed', defaults: ['category' => null])]
public function feed(
CacheInterface $cache,
PostRepository $postRepository,
string $type = 'posts',
?string $category = null
): Response {
$cacheKey = sprintf('feed_%s_%s', $type, $category ?? 'all');
$feedXml = $cache->get($cacheKey, function (ItemInterface $item) use ($type, $category, $postRepository) {
$item->expiresAfter(3600); // 1 hour
$item->tag(['feeds', "feed_{$type}"]);
return $this->generateFeed($type, $category, $postRepository);
});
return new Response($feedXml, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=3600'
]);
}
#[Route('/admin/cache/feeds/clear', name: 'clear_feed_cache')]
public function clearCache(CacheInterface $cache): Response
{
$cache->invalidateTags(['feeds']);
return $this->json(['message' => 'Feed cache cleared successfully']);
}
#[Route('/admin/cache/feeds/warm', name: 'warm_feed_cache')]
public function warmCache(CacheInterface $cache, PostRepository $postRepository): Response
{
$feedConfigs = [
['type' => 'posts', 'category' => null],
['type' => 'posts', 'category' => 'php'],
['type' => 'posts', 'category' => 'symfony'],
];
foreach ($feedConfigs as $config) {
$cacheKey = sprintf('feed_%s_%s', $config['type'], $config['category'] ?? 'all');
$cache->get($cacheKey, function (ItemInterface $item) use ($config, $postRepository) {
$item->expiresAfter(3600);
$item->tag(['feeds', "feed_{$config['type']}"]);
return $this->generateFeed($config['type'], $config['category'], $postRepository);
});
}
return $this->json(['message' => 'Feed cache warmed successfully']);
}
private function generateFeed(string $type, ?string $category, PostRepository $postRepository): string
{
$feed = FeedFactory::create();
switch ($type) {
case 'posts':
$feed->setTitle($this->getParameter('app.name') . ($category ? " - {$category}" : ''));
$feed->setDescription('Latest blog posts');
$posts = $postRepository->findForFeed($category, 20);
break;
default:
throw new \InvalidArgumentException("Unknown feed type: {$type}");
}
$feed->setLink($this->generateUrl('homepage', [], urlType: 'absolute'));
foreach ($posts as $post) {
$feed->addItem([
'title' => $post->getTitle(),
'author' => $post->getAuthor()->getName(),
'link' => $this->generateUrl('post_show', ['slug' => $post->getSlug()], urlType: 'absolute'),
'pubdate' => $post->getCreatedAt()->format('c'),
'description' => $post->getExcerpt()
]);
}
return $feed->render('rss');
}
}
Optimize database queries for better caching:
<?php
class OptimizedFeedGenerator
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function generateFeed(string $type, array $params = []): string
{
$feed = FeedFactory::create();
// Single optimized query instead of multiple queries
$items = $this->fetchOptimizedData($type, $params);
$feed->setTitle('My Website - ' . ucfirst($type));
$feed->setDescription("Latest {$type} content");
$feed->setLink('https://example.com');
foreach ($items as $item) {
$feed->addItem([
'title' => $item['title'],
'author' => $item['author_name'],
'link' => $item['url'],
'pubdate' => $item['created_at'],
'description' => $item['description'],
'category' => explode(',', $item['categories'] ?? '')
]);
}
return $feed->render('rss');
}
private function fetchOptimizedData(string $type, array $params): array
{
// Optimized query with joins to avoid N+1 problems
$sql = "
SELECT
p.id,
p.title,
p.slug,
p.excerpt as description,
p.created_at,
u.name as author_name,
CONCAT('https://example.com/', p.type, '/', p.slug) as url,
GROUP_CONCAT(c.name ORDER BY c.name) as categories
FROM posts p
JOIN users u ON p.author_id = u.id
LEFT JOIN post_categories pc ON p.id = pc.post_id
LEFT JOIN categories c ON pc.category_id = c.id
WHERE p.published = 1 AND p.type = ?
";
$queryParams = [$type];
// Add category filter if specified
if (!empty($params['category'])) {
$sql .= " AND c.slug = ?";
$queryParams[] = $params['category'];
}
$sql .= "
GROUP BY p.id, u.name
ORDER BY p.created_at DESC
LIMIT ?
";
$queryParams[] = $params['limit'] ?? 20;
$stmt = $this->pdo->prepare($sql);
$stmt->execute($queryParams);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
Automatically invalidate cache when content changes:
<?php
// Laravel Event Listener
namespace App\Listeners;
use Illuminate\Support\Facades\Cache;
use App\Events\PostPublished;
use App\Events\PostUpdated;
class InvalidateFeedCache
{
public function handle(PostPublished|PostUpdated $event): void
{
$post = $event->post;
// Clear main feed cache
Cache::forget('feed:posts:all');
// Clear category-specific caches
foreach ($post->categories as $category) {
Cache::forget("feed:posts:{$category->slug}");
}
// Or use cache tags if available
Cache::tags(['feeds', 'posts'])->flush();
}
}
// Register in EventServiceProvider
protected $listen = [
PostPublished::class => [InvalidateFeedCache::class],
PostUpdated::class => [InvalidateFeedCache::class],
];
Implement ETags for better HTTP caching:
<?php
class ETagCachedFeed
{
private $cacheAdapter;
public function generateResponse(string $feedType, array $params = []): array
{
$cacheKey = $this->buildCacheKey($feedType, $params);
$lastModifiedKey = $cacheKey . ':lastmod';
// Get last modified time
$lastModified = $this->cacheAdapter->get($lastModifiedKey, time());
$etag = md5($cacheKey . $lastModified);
// Check if client has current version
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
return [
'status' => 304,
'headers' => [
'ETag' => $etag,
'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'
]
];
}
// Generate or get cached content
$content = $this->cacheAdapter->get($cacheKey, function () use ($feedType, $params) {
return $this->generateFeed($feedType, $params);
});
return [
'status' => 200,
'headers' => [
'Content-Type' => 'application/rss+xml; charset=utf-8',
'ETag' => $etag,
'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT',
'Cache-Control' => 'public, max-age=3600'
],
'content' => $content
];
}
public function invalidateCache(string $feedType, array $params = []): void
{
$cacheKey = $this->buildCacheKey($feedType, $params);
$lastModifiedKey = $cacheKey . ':lastmod';
// Clear cached content and update last modified
$this->cacheAdapter->delete($cacheKey);
$this->cacheAdapter->set($lastModifiedKey, time());
}
}
Track cache hit rates and performance:
<?php
class CacheMonitoredFeed
{
private $cache;
private $metrics;
public function getFeed(string $type, array $params = []): string
{
$cacheKey = $this->buildCacheKey($type, $params);
$startTime = microtime(true);
$content = $this->cache->get($cacheKey);
if ($content !== null) {
// Cache hit
$this->metrics->increment('feed.cache.hit');
$this->metrics->timing('feed.cache.time', microtime(true) - $startTime);
return $content;
}
// Cache miss - generate content
$this->metrics->increment('feed.cache.miss');
$content = $this->generateFeed($type, $params);
$this->cache->set($cacheKey, $content, 3600);
$totalTime = microtime(true) - $startTime;
$this->metrics->timing('feed.generation.time', $totalTime);
return $content;
}
}
- Learn about Custom Views for better template caching
- See Advanced Features for optimizing complex feeds
- Check out framework-specific examples: Laravel or Symfony