PDOdb provides built-in support for query result caching using PSR-16 (Simple Cache) compatible implementations. This feature can significantly improve application performance by reducing database load and query execution time.
- Overview
- Setup
- Basic Usage
- Cache Methods
- Cache Configuration
- Cache Key Generation
- PSR-16 Implementations
- Best Practices
- Performance Considerations
- Troubleshooting
Query caching stores the results of SELECT queries in a cache layer, allowing subsequent identical queries to retrieve results from cache instead of executing against the database.
- Reduced Database Load: Fewer queries executed
- Faster Response Times: Cache reads are typically 10-100x faster than database queries
- Scalability: Better handling of high-traffic scenarios
- Cost Savings: Lower database server resource usage
✅ Good candidates for caching:
- Configuration and settings
- Product catalogs
- User profiles (with appropriate TTL)
- Aggregate reports and statistics
- Static or rarely-changing content
- Expensive JOIN queries
- Complex aggregations
❌ Poor candidates for caching:
- Real-time data (stock prices, live scores)
- Frequently updated records
- User-specific data with high variability
- Write-heavy tables
- Data requiring strong consistency
# Symfony Cache (recommended - provides multiple adapters)
composer require symfony/cache
# Optional: For APCu cache
# Enable ext-apcu extension in php.ini
# Optional: For Redis cache
# Install ext-redis extension
# Optional: For Memcached cache
# Install ext-memcached extensionCacheFactory provides a convenient way to create cache adapters from configuration:
use tommyknocker\pdodb\cache\CacheFactory;
use tommyknocker\pdodb\PdoDb;
// Filesystem cache
$cache = CacheFactory::create([
'type' => 'filesystem',
'directory' => '/var/cache/pdodb',
'namespace' => 'app',
'default_lifetime' => 3600,
]);
// Redis cache
$cache = CacheFactory::create([
'type' => 'redis',
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
'namespace' => 'app',
]);
// APCu cache
$cache = CacheFactory::create([
'type' => 'apcu',
'namespace' => 'app',
]);
// Pass cache to PdoDb
$db = new PdoDb(
'mysql',
[
'host' => 'localhost',
'dbname' => 'myapp',
'username' => 'user',
'password' => 'pass',
],
[],
null,
$cache
);use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use tommyknocker\pdodb\PdoDb;
// Create PSR-16 cache
$adapter = new FilesystemAdapter();
$cache = new Psr16Cache($adapter);
// Pass cache to PdoDb constructor
$db = new PdoDb(
'mysql',
[
'host' => 'localhost',
'dbname' => 'myapp',
'username' => 'user',
'password' => 'pass',
],
[],
null,
$cache // PSR-16 cache instance
);$db = new PdoDb(
'mysql',
[
'host' => 'localhost',
'dbname' => 'myapp',
'username' => 'user',
'password' => 'pass',
'cache' => [
'prefix' => 'myapp_', // Cache key prefix
'default_ttl' => 7200, // Default TTL (seconds)
'enabled' => true, // Global enable/disable
],
],
[],
null,
$cache
);Use the cache() method to enable caching for a specific query:
// Cache for 1 hour (3600 seconds)
$users = $db->find()
->from('users')
->where('active', 1)
->cache(3600)
->get();Provide a custom key for easier cache management:
$settings = $db->find()
->from('settings')
->cache(3600, 'app_settings')
->get();Use noCache() to explicitly disable caching:
$data = $db->find()
->from('products')
->cache(3600) // Enable cache
->noCache() // But then disable it
->get(); // This query won't be cachedEnable caching for the current query.
Parameters:
$ttl- Time-to-live in seconds$key- Optional custom cache key (auto-generated if null)
Returns: self (for method chaining)
// Auto-generated key
$result = $db->find()
->from('products')
->where('category', 'Electronics')
->cache(1800) // 30 minutes
->get();
// Custom key
$result = $db->find()
->from('products')
->where('featured', 1)
->cache(3600, 'featured_products')
->get();Disable caching for the current query.
Returns: self (for method chaining)
$result = $db->find()
->from('users')
->noCache()
->get();Caching works seamlessly with all result-fetching methods:
$products = $db->find()
->from('products')
->where('stock', 0, '>')
->cache(600)
->get();$user = $db->find()
->from('users')
->where('email', $email)
->cache(1800)
->getOne();$count = $db->find()
->from('orders')
->select([Db::count('*', 'total')])
->where('status', 'pending')
->cache(300)
->getValue();$usernames = $db->find()
->from('users')
->select('username')
->where('active', 1)
->cache(600)
->getColumn();| Option | Type | Default | Description |
|---|---|---|---|
prefix |
string |
'pdodb_' |
Cache key prefix for namespacing |
default_ttl |
int |
3600 |
Default TTL in seconds |
enabled |
bool |
true |
Global cache enable/disable |
$config = [
'host' => 'localhost',
'dbname' => 'myapp',
'username' => 'user',
'password' => 'pass',
'cache' => [
'prefix' => 'myapp_v1_',
'default_ttl' => 7200,
'enabled' => true,
],
];
$db = new PdoDb('mysql', $config, [], null, $cache);$cacheManager = $db->getCacheManager();
$config = $cacheManager->getConfig();
echo "Prefix: " . $config->getPrefix() . "\n";
echo "Default TTL: " . $config->getDefaultTtl() . " seconds\n";
echo "Enabled: " . ($config->isEnabled() ? 'Yes' : 'No') . "\n";When no custom key is provided, cache keys are automatically generated using:
- SQL query string
- Query parameters
- Database driver name
This ensures different queries (or same query with different parameters) get different cache entries.
// These create different cache entries
$result1 = $db->find()->from('users')->where('age', 25, '>')->cache(3600)->get();
$result2 = $db->find()->from('users')->where('age', 30, '>')->cache(3600)->get();Custom keys are useful for:
- Easier cache invalidation
- Shared caching logic
- Debugging and monitoring
// Set cache with custom key
$products = $db->find()
->from('products')
->where('featured', 1)
->cache(3600, 'featured_products')
->get();
// Later, invalidate if needed
$cacheManager = $db->getCacheManager();
$cacheManager->delete('featured_products');All cache keys are automatically prefixed based on configuration:
// With prefix 'myapp_'
$cache->cache(3600, 'users');
// Actual key: 'myapp_users'
// Auto-generated keys also get prefix
$cache->cache(3600);
// Actual key: 'myapp_[hash]'PDOdb works with any PSR-16 compatible cache. Here are recommended implementations:
CacheFactory provides a unified interface for creating cache adapters from optional dependencies:
use tommyknocker\pdodb\cache\CacheFactory;
// Filesystem cache (requires symfony/cache)
$cache = CacheFactory::create([
'type' => 'filesystem',
'directory' => '/var/cache/pdodb',
'namespace' => 'myapp',
'default_lifetime' => 3600,
]);
// Redis cache (requires ext-redis and symfony/cache)
$cache = CacheFactory::create([
'type' => 'redis',
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
'password' => null, // Optional
'namespace' => 'myapp',
]);
// APCu cache (requires ext-apcu and symfony/cache)
$cache = CacheFactory::create([
'type' => 'apcu',
'namespace' => 'myapp',
]);
// Memcached cache (requires ext-memcached and symfony/cache)
$cache = CacheFactory::create([
'type' => 'memcached',
'servers' => [['127.0.0.1', 11211]],
'namespace' => 'myapp',
]);Note: CacheFactory::create() returns null if the required dependencies are not installed, so always check:
$cache = CacheFactory::create($config);
if ($cache === null) {
// Handle missing dependencies
throw new \RuntimeException('Cache adapter not available');
}You can also create cache adapters manually:
Filesystem Cache:
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
$adapter = new FilesystemAdapter(
namespace: 'myapp',
defaultLifetime: 0,
directory: '/tmp/cache'
);
$cache = new Psr16Cache($adapter);Redis Cache:
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\Psr16Cache;
$redis = RedisAdapter::createConnection('redis://localhost:6379');
$adapter = new RedisAdapter($redis);
$cache = new Psr16Cache($adapter);APCu Cache (High Performance):
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Psr16Cache;
$adapter = new ApcuAdapter(namespace: 'myapp');
$cache = new Psr16Cache($adapter);Memcached Cache:
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
use Symfony\Component\Cache\Psr16Cache;
$client = MemcachedAdapter::createConnection('memcached://localhost:11211');
$adapter = new MemcachedAdapter($client);
$cache = new Psr16Cache($adapter);For unit tests and development:
use tommyknocker\pdodb\tests\fixtures\ArrayCache;
$cache = new ArrayCache();
$db = new PdoDb('sqlite', ['path' => ':memory:'], [], null, $cache);Or use CacheFactory:
use tommyknocker\pdodb\cache\CacheFactory;
$cache = CacheFactory::create(['type' => 'array']);// Short TTL for frequently changing data
$recentOrders = $db->find()
->from('orders')
->where('created_at', date('Y-m-d'), '>')
->cache(300) // 5 minutes
->get();
// Long TTL for static data
$categories = $db->find()
->from('categories')
->cache(86400) // 24 hours
->get();
// Very long TTL for configuration
$settings = $db->find()
->from('app_settings')
->cache(604800, 'app_settings') // 1 week
->get();// Easy to invalidate later
$featuredProducts = $db->find()
->from('products')
->where('featured', 1)
->cache(3600, 'featured_products')
->get();
// When product is updated
$db->find()->table('products')->where('id', $productId)->update(['featured' => 1]);
$db->getCacheManager()->delete('featured_products');// Complex aggregation - good candidate for caching
$report = $db->find()
->from('orders')
->select([
'date' => Db::date('created_at'),
Db::count('*', 'total_orders'),
Db::sum('total_amount', 'revenue'),
])
->where('created_at', date('Y-m-d', strtotime('-30 days')), '>=')
->groupBy('date')
->cache(3600, 'monthly_report')
->get();function getProducts($category, $useCache = true) {
$query = $db->find()
->from('products')
->where('category', $category);
if ($useCache) {
$query->cache(1800);
}
return $query->get();
}
// Development: no cache
$products = getProducts('Electronics', false);
// Production: use cache
$products = getProducts('Electronics', true);// Track cache hits/misses
$cacheManager = $db->getCacheManager();
$startTime = microtime(true);
$result = $db->find()
->from('products')
->cache(3600, 'all_products')
->get();
$duration = microtime(true) - $startTime;
error_log("Query duration: {$duration}s");| Operation | Database | Cache (Memory) | Cache (Redis) | Speedup |
|---|---|---|---|---|
| Simple SELECT | 5-10ms | 0.1-0.5ms | 1-2ms | 5-100x |
| Complex JOIN | 50-500ms | 0.1-0.5ms | 1-2ms | 50-500x |
| Aggregation | 100-1000ms | 0.1-0.5ms | 1-2ms | 100-1000x |
Cache Size Estimation:
Average Row Size: 1 KB
Rows per Query: 100
Queries Cached: 1000
Total Memory: 100 MB
Recommendations:
- Monitor cache size in production
- Set appropriate TTL to limit growth
- Use Redis/Memcached for large datasets
- Clear cache periodically
Local Cache (APCu, File):
- Latency: <0.1ms
- Best for single-server deployments
Remote Cache (Redis, Memcached):
- Latency: 1-5ms
- Best for multi-server deployments
- Adds network round-trip time
Check if cache is enabled:
$cacheManager = $db->getCacheManager();
if ($cacheManager === null) {
echo "No cache manager configured\n";
} else {
$config = $cacheManager->getConfig();
echo "Cache enabled: " . ($config->isEnabled() ? 'Yes' : 'No') . "\n";
}Verify cache() is called:
// This won't cache
$result = $db->find()->from('users')->get();
// This will cache
$result = $db->find()->from('users')->cache(3600)->get();Symptoms: Cached data doesn't reflect recent updates
Solutions:
- Reduce TTL:
$data = $db->find()->from('products')->cache(300)->get(); // 5 minutes instead of 1 hour- Manual Invalidation:
// After update
$db->find()->table('products')->where('id', $id)->update($data);
$db->getCacheManager()->delete('product_list');- Disable cache for writes:
// Always fetch fresh after write
$db->find()->table('products')->where('id', $id)->update($data);
$product = $db->find()->from('products')->where('id', $id)->noCache()->getOne();Symptoms: Out of memory errors, slow cache performance
Solutions:
- Use distributed cache (Redis, Memcached)
- Reduce cache TTL
- Limit cached result size:
// Don't cache huge result sets
$query = $db->find()->from('logs')->limit(1000);
if ($query->limit <= 100) {
$query->cache(600);
}
$results = $query->get();Symptoms: Cached results differ from database
Cause: Parameters or query changed, but same cache key used
Solution: Use unique cache keys or rely on auto-generation:
// Bad - reuses same key
$result1 = $db->find()->from('users')->where('age', 25, '>')->cache(3600, 'users')->get();
$result2 = $db->find()->from('users')->where('age', 30, '>')->cache(3600, 'users')->get();
// Good - different keys
$result1 = $db->find()->from('users')->where('age', 25, '>')->cache(3600, 'users_over_25')->get();
$result2 = $db->find()->from('users')->where('age', 30, '>')->cache(3600, 'users_over_30')->get();
// Best - auto-generated keys
$result1 = $db->find()->from('users')->where('age', 25, '>')->cache(3600)->get();
$result2 = $db->find()->from('users')->where('age', 30, '>')->cache(3600)->get();See Query Caching examples for complete working examples.