Caching
Melodic provides a simple caching abstraction with file-based and in-memory implementations. The CacheInterface defines the standard operations, while FileCache and ArrayCache provide concrete implementations for production and testing use.
CacheInterface
The Melodic\Cache\CacheInterface defines five methods for working with cached data. All cache implementations share this interface.
| Method | Signature | Description |
|---|---|---|
get |
(string $key, mixed $default = null): mixed |
Retrieves a value by key. Returns $default if the key does not exist or has expired. |
set |
(string $key, mixed $value, ?int $ttl = null): bool |
Stores a value with an optional TTL in seconds. Pass null for no expiration. |
delete |
(string $key): bool |
Removes a cached entry by key. |
has |
(string $key): bool |
Checks whether a non-expired entry exists for the given key. |
clear |
(): bool |
Removes all entries from the cache. |
<?php
use Melodic\Cache\CacheInterface;
class ProductService
{
public function __construct(
private readonly CacheInterface $cache,
private readonly ProductRepository $repo,
) {}
public function getFeatured(): array
{
if ($this->cache->has('featured_products')) {
return $this->cache->get('featured_products');
}
$products = $this->repo->findFeatured();
$this->cache->set('featured_products', $products, ttl: 3600);
return $products;
}
}
FileCache
The Melodic\Cache\FileCache stores serialized entries on disk. Each cache key is hashed with MD5 to produce a flat filename in the cache directory. Entries are stored as serialized arrays with a value and an optional expires timestamp.
<?php
use Melodic\Cache\FileCache;
$cache = new FileCache('/path/to/storage/cache');
// Store a value for 30 minutes
$cache->set('user:42', $userData, ttl: 1800);
// Retrieve it
$user = $cache->get('user:42');
// Check existence (also cleans expired entries)
if ($cache->has('user:42')) {
// ...
}
// Remove a specific entry
$cache->delete('user:42');
// Clear everything in the cache directory
$cache->clear();
Automatic directory creation. The FileCache constructor creates the cache directory (with 0775 permissions, recursive) if it does not already exist.
How FileCache Works
| Aspect | Detail |
|---|---|
| Storage format | PHP serialize() of ['value' => ..., 'expires' => ...] |
| Filename | md5($key) in the cache directory |
| TTL | Stored as time() + $ttl; null means no expiration |
| Expiry check | Checked on get() and has(); expired files are deleted on access |
| Clear | Deletes all files in the cache directory via glob() |
Lazy expiration. Expired entries are only removed when accessed via get() or has(). If you need to purge stale files proactively, use the cache:clear console command or call clear() on a schedule.
ArrayCache
The Melodic\Cache\ArrayCache stores entries in a PHP array in memory. It implements the same CacheInterface and supports TTL expiration. Expired entries are cleaned up on access, just like FileCache.
<?php
use Melodic\Cache\ArrayCache;
$cache = new ArrayCache();
$cache->set('key', 'value', ttl: 60);
$cache->get('key'); // 'value'
$cache->has('key'); // true
$cache->delete('key');
$cache->clear();
Use ArrayCache for testing. Swap FileCache for ArrayCache in your test suite by rebinding the interface in the DI container. It behaves identically but keeps everything in memory with no filesystem side effects.
CacheServiceProvider
The Melodic\Cache\CacheServiceProvider registers CacheInterface as a singleton bound to a FileCache instance. Pass the cache directory path to the provider's constructor.
<?php
use Melodic\Core\Application;
use Melodic\Cache\CacheServiceProvider;
$app = new Application(__DIR__);
$app->services(function ($container) {
(new CacheServiceProvider(__DIR__ . '/storage/cache'))->register($container);
});
// Now CacheInterface resolves to a shared FileCache instance
After registration, any class that depends on CacheInterface will automatically receive the FileCache singleton through constructor injection:
<?php
use Melodic\Cache\CacheInterface;
class ReportService
{
public function __construct(
private readonly CacheInterface $cache,
) {}
public function getMonthlyReport(int $month): array
{
$key = "report:monthly:{$month}";
$cached = $this->cache->get($key);
if ($cached !== null) {
return $cached;
}
$report = $this->generateReport($month);
$this->cache->set($key, $report, ttl: 86400); // Cache for 24 hours
return $report;
}
}
View Caching
The ViewEngine supports cached rendering through its renderCached() method. When a CacheInterface implementation is provided to the ViewEngine, rendered templates can be stored and served from cache.
renderCached() Method
| Parameter | Type | Default | Description |
|---|---|---|---|
$template |
string |
— | Template name (without .phtml extension) |
$data |
array |
[] |
Data to pass to the template |
$layout |
?string |
null |
Optional layout template name |
$ttl |
int |
3600 |
Cache lifetime in seconds (default: 1 hour) |
<?php
use Melodic\View\ViewEngine;
use Melodic\Cache\FileCache;
$cache = new FileCache('/path/to/storage/cache');
$viewEngine = new ViewEngine('/path/to/views', $cache);
// Render and cache for 1 hour (default)
$html = $viewEngine->renderCached('products/index', ['products' => $products], 'layouts/main');
// Render and cache for 5 minutes
$html = $viewEngine->renderCached('dashboard/stats', $data, 'layouts/admin', ttl: 300);
Cache Key Format
The cache key is computed as:
view:{template}:{layout}:{md5(serialize($data))}
This means the same template rendered with different data or a different layout will produce separate cache entries. If the layout is null, the layout segment is left empty.
Graceful fallback. If the ViewEngine is constructed without a cache (passing null), renderCached() silently falls back to render() with no caching. This makes it safe to call renderCached() in all environments.
Cache Clear Command
Melodic includes a built-in cache:clear console command that calls clear() on the registered CacheInterface implementation.
php melodic cache:clear
# Output: Cache cleared successfully.
The command is implemented as a Melodic\Console\CacheClearCommand that receives the cache via constructor injection:
<?php
use Melodic\Console\Command;
use Melodic\Cache\CacheInterface;
class CacheClearCommand extends Command
{
public function __construct(
private readonly CacheInterface $cache,
) {
parent::__construct('cache:clear', 'Clear the application cache');
}
public function execute(array $args): int
{
if ($this->cache->clear()) {
$this->writeln('Cache cleared successfully.');
return 0;
}
$this->error('Failed to clear cache.');
return 1;
}
}
Deploy hook. Run cache:clear as part of your deployment process to ensure stale cached views and data are purged when new code is released.
Usage Patterns
Cache-Aside Pattern
The most common caching pattern: check the cache first, fetch from the source on miss, then store the result.
<?php
public function getUser(int $id): ?User
{
$key = "user:{$id}";
$cached = $this->cache->get($key);
if ($cached !== null) {
return $cached;
}
$user = (new GetUserByIdQuery($id))->execute($this->context);
if ($user !== null) {
$this->cache->set($key, $user, ttl: 600); // 10 minutes
}
return $user;
}
Cache Invalidation
Delete specific cache entries when the underlying data changes:
<?php
public function updateUser(int $id, array $data): User
{
$user = (new UpdateUserCommand($id, $data))->execute($this->context);
// Invalidate the cached entry
$this->cache->delete("user:{$id}");
return $user;
}
Swapping Implementations for Tests
Override the cache binding in your test setup to use ArrayCache:
<?php
use Melodic\Cache\ArrayCache;
use Melodic\Cache\CacheInterface;
// In test setup
$container->singleton(CacheInterface::class, fn() => new ArrayCache());