Skip to content

Caching

Føhn provides an injectable caching service built on WordPress transients. Use it to cache expensive operations like database queries, API calls, or computed data.

Getting the Cache Service

Føhn registers CacheInterface as a singleton in the DI container. Inject it into any discovered class:

php
use Studiometa\Foehn\Contracts\CacheInterface;

final readonly class ProductService
{
    public function __construct(
        private CacheInterface $cache,
    ) {}

    public function getFeatured(): array
    {
        return $this->cache->remember('featured_products', 3600, fn() => get_posts([
            'post_type' => 'product',
            'meta_key' => 'featured',
            'meta_value' => '1',
        ]));
    }
}

You can also retrieve it from the container directly:

php
use Studiometa\Foehn\Contracts\CacheInterface;
use function Studiometa\Foehn\app;

$cache = app(CacheInterface::class);

Basic Usage

Store and Retrieve

php
// Store a value (TTL in seconds)
$cache->set('my_key', $value, 3600); // 1 hour

// Retrieve a value
$value = $cache->get('my_key');
$value = $cache->get('my_key', 'default'); // With default

Remember Pattern

The most common pattern: get from cache or compute and store.

php
$posts = $cache->remember('recent_posts', 3600, function () {
    return get_posts([
        'post_type' => 'post',
        'numberposts' => 10,
        'orderby' => 'date',
    ]);
});

The callback is only executed if the key doesn't exist in cache.

Other Operations

php
// Check if key exists
if ($cache->has('my_key')) {
    // ...
}

// Delete a key
$cache->forget('my_key');

// Store forever (no expiration)
$cache->forever('my_key', $value);

// Remember forever
$cache->rememberForever('my_key', fn() => expensive_computation());

// Increment/decrement counters
$cache->increment('page_views');
$cache->decrement('remaining_credits', 5);

Cache Tags

Tags allow grouping cache keys for batch invalidation.

Storing with Tags

php
// Store with a single tag
$products = $cache->tags(['products'])
    ->remember('products_list', 3600, fn() => get_products());

// Store with multiple tags
$featured = $cache->tags(['products', 'homepage'])
    ->remember('featured_products', 3600, fn() => get_featured_products());

// Paginated results - same tag, different keys
$page1 = $cache->tags(['products'])
    ->remember('products_page_1', 3600, fn() => get_products(page: 1));

$page2 = $cache->tags(['products'])
    ->remember('products_page_2', 3600, fn() => get_products(page: 2));

Invalidating by Tag

When content changes, flush all related cache entries at once:

php
// Flush all keys tagged with 'products'
$cache->flushTag('products');
// Clears: products_list, featured_products, products_page_1, products_page_2

// Flush multiple tags
$cache->flushTags(['products', 'categories']);

Tagged Cache Methods

All standard cache methods are available on tagged cache:

php
$cache->tags(['products'])->set('key', $value, 3600);
$cache->tags(['products'])->forever('key', $value);
$cache->tags(['products'])->remember('key', 3600, fn() => compute());
$cache->tags(['products'])->rememberForever('key', fn() => compute());
$cache->tags(['products'])->forget('key');

Cache Invalidation with Hooks

Combine cache tags with Føhn's hook system for automatic invalidation:

php
use Studiometa\Foehn\Attributes\AsAction;
use Studiometa\Foehn\Contracts\CacheInterface;

final readonly class ProductCache
{
    public function __construct(
        private CacheInterface $cache,
    ) {}

    /**
     * Get cached products list.
     */
    public function list(): array
    {
        return $this->cache->tags(['products'])
            ->remember('products_list', 3600, fn() => get_posts([
                'post_type' => 'product',
                'numberposts' => -1,
            ]));
    }

    /**
     * Get cached products by category.
     */
    public function byCategory(int $categoryId): array
    {
        return $this->cache->tags(['products', "category_{$categoryId}"])
            ->remember("products_cat_{$categoryId}", 3600, fn() => get_posts([
                'post_type' => 'product',
                'tax_query' => [
                    ['taxonomy' => 'product_cat', 'terms' => $categoryId],
                ],
            ]));
    }

    /**
     * Invalidate all product caches when a product is saved.
     */
    #[AsAction('save_post_product')]
    public function onProductSave(int $postId): void
    {
        $this->cache->flushTag('products');
    }

    /**
     * Invalidate category cache when a term is edited.
     */
    #[AsAction('edited_product_cat')]
    public function onCategoryEdit(int $termId): void
    {
        $this->cache->flushTag("category_{$termId}");
    }
}

