Lazy evaluation and lazy loading objects
Advanced5 min read·php-15-008
performancelaravel-src
Concept
Caching is the most powerful performance tool available to web applications. Caching stores the result of expensive operations (database queries, API calls, complex calculations) so they can be served immediately on subsequent requests.
PHP caching layers:
- OPcache: Caches compiled PHP bytecode — transparent, no code changes needed.
- APCu: In-memory key-value store per PHP process (not shared across processes in FPM). Good for caching within a single request or for read-heavy data shared by workers on the same server.
- Redis / Memcached: Shared memory cache across all PHP workers and servers. The standard for production application caching.
- HTTP caching:
Cache-Controlheaders, ETags, CDN caching — caches at the network level. - Query result caching: Cache database query results. Laravel's query result caching via
remember(). - Full page caching: Cache the entire rendered HTML response. Laravel's response caching packages.
Cache invalidation strategies:
- TTL (Time-to-Live): Cache expires after N seconds. Simple but may serve stale data.
- Cache-aside (lazy loading): Check cache → if miss, load from DB → store in cache. Most common pattern.
- Write-through: Update cache and DB simultaneously on every write. Consistent but slower writes.
- Event-driven invalidation: When a model is updated (
OrderUpdatedevent), explicitly delete relevant cache keys.
The two hard things in computer science: cache invalidation and naming things. Tag your cache entries with Laravel's cache tags to invalidate groups of related entries together.
Code Example
php
<?php
// Cache facade — key abstraction over Redis/Memcached/file/array
use Illuminate\Support\Facades\Cache;
// Get or compute (cache-aside pattern)
$products = Cache::remember('products:active', 3600, function() {
return Product::where('active', true)->with('category')->get();
});
// Remember forever (explicitly invalidate)
$config = Cache::rememberForever('app:config', fn() => Config::all()->keyBy('key'));
// Put / get / forget
Cache::put('user:' . $userId . ':profile', $profile, now()->addHours(2));
$profile = Cache::get('user:42:profile');
Cache::forget('user:42:profile'); // explicit invalidation
// Atomic lock (prevent cache stampede)
$value = Cache::lock('products:rebuild', 10)->get(function() {
return expensiveQuery();
});
// Tags — group related cache entries
Cache::tags(['orders', 'user:' . $userId])->put('order:' . $orderId, $order, 3600);
Cache::tags(['orders'])->flush(); // invalidate ALL order cache entries at once
// Flexible — remember even if expired (serve stale while regenerating)
use Illuminate\Support\Facades\Cache;
$data = Cache::flexible('homepage:stats', [3600, 7200], fn() => buildStats());
// Serves cached data for up to 7200s, regenerates in background if >3600s old
// APCu — in-process cache (no Redis needed for single-server apps)
apcu_store('counter', 0, 3600);
$current = apcu_fetch('counter');
apcu_inc('counter'); // atomic increment
// Cache key namespacing strategy
$key = sprintf('v1:users:%d:orders:%d', $userId, $page); // versioned, namespaced
// To invalidate all v1 cache: change 'v1' to 'v2' in all keys (key prefix rotation)