Skip to content

Caching

Rumen Damyanov edited this page Jul 29, 2025 · 1 revision

Caching Examples

This guide shows how to implement caching strategies to improve feed performance and reduce server load.

Why Cache Feeds?

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

Simple File-Based Caching

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);

Redis Caching

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));

Laravel Caching Example

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');
    }
}

Symfony Caching Example

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');
    }
}

Database Query Optimization

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);
    }
}

Cache Invalidation Strategies

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],
];

Advanced Caching with ETags

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());
    }
}

Monitoring Cache Performance

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;
    }
}

Next Steps

Clone this wiki locally