Skip to content

Declarative Method Caching with Attributes in Winter Boot

Winter Boot lets you cache the results of any bean method without writing cache-management code. Caching is built on AOP: a caching attribute on a public method wraps it so lookups, storage, and eviction happen automatically. You choose the backend — in-memory, the node-local KV store, or Redis — by defining a CacheManager bean; the attributes stay the same whichever backend you use.

Caching attributes only take effect when your main application class carries #[EnableCaching]. Without it they are silently ignored.

MyApplication.php
use dev\winterframework\stereotype\cache\EnableCaching;
use dev\winterframework\stereotype\WinterBootApplication;
#[WinterBootApplication(
configDirectory: [__DIR__ . '/config'],
scanNamespaces: [['com\\example\\myapp', __DIR__ . '/src']]
)]
#[EnableCaching]
class MyApplication {
// application entry point
}

#[Cacheable] is a method-level attribute. The first time a method is called for a given cache key, Winter Boot executes the method and stores the return value in the cache. On subsequent calls with the same key the cached value is returned directly and the method body is not executed.

  • cacheNames string|string[] (default: "default") One or more cache container names where the result will be stored. Accepts a single string or an array of strings.

  • key string (default: Class::method + arguments) The key under which the value is cached. Supports #{param} expression interpolation. The default key includes the class name and every argument value; arrays and objects are hashed, and keys over 255 characters keep a prefix plus a hash, so different arguments never share an entry. With a custom key, keep placeholders unambiguous (user-#{a}-#{b} can collide when values contain -).

    Cache keys and values are never logged.

  • keyGenerator string (default: Framework managed) Bean name of a class implementing KeyGenerator for custom key generation logic.

  • cacheManager string (default: Framework managed) Bean name of a class implementing CacheManager. Use when you have multiple managers registered.

  • cacheResolver string (default: Framework managed) Bean name of a class implementing CacheResolver for dynamic cache resolution at runtime.

  • condition / unless string (default: "") Reserved for future use. They are accepted for Spring compatibility but are not evaluated yet — setting them has no effect.

Default Cache
use dev\winterframework\cache\stereotype\Cacheable;
// Default cache container (in-memory, key = method name + arguments)
#[Cacheable]
public function getExpensiveCalculationResult(): mixed
{
// Complex, time-consuming calculation
return 'some_calculated_value';
}
Named Cache
use dev\winterframework\cache\stereotype\Cacheable;
// Single named cache container
#[Cacheable('product-prices')]
public function getProductPrice(string $productId): mixed
{
// Fetch price from database or external API
return '19.99';
}
Multiple Caches
use dev\winterframework\cache\stereotype\Cacheable;
// Multiple named cache containers — result is stored in both
#[Cacheable(cacheNames: ['user-sessions', 'api-tokens'])]
public function getUserSessionData(string $userId): mixed
{
return ['session_id' => 'abc', 'token' => 'xyz'];
}

#[CachePut] is a method-level attribute that always executes the underlying method and then writes the fresh return value to the cache. Use it when you need to keep the cache up to date after a write operation. #[CachePut] accepts the same options as #[Cacheable].

use dev\winterframework\cache\stereotype\CachePut;
#[CachePut('product-prices')]
public function updateProductPrice(string $productId, float $newPrice): mixed
{
// Persist new price to the database first…
return $newPrice; // return value is written back to the cache
}

#[CacheEvict] is a method-level attribute that removes one or more entries from the cache when the annotated method is called. You can target a specific entry using the key option or flush an entire cache container by setting allEntries: true.

  • allEntries bool (default: false) When true, all entries in the specified cache(s) are removed rather than just the entry matching key.

  • beforeInvocation bool (default: false) When true, the cache is cleared before the method executes. The default clears the cache after successful execution.

use dev\winterframework\cache\stereotype\CacheEvict;
#[CacheEvict(
cacheNames: 'stock-prices',
cacheManager: 'redisCacheManager',
allEntries: true,
beforeInvocation: false
)]
public function clearAllStockPricesCache(): void
{
// All entries in the "stock-prices" cache are removed after this method returns.
}

Pick the backend that fits how you deploy.

In-Memory

Zero-config default. Fast and simple, but each worker process has its own copy.

SharedKvCache

Shared across all processes on the same node. Ideal for single-node deployments.

RedisCache

Distributed cache shared across the entire cluster. Requires winter-data-redis.

Out of the box, Winter Boot registers a SimpleCacheManager backed by a fast PHP in-memory store. No additional configuration is required — add #[Cacheable] to your methods and caching works immediately.

SharedKvCache is backed by a local Key-Value store that is shared across all processes on the same node. It is a good fit for single-node deployments or for data that does not need to be distributed across a cluster.

CacheConfiguration controls the eviction and expiry policy per cache container:

  • maximumSize int (default: effectively unlimited) Maximum number of entries before LRU eviction kicks in.

  • expireAfterWriteMs int (default: -1) Milliseconds after which a written entry expires. -1 means entries never expire.

  • expireAfterAccessMs int (default: -1) Milliseconds since the last read after which an entry expires. -1 means never.

CacheConfig.php
use dev\winterframework\stereotype\Configuration;
use dev\winterframework\stereotype\Bean;
use dev\winterframework\cache\impl\SharedKvCache;
use dev\winterframework\cache\CacheConfiguration;
use dev\winterframework\cache\impl\SimpleCacheManager;
use dev\winterframework\cache\CacheManager;
use dev\winterframework\io\kv\KvTemplate;
#[Configuration]
class CacheConfig
{
#[Bean]
public function getCacheManager(KvTemplate $kvTemplate): CacheManager
{
$cache = new SharedKvCache(
$kvTemplate,
'stock-prices', // unique cache name
CacheConfiguration::get(
maximumSize: 5000, // LRU eviction after 5 000 entries
expireAfterWriteMs: 600_000, // entries expire after 10 minutes
)
);
$manager = new SimpleCacheManager();
$manager->addCache($cache);
return $manager;
}
}

For multi-node deployments where all nodes must share the same cached data, RedisCache provides a distributed cache backed by Redis. It is available in the winter-data-redis module.

  1. Define the Redis CacheManager bean

    CacheConfig.php
    use dev\winterframework\stereotype\Configuration;
    use dev\winterframework\stereotype\Bean;
    use dev\winterframework\data\redis\cache\RedisCache;
    use dev\winterframework\cache\CacheConfiguration;
    use dev\winterframework\cache\impl\SimpleCacheManager;
    use dev\winterframework\cache\CacheManager;
    use dev\winterframework\data\redis\phpredis\PhpRedisTemplate;
    #[Configuration]
    class CacheConfig
    {
    #[Bean('redisCacheManager')]
    public function getRedisCacheManager(PhpRedisTemplate $redisTpl): CacheManager
    {
    $pricesCache = new RedisCache(
    $redisTpl,
    'stock-prices',
    CacheConfiguration::get(
    maximumSize: 5000,
    expireAfterWriteMs: 600_000,
    )
    );
    $manager = new SimpleCacheManager();
    $manager->addCache($pricesCache);
    return $manager;
    }
    }
  2. Reference the bean name in your caching attributes

    StockService.php
    use dev\winterframework\cache\stereotype\Cacheable;
    use dev\winterframework\cache\stereotype\CacheEvict;
    #[Cacheable(cacheNames: 'stock-prices', cacheManager: 'redisCacheManager')]
    public function getStockPrice(string $symbol): mixed
    {
    // Only called on a cache miss; result is stored in Redis
    return $this->stockApi->fetchPrice($symbol);
    }
    #[CacheEvict(cacheNames: 'stock-prices', cacheManager: 'redisCacheManager', allEntries: true)]
    public function refreshAllStockPrices(): void
    {
    // Flushes the entire "stock-prices" cache in Redis after execution
    }