Real-World Examples

Caching Menu Data

php
use Studiometa\Foehn\Attributes\AsAction;
use Studiometa\Foehn\Contracts\CacheInterface;

final readonly class MenuCache
{
    public function __construct(
        private CacheInterface $cache,
    ) {}

    public function getPrimaryMenu(): array
    {
        return $this->cache->tags(['menus'])
            ->remember('menu_primary', DAY_IN_SECONDS, fn() => wp_get_nav_menu_items('primary') ?: []);
    }

    #[AsAction('wp_update_nav_menu')]
    public function invalidate(): void
    {
        $this->cache->flushTag('menus');
    }
}

Caching API Responses

php
use Studiometa\Foehn\Contracts\CacheInterface;

final readonly class WeatherService
{
    public function __construct(
        private CacheInterface $cache,
    ) {}

    public function getCurrentWeather(string $city): ?array
    {
        return $this->cache->tags(['weather', "city_{$city}"])
            ->remember("weather_{$city}", HOUR_IN_SECONDS, function () use ($city) {
                $response = wp_remote_get("https://api.weather.com/v1/current?city={$city}");

                if (is_wp_error($response)) {
                    return null;
                }

                return json_decode(wp_remote_retrieve_body($response), true);
            });
    }

    public function invalidateCity(string $city): void
    {
        $this->cache->flushTag("city_{$city}");
    }
}

Cache Key Prefix

All cache keys are automatically prefixed with foehn_ to avoid collisions with other plugins.

How It Works

Under the hood, TransientCache uses WordPress transients:

  • set()set_transient()
  • get()get_transient()
  • forget()delete_transient()

Tag-to-key mappings are stored in a WordPress option (foehn_cache_tags). When you flush a tag, all associated transients are deleted and the mapping is cleaned up automatically.

Object Caching

If you have a persistent object cache (Redis, Memcached), WordPress transients automatically use it. This means Føhn's cache benefits from your object cache without any configuration.

Best Practices

Choose Appropriate TTLs

php
// Frequently changing data - short TTL
$cache->remember('latest_posts', 5 * MINUTE_IN_SECONDS, fn() => ...);

// Relatively stable data - medium TTL
$cache->remember('menu_items', HOUR_IN_SECONDS, fn() => ...);

// Rarely changing data - long TTL with invalidation
$cache->tags(['settings'])
    ->remember('site_settings', DAY_IN_SECONDS, fn() => ...);
php
// Group related caches with tags
$cache->tags(['user', "user_{$userId}"])->remember("user_profile_{$userId}", ...);
$cache->tags(['user', "user_{$userId}"])->remember("user_orders_{$userId}", ...);

// Invalidate all user data at once
$cache->flushTag("user_{$userId}");

Don't Cache Small Operations

php
// ❌ Don't cache simple operations
$cache->remember('current_time', 60, fn() => time());

// ✅ Cache expensive operations
$cache->remember('complex_query', 3600, fn() => $wpdb->get_results($complex_sql));

Handle Cache Failures Gracefully

php
// The callback always provides a fallback
$data = $cache->remember('api_data', 3600, function () {
    $response = wp_remote_get('https://api.example.com/data');

    // Return empty array on failure - it will be cached
    // Consider a shorter TTL for error states
    if (is_wp_error($response)) {
        return [];
    }

    return json_decode(wp_remote_retrieve_body($response), true);
});

Released under the MIT License.