Laravel Feature and Unit Tests: A Practical Guide with PHPUnit
Write comprehensive tests for your Laravel applications.
Caching is one of the most effective ways to dramatically improve your Laravel application's performance. By storing frequently accessed data in memory, you can reduce database queries, API calls, and expensive computations, resulting in faster response times and better user experience.
// config/cache.php
return [
'default' => env('CACHE_DRIVER', 'file'),
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
'lock_connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
];
# .env file
CACHE_DRIVER=redis
# Redis Configuration
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
# Memcached Configuration
MEMCACHED_HOST=127.0.0.1
MEMCACHED_PORT=11211
MEMCACHED_USERNAME=null
MEMCACHED_PASSWORD=null
# Database Cache (if using database driver)
DB_CONNECTION=mysql
# Install Redis on Ubuntu
sudo apt update
sudo apt install redis-server
# Start Redis service
sudo systemctl start redis-server
sudo systemctl enable redis-server
# Test Redis connection
redis-cli ping
# Install Redis PHP extension
sudo apt install php-redis
sudo systemctl restart php-fpm # or your PHP service
# Clear configuration cache
php artisan config:clear
# Install Memcached on Ubuntu
sudo apt update
sudo apt install memcached
# Install PHP Memcached extension
sudo apt install php-memcached
# Start Memcached service
sudo systemctl start memcached
sudo systemctl enable memcached
# Check Memcached status
systemctl status memcached
# Test Memcached connection
telnet localhost 11211
<?php
// app/Services/CacheService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class CacheService
{
public function basicCacheOperations()
{
// Store a value in cache for 60 minutes
Cache::put('key', 'value', 60 * 60);
// Store a value forever
Cache::forever('key', 'value');
// Store if not exists
Cache::add('key', 'value', 60);
// Retrieve a value
$value = Cache::get('key');
// Retrieve with default value
$value = Cache::get('key', 'default');
// Retrieve or store if not exists
$value = Cache::remember('key', 60, function () {
return 'computed value';
});
// Retrieve and delete
$value = Cache::pull('key');
// Check if key exists
if (Cache::has('key')) {
$value = Cache::get('key');
}
// Delete a key
Cache::forget('key');
// Clear entire cache
Cache::flush();
}
public function cacheMultipleItems()
{
// Store multiple items
Cache::putMany([
'user_1' => 'John Doe',
'user_2' => 'Jane Smith',
'settings' => ['theme' => 'dark', 'language' => 'en'],
], 60);
// Get multiple items
$values = Cache::many(['user_1', 'user_2', 'settings']);
// Store multiple items forever
Cache::putManyForever([
'config_site_name' => 'My App',
'config_version' => '1.0.0',
]);
}
public function atomicCacheOperations()
{
// Increment a value
Cache::increment('page_views');
Cache::increment('page_views', 5);
// Decrement a value
Cache::decrement('stock_count');
Cache::decrement('stock_count', 3);
// Get and increment (atomic)
$current = Cache::get('counter', 0);
$new = Cache::increment('counter');
}
}
<?php
// Using global cache() helper
// Store value
cache(['key' => 'value'], 300); // 5 minutes
// Retrieve value
$value = cache('key');
// Retrieve with default
$value = cache('key', function () {
return 'default value';
});
// Store if not exists
$value = cache()->remember('key', 60, function () {
return expensiveComputation();
});
// Clear specific key
cache()->forget('key');
// Clear all cache
cache()->flush();
<?php
// app/Services/ProductService.php
namespace App\Services;
use App\Models\Product;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ProductService
{
public function getPopularProducts($limit = 10)
{
$cacheKey = "products:popular:{$limit}";
$cacheDuration = 3600; // 1 hour
return Cache::remember($cacheKey, $cacheDuration, function () use ($limit) {
return Product::with(['category', 'brand'])
->where('status', 'active')
->where('stock_quantity', '>', 0)
->withCount(['orders', 'reviews'])
->orderByDesc('orders_count')
->limit($limit)
->get()
->map(function ($product) {
return [
'id' => $product->id,
'name' => $product->name,
'slug' => $product->slug,
'price' => $product->price / 100,
'image' => $product->primary_image_url,
'rating' => $product->average_rating,
'reviews_count' => $product->reviews_count,
'category' => $product->category->name,
];
});
});
}
public function getProductWithCache($productId)
{
$cacheKey = "product:{$productId}:full";
$cacheDuration = 1800; // 30 minutes
return Cache::remember($cacheKey, $cacheDuration, function () use ($productId) {
return Product::with([
'category',
'brand',
'variants',
'reviews' => function ($query) {
$query->with('user:id,name,avatar_url')
->where('approved', true)
->latest()
->limit(10);
},
'relatedProducts' => function ($query) {
$query->select('id', 'name', 'slug', 'price', 'primary_image_url')
->where('status', 'active');
},
])->findOrFail($productId);
});
}
public function getProductStats()
{
$cacheKey = 'stats:products:global';
$cacheDuration = 300; // 5 minutes
return Cache::remember($cacheKey, $cacheDuration, function () {
return [
'total_products' => Product::count(),
'active_products' => Product::where('status', 'active')->count(),
'out_of_stock' => Product::where('stock_quantity', 0)->count(),
'total_value' => Product::sum('price') / 100,
'average_price' => Product::average('price') / 100,
'by_category' => DB::table('products')
->join('categories', 'products.category_id', '=', 'categories.id')
->select('categories.name', DB::raw('COUNT(*) as count'))
->groupBy('categories.id', 'categories.name')
->get()
->pluck('count', 'name'),
];
});
}
public function clearProductCache($productId = null)
{
if ($productId) {
// Clear specific product cache
Cache::forget("product:{$productId}:full");
Cache::forget("product:{$productId}:basic");
// Clear related caches
Cache::forget('products:popular:10');
Cache::forget('products:popular:20');
Cache::forget('stats:products:global');
} else {
// Clear all product-related caches with tags
Cache::tags(['products'])->flush();
}
}
}
<?php
// app/Services/ApiCacheService.php
namespace App\Services;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class ApiCacheService
{
public function getWeatherData($city, $country = null)
{
$cacheKey = "weather:{$city}:{$country}";
$cacheDuration = 1800; // 30 minutes
return Cache::remember($cacheKey, $cacheDuration, function () use ($city, $country) {
$apiKey = config('services.weather.api_key');
$location = $country ? "{$city},{$country}" : $city;
$response = Http::timeout(10)
->retry(3, 100)
->get("https://api.openweathermap.org/data/2.5/weather", [
'q' => $location,
'appid' => $apiKey,
'units' => 'metric',
]);
if ($response->failed()) {
throw new \Exception("Weather API failed: " . $response->body());
}
$data = $response->json();
return [
'temperature' => $data['main']['temp'],
'feels_like' => $data['main']['feels_like'],
'humidity' => $data['main']['humidity'],
'pressure' => $data['main']['pressure'],
'description' => $data['weather'][0]['description'],
'icon' => $data['weather'][0]['icon'],
'wind_speed' => $data['wind']['speed'],
'wind_direction' => $data['wind']['deg'],
'visibility' => $data['visibility'],
'sunrise' => $data['sys']['sunrise'],
'sunset' => $data['sys']['sunset'],
'last_updated' => now()->toISOString(),
];
});
}
public function getExchangeRates($baseCurrency = 'USD')
{
$cacheKey = "exchange_rates:{$baseCurrency}";
$cacheDuration = 3600; // 1 hour
return Cache::remember($cacheKey, $cacheDuration, function () use ($baseCurrency) {
$response = Http::timeout(10)
->retry(3, 100)
->get("https://api.exchangerate-api.com/v4/latest/{$baseCurrency}");
if ($response->failed()) {
// Fallback to stored rates
return $this->getFallbackRates($baseCurrency);
}
$data = $response->json();
return [
'base' => $data['base'],
'date' => $data['date'],
'rates' => $data['rates'],
'timestamp' => now()->timestamp,
'source' => 'api',
];
});
}
public function cacheApiResponse(Request $request, $cacheDuration = 300)
{
$cacheKey = $this->generateCacheKey($request);
return Cache::remember($cacheKey, $cacheDuration, function () use ($request) {
// Simulate API call
return [
'data' => 'API Response Data',
'timestamp' => now()->toISOString(),
'request_id' => Str::uuid(),
];
});
}
protected function generateCacheKey(Request $request): string
{
$path = $request->path();
$query = $request->query();
$userId = $request->user()?->id;
ksort($query); // Sort query parameters for consistent keys
return sprintf(
'api:%s:%s:%s',
$path,
md5(serialize($query)),
$userId ?: 'guest'
);
}
protected function getFallbackRates($baseCurrency): array
{
// Return cached rates or default rates
$defaultRates = [
'USD' => 1.0,
'EUR' => 0.85,
'GBP' => 0.73,
'JPY' => 110.0,
'CAD' => 1.25,
];
$baseRate = $defaultRates[$baseCurrency] ?? 1.0;
$rates = [];
foreach ($defaultRates as $currency => $rate) {
$rates[$currency] = $rate / $baseRate;
}
return [
'base' => $baseCurrency,
'date' => now()->format('Y-m-d'),
'rates' => $rates,
'timestamp' => now()->timestamp,
'source' => 'fallback',
];
}
}
<?php
// app/Services/ViewCacheService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\View;
class ViewCacheService
{
public function cacheComplexView($viewName, $data, $cacheDuration = 3600)
{
$cacheKey = "view:{$viewName}:" . md5(serialize($data));
return Cache::remember($cacheKey, $cacheDuration, function () use ($viewName, $data) {
return View::make($viewName, $data)->render();
});
}
public function getCachedMenu($menuName)
{
$cacheKey = "menu:{$menuName}";
$cacheDuration = 86400; // 24 hours
return Cache::remember($cacheKey, $cacheDuration, function () use ($menuName) {
return $this->buildMenu($menuName);
});
}
protected function buildMenu($menuName): array
{
// Complex menu building logic
$menu = [
'home' => [
'title' => 'Home',
'url' => '/',
'icon' => 'home',
'children' => [],
],
'products' => [
'title' => 'Products',
'url' => '/products',
'icon' => 'shopping-bag',
'children' => $this->getProductCategories(),
],
// ... more menu items
];
return $menu;
}
protected function getProductCategories(): array
{
// This would typically come from database
return Cache::remember('menu:product_categories', 3600, function () {
return \App\Models\Category::withCount('products')
->where('active', true)
->orderBy('order')
->get()
->map(function ($category) {
return [
'title' => $category->name,
'url' => "/products/category/{$category->slug}",
'count' => $category->products_count,
];
})->toArray();
});
}
}
<?php
// app/Services/TaggedCacheService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use App\Models\User;
use App\Models\Product;
use App\Models\Order;
class TaggedCacheService
{
public function getUserWithTags($userId)
{
$cacheKey = "user:{$userId}:profile";
return Cache::tags(['users', "user:{$userId}"])
->remember($cacheKey, 3600, function () use ($userId) {
return User::with(['profile', 'orders', 'addresses'])
->findOrFail($userId);
});
}
public function getProductWithTags($productId)
{
$cacheKey = "product:{$productId}:full";
return Cache::tags(['products', "product:{$productId}"])
->remember($cacheKey, 1800, function () use ($productId) {
return Product::with(['category', 'brand', 'variants', 'reviews'])
->findOrFail($productId);
});
}
public function clearUserCache($userId)
{
// Clear all cache tagged with this user
Cache::tags(["user:{$userId}"])->flush();
// Also clear general user cache
Cache::tags(['users'])->forget("user:{$userId}:profile");
Cache::tags(['users'])->forget("user:{$userId}:orders");
Cache::tags(['users'])->forget("user:{$userId}:stats");
}
public function clearProductCache($productId)
{
Cache::tags(["product:{$productId}"])->flush();
Cache::tags(['products'])->forget("product:{$productId}:full");
Cache::tags(['products'])->forget("product:{$productId}:basic");
// Clear related caches
Cache::tags(['products'])->forget('products:popular');
Cache::tags(['products'])->forget('products:featured');
}
public function clearAllCache()
{
// Clear cache by tags
Cache::tags(['users'])->flush();
Cache::tags(['products'])->flush();
Cache::tags(['orders'])->flush();
Cache::tags(['categories'])->flush();
// Clear all tagged cache
Cache::flush();
}
public function getCachedDataWithMultipleTags()
{
$cacheKey = 'dashboard:stats';
return Cache::tags(['dashboard', 'stats', 'global'])
->remember($cacheKey, 300, function () {
return [
'users' => [
'total' => User::count(),
'active' => User::where('active', true)->count(),
'new_today' => User::whereDate('created_at', today())->count(),
],
'products' => [
'total' => Product::count(),
'out_of_stock' => Product::where('stock_quantity', 0)->count(),
'low_stock' => Product::where('stock_quantity', '<', 10)->count(),
],
'orders' => [
'total' => Order::count(),
'pending' => Order::where('status', 'pending')->count(),
'today' => Order::whereDate('created_at', today())->count(),
'revenue_today' => Order::whereDate('created_at', today())->sum('total_amount'),
],
'updated_at' => now()->toISOString(),
];
});
}
}
<?php
// app/Services/CacheLockService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Cache\Lock;
use Illuminate\Support\Facades\Log;
class CacheLockService
{
public function processWithLock($key, $callback, $lockTimeout = 10)
{
$lock = Cache::lock($key, $lockTimeout);
try {
if ($lock->get()) {
// We have the lock, process the callback
Log::info("Acquired lock for key: {$key}");
try {
$result = $callback();
} finally {
$lock->release();
Log::info("Released lock for key: {$key}");
}
return $result;
} else {
// Could not acquire lock, handle accordingly
Log::warning("Could not acquire lock for key: {$key}");
throw new \Exception("Could not acquire lock for {$key}");
}
} catch (\Throwable $e) {
// Ensure lock is released on error
if (isset($lock) && $lock->get()) {
$lock->release();
}
throw $e;
}
}
public function updateProductStock($productId, $quantityChange)
{
$lockKey = "product:{$productId}:stock:lock";
return $this->processWithLock($lockKey, function () use ($productId, $quantityChange) {
// This code will only run by one process at a time
$product = \App\Models\Product::findOrFail($productId);
// Simulate complex stock calculation
sleep(1);
$newStock = $product->stock_quantity + $quantityChange;
if ($newStock < 0) {
throw new \Exception("Insufficient stock");
}
$product->update(['stock_quantity' => $newStock]);
// Clear product cache
Cache::forget("product:{$productId}:stock");
Cache::forget("product:{$productId}:full");
return [
'product_id' => $productId,
'old_stock' => $product->stock_quantity,
'new_stock' => $newStock,
'change' => $quantityChange,
];
}, 30); // 30 second lock timeout
}
public function processQueueWithLock($queueName, $callback, $timeout = 60)
{
$lockKey = "queue:{$queueName}:lock";
return Cache::lock($lockKey, $timeout)->block(10, function () use ($callback) {
// Block for up to 10 seconds waiting for the lock
return $callback();
});
}
public function getOrSetWithLock($key, $callback, $ttl = 3600, $lockTimeout = 10)
{
// Try to get cached value first
if (Cache::has($key)) {
return Cache::get($key);
}
// Use lock to prevent cache stampede
$lock = Cache::lock("{$key}:lock", $lockTimeout);
try {
if ($lock->get()) {
// We have the lock, compute and cache the value
$value = $callback();
Cache::put($key, $value, $ttl);
$lock->release();
return $value;
} else {
// Wait for other process to compute the value
$lock->block(5); // Wait up to 5 seconds
return Cache::get($key);
}
} catch (\Throwable $e) {
if (isset($lock) && $lock->get()) {
$lock->release();
}
throw $e;
}
}
}
<?php
// app/Console/Commands/WarmCache.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use App\Models\Product;
use App\Models\User;
use App\Models\Category;
class WarmCache extends Command
{
protected $signature = 'cache:warm
{--all : Warm all caches}
{--products : Warm product caches}
{--users : Warm user caches}
{--categories : Warm category caches}';
protected $description = 'Warm frequently used caches to improve performance';
public function handle()
{
$this->info('Starting cache warming...');
if ($this->option('all') || $this->option('products')) {
$this->warmProductCaches();
}
if ($this->option('all') || $this->option('users')) {
$this->warmUserCaches();
}
if ($this->option('all') || $this->option('categories')) {
$this->warmCategoryCaches();
}
$this->info('Cache warming completed!');
}
protected function warmProductCaches()
{
$this->info('Warming product caches...');
// Warm popular products cache
$popularProducts = Product::with(['category', 'brand'])
->where('status', 'active')
->where('stock_quantity', '>', 0)
->withCount(['orders', 'reviews'])
->orderByDesc('orders_count')
->limit(20)
->get();
Cache::put('products:popular:20', $popularProducts, 3600);
$this->line('✓ Popular products cached');
// Warm featured products
$featuredProducts = Product::with(['category'])
->where('featured', true)
->where('status', 'active')
->limit(10)
->get();
Cache::put('products:featured', $featuredProducts, 3600);
$this->line('✓ Featured products cached');
// Warm new arrivals
$newProducts = Product::with(['category'])
->where('status', 'active')
->orderByDesc('created_at')
->limit(15)
->get();
Cache::put('products:new', $newProducts, 1800);
$this->line('✓ New products cached');
// Warm product counts by category
$categories = Category::withCount(['products' => function ($query) {
$query->where('status', 'active');
}])->get();
foreach ($categories as $category) {
Cache::put("category:{$category->id}:product_count", $category->products_count, 86400);
}
$this->line('✓ Product counts by category cached');
}
protected function warmUserCaches()
{
$this->info('Warming user caches...');
// Warm user stats
$userStats = [
'total' => User::count(),
'active' => User::where('active', true)->count(),
'new_today' => User::whereDate('created_at', today())->count(),
'verified' => User::whereNotNull('email_verified_at')->count(),
];
Cache::put('stats:users', $userStats, 1800);
$this->line('✓ User stats cached');
// Warm top users
$topUsers = User::withCount(['orders', 'reviews'])
->orderByDesc('orders_count')
->limit(10)
->get()
->map(function ($user) {
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'orders_count' => $user->orders_count,
'total_spent' => $user->orders()->sum('total_amount'),
];
});
Cache::put('users:top', $topUsers, 3600);
$this->line('✓ Top users cached');
}
protected function warmCategoryCaches()
{
$this->info('Warming category caches...');
// Warm category tree
$categories = Category::with(['children' => function ($query) {
$query->withCount('products');
}])
->whereNull('parent_id')
->orderBy('order')
->get();
Cache::put('categories:tree', $categories, 86400);
$this->line('✓ Category tree cached');
// Warm all categories with product counts
$allCategories = Category::withCount(['products' => function ($query) {
$query->where('status', 'active');
}])->get();
Cache::put('categories:all', $allCategories, 86400);
$this->line('✓ All categories cached');
}
}
<?php
// app/Services/RedisService.php
namespace App\Services;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
class RedisService
{
public function useRedisDataStructures()
{
// Strings
Redis::set('user:1:name', 'John Doe');
Redis::setex('user:1:session', 3600, 'session_token'); // With expiry
Redis::setnx('user:1:lock', 'processing'); // Set if not exists
$name = Redis::get('user:1:name');
// Hashes (perfect for objects)
Redis::hmset('user:1', [
'name' => 'John Doe',
'email' => 'john@example.com',
'age' => 30,
'city' => 'New York',
]);
$user = Redis::hgetall('user:1');
$email = Redis::hget('user:1', 'email');
// Increment hash field
Redis::hincrby('user:1', 'login_count', 1);
// Lists (queues, stacks)
Redis::rpush('queue:emails', json_encode(['to' => 'user@example.com']));
Redis::lpush('recent:users', 'user:1');
Redis::ltrim('recent:users', 0, 9); // Keep only 10 items
$nextJob = Redis::lpop('queue:emails');
$recentUsers = Redis::lrange('recent:users', 0, -1);
// Sets (unique items)
Redis::sadd('online:users', 'user:1');
Redis::sadd('online:users', 'user:2');
$onlineUsers = Redis::smembers('online:users');
$isOnline = Redis::sismember('online:users', 'user:1');
// Sorted Sets (leaderboards, rankings)
Redis::zadd('leaderboard', 100, 'player:1');
Redis::zadd('leaderboard', 85, 'player:2');
Redis::zadd('leaderboard', 120, 'player:3');
// Increment score
Redis::zincrby('leaderboard', 10, 'player:1');
$topPlayers = Redis::zrevrange('leaderboard', 0, 9, 'WITHSCORES');
$playerRank = Redis::zrevrank('leaderboard', 'player:1');
return [
'user' => $user,
'online_users' => $onlineUsers,
'top_players' => $topPlayers,
];
}
public function implementLeaderboard()
{
$leaderboardKey = 'game:leaderboard';
// Add or update player scores
Redis::zadd($leaderboardKey, [
'player_123' => 1500,
'player_456' => 1420,
'player_789' => 1380,
'player_101' => 1550,
]);
// Get top 10 players
$topPlayers = Redis::zrevrange($leaderboardKey, 0, 9, 'WITHSCORES');
// Get player rank and score
$playerRank = Redis::zrevrank($leaderboardKey, 'player_123');
$playerScore = Redis::zscore($leaderboardKey, 'player_123');
// Increment player score
Redis::zincrby($leaderboardKey, 50, 'player_123');
// Get players in score range
$playersInRange = Redis::zrangebyscore($leaderboardKey, 1400, 1600, [
'WITHSCORES' => true,
]);
return [
'top_players' => $topPlayers,
'player_123' => [
'rank' => $playerRank,
'score' => $playerScore,
],
'players_1400_1600' => $playersInRange,
];
}
public function implementRateLimiting($userId, $action, $limit = 10, $window = 60)
{
$key = "rate_limit:{$userId}:{$action}";
// Get current count
$current = Redis::get($key) ?: 0;
if ($current >= $limit) {
return [
'allowed' => false,
'remaining' => 0,
'reset_in' => Redis::ttl($key),
];
}
// Increment count
Redis::multi();
Redis::incr($key);
Redis::expire($key, $window);
Redis::exec();
$newCount = $current + 1;
return [
'allowed' => true,
'remaining' => $limit - $newCount,
'reset_in' => $window,
];
}
public function trackRealTimeAnalytics()
{
$today = now()->format('Y-m-d');
// Page views
Redis::incr("analytics:page_views:{$today}");
Redis::incr("analytics:page_views:total");
// Unique visitors (using HyperLogLog for approximate counting)
$visitorId = request()->ip() . ':' . request()->userAgent();
Redis::pfadd("analytics:unique_visitors:{$today}", $visitorId);
Redis::pfadd("analytics:unique_visitors:total", $visitorId);
// Real-time online users
$userId = auth()->id();
if ($userId) {
Redis::sadd("analytics:online_users", $userId);
Redis::expire("analytics:online_users", 300); // 5 minutes
}
// Get analytics data
$pageViewsToday = Redis::get("analytics:page_views:{$today}") ?: 0;
$uniqueVisitorsToday = Redis::pfcount("analytics:unique_visitors:{$today}");
$onlineUsers = Redis::smembers("analytics:online_users");
return [
'page_views_today' => $pageViewsToday,
'unique_visitors_today' => $uniqueVisitorsToday,
'online_users_count' => count($onlineUsers),
'online_users' => $onlineUsers,
];
}
}
<?php
// app/Console/Commands/MonitorCache.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\DB;
class MonitorCache extends Command
{
protected $signature = 'cache:monitor
{--driver= : Specific cache driver to monitor}
{--detailed : Show detailed cache information}';
protected $description = 'Monitor cache performance and statistics';
public function handle()
{
$driver = $this->option('driver') ?: config('cache.default');
$this->info("Cache Driver: {$driver}");
$this->info("=========================");
switch ($driver) {
case 'redis':
$this->monitorRedis();
break;
case 'memcached':
$this->monitorMemcached();
break;
case 'database':
$this->monitorDatabaseCache();
break;
case 'file':
$this->monitorFileCache();
break;
default:
$this->error("Unsupported driver: {$driver}");
}
if ($this->option('detailed')) {
$this->showDetailedCacheInfo();
}
}
protected function monitorRedis()
{
try {
$info = Redis::command('INFO');
$this->info("Redis Server Information:");
$this->table(['Metric', 'Value'], [
['Version', $info['redis_version']],
['Uptime (days)', round($info['uptime_in_days'], 2)],
['Connected Clients', $info['connected_clients']],
['Memory Used', $this->formatBytes($info['used_memory'])],
['Memory Peak', $this->formatBytes($info['used_memory_peak'])],
['Keyspace Hits', $info['keyspace_hits']],
['Keyspace Misses', $info['keyspace_misses']],
['Hit Rate', round($info['keyspace_hits'] / max(1, $info['keyspace_hits'] + $info['keyspace_misses']) * 100, 2) . '%'],
]);
// Get keys count by pattern
$this->info("\nCache Key Patterns:");
$patterns = ['*', 'products:*', 'users:*', 'session:*', 'queue:*'];
$keyCounts = [];
foreach ($patterns as $pattern) {
$count = Redis::command('KEYS', [$pattern]);
$keyCounts[] = [$pattern, count($count)];
}
$this->table(['Pattern', 'Key Count'], $keyCounts);
} catch (\Exception $e) {
$this->error("Failed to connect to Redis: " . $e->getMessage());
}
}
protected function monitorMemcached()
{
try {
$memcached = app('cache')->store('memcached')->getStore()->getMemcached();
$stats = $memcached->getStats();
if (empty($stats)) {
$this->error("No Memcached servers found");
return;
}
$this->info("Memcached Server Information:");
foreach ($stats as $server => $serverStats) {
$this->info("\nServer: {$server}");
$this->table(['Metric', 'Value'], [
['Uptime (days)', round($serverStats['uptime'] / 86400, 2)],
['Current Connections', $serverStats['curr_connections']],
['Total Connections', $serverStats['total_connections']],
['Get Hits', $serverStats['get_hits']],
['Get Misses', $serverStats['get_misses']],
['Hit Rate', round($serverStats['get_hits'] / max(1, $serverStats['get_hits'] + $serverStats['get_misses']) * 100, 2) . '%'],
['Current Items', $serverStats['curr_items']],
['Total Items', $serverStats['total_items']],
['Bytes Used', $this->formatBytes($serverStats['bytes'])],
['Evictions', $serverStats['evictions']],
]);
}
} catch (\Exception $e) {
$this->error("Failed to get Memcached stats: " . $e->getMessage());
}
}
protected function monitorDatabaseCache()
{
try {
$table = config('cache.stores.database.table', 'cache');
$stats = DB::table($table)
->select([
DB::raw('COUNT(*) as total_entries'),
DB::raw('SUM(LENGTH(key)) as total_key_size'),
DB::raw('SUM(LENGTH(value)) as total_value_size'),
DB::raw('AVG(TIMESTAMPDIFF(SECOND, expiration, NOW())) as avg_seconds_to_expire'),
DB::raw('SUM(CASE WHEN expiration < NOW() THEN 1 ELSE 0 END) as expired_entries'),
])
->first();
$this->info("Database Cache Information:");
$this->table(['Metric', 'Value'], [
['Total Entries', $stats->total_entries],
['Expired Entries', $stats->expired_entries],
['Active Entries', $stats->total_entries - $stats->expired_entries],
['Total Key Size', $this->formatBytes($stats->total_key_size)],
['Total Value Size', $this->formatBytes($stats->total_value_size)],
['Average Time to Expire', round($stats->avg_seconds_to_expire / 3600, 2) . ' hours'],
]);
} catch (\Exception $e) {
$this->error("Failed to get database cache stats: " . $e->getMessage());
}
}
protected function monitorFileCache()
{
$path = config('cache.stores.file.path', storage_path('framework/cache/data'));
if (!is_dir($path)) {
$this->error("Cache directory not found: {$path}");
return;
}
$files = glob($path . '/*');
$totalSize = 0;
$fileCount = 0;
foreach ($files as $file) {
if (is_file($file)) {
$totalSize += filesize($file);
$fileCount++;
}
}
$this->info("File Cache Information:");
$this->table(['Metric', 'Value'], [
['Cache Directory', $path],
['Total Files', $fileCount],
['Total Size', $this->formatBytes($totalSize)],
['Average File Size', $this->formatBytes($fileCount > 0 ? $totalSize / $fileCount : 0)],
]);
}
protected function showDetailedCacheInfo()
{
$this->info("\nDetailed Cache Information:");
$this->info("===========================");
// Show cache configuration
$config = config('cache');
$this->info("\nCache Configuration:");
$this->table(['Setting', 'Value'], [
['Default Driver', $config['default']],
['Cache Prefix', $config['prefix']],
['Available Stores', implode(', ', array_keys($config['stores']))],
]);
// Test cache operations
$this->info("\nCache Performance Test:");
$start = microtime(true);
Cache::put('test_key', str_repeat('x', 1024), 10); // 1KB data
$writeTime = microtime(true) - $start;
$start = microtime(true);
$value = Cache::get('test_key');
$readTime = microtime(true) - $start;
$this->table(['Operation', 'Time'], [
['Write (1KB)', round($writeTime * 1000, 2) . 'ms'],
['Read (1KB)', round($readTime * 1000, 2) . 'ms'],
]);
Cache::forget('test_key');
}
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
}
<?php
// tests/Feature/CacheTest.php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\Product;
use App\Models\User;
class CacheTest extends TestCase
{
use RefreshDatabase;
public function test_cache_put_and_get()
{
Cache::put('test_key', 'test_value', 60);
$this->assertEquals('test_value', Cache::get('test_key'));
}
public function test_cache_remember()
{
$value = Cache::remember('expensive_calculation', 60, function () {
return 'calculated_value';
});
$this->assertEquals('calculated_value', $value);
$this->assertTrue(Cache::has('expensive_calculation'));
}
public function test_product_cache()
{
$product = Product::factory()->create();
// Cache product
Cache::remember("product:{$product->id}", 3600, function () use ($product) {
return $product;
});
// Retrieve from cache
$cachedProduct = Cache::get("product:{$product->id}");
$this->assertNotNull($cachedProduct);
$this->assertEquals($product->id, $cachedProduct->id);
$this->assertEquals($product->name, $cachedProduct->name);
}
public function test_cache_tags()
{
Cache::tags(['users', 'products'])->put('test_key', 'test_value', 60);
$this->assertEquals('test_value', Cache::tags(['users', 'products'])->get('test_key'));
// Clear by tag
Cache::tags(['users'])->flush();
$this->assertNull(Cache::tags(['users', 'products'])->get('test_key'));
}
public function test_cache_increment_decrement()
{
Cache::put('counter', 0, 60);
Cache::increment('counter');
$this->assertEquals(1, Cache::get('counter'));
Cache::increment('counter', 4);
$this->assertEquals(5, Cache::get('counter'));
Cache::decrement('counter', 2);
$this->assertEquals(3, Cache::get('counter'));
}
public function test_cache_lock()
{
$lock = Cache::lock('test_lock', 10);
$this->assertTrue($lock->get());
$this->assertFalse($lock->get()); // Should not get lock again
$lock->release();
$this->assertTrue($lock->get()); // Should get lock after release
$lock->release();
}
public function test_cache_flush()
{
Cache::put('key1', 'value1', 60);
Cache::put('key2', 'value2', 60);
$this->assertTrue(Cache::has('key1'));
$this->assertTrue(Cache::has('key2'));
Cache::flush();
$this->assertFalse(Cache::has('key1'));
$this->assertFalse(Cache::has('key2'));
}
public function test_cache_with_fake()
{
Cache::fake();
Cache::put('key', 'value', 60);
// Assert cache was "put"
Cache::assertPut('key', 'value', 60);
// Assert cache was "put" with closure
Cache::assertPut('key', function ($value) {
return $value === 'value';
}, 60);
// Assert nothing in cache
Cache::assertNothingWritten();
}
}
<?php
// app/Services/CacheInvalidationService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use App\Models\Product;
use App\Models\User;
use App\Models\Order;
class CacheInvalidationService
{
public function registerEventListeners()
{
// Invalidate cache when product is updated
Event::listen('eloquent.updated: ' . Product::class, function ($product) {
$this->invalidateProductCache($product);
});
Event::listen('eloquent.deleted: ' . Product::class, function ($product) {
$this->invalidateProductCache($product);
});
// Invalidate cache when user is updated
Event::listen('eloquent.updated: ' . User::class, function ($user) {
$this->invalidateUserCache($user);
});
// Invalidate cache when order is created/updated
Event::listen('eloquent.created: ' . Order::class, function ($order) {
$this->invalidateOrderCache($order);
});
Event::listen('eloquent.updated: ' . Order::class, function ($order) {
$this->invalidateOrderCache($order);
});
}
public function invalidateProductCache(Product $product)
{
// Clear specific product cache
Cache::forget("product:{$product->id}:full");
Cache::forget("product:{$product->id}:basic");
Cache::forget("product:{$product->id}:variants");
// Clear product lists
Cache::forget('products:popular');
Cache::forget('products:featured');
Cache::forget('products:new');
Cache::forget('products:category:' . $product->category_id);
// Clear related caches
Cache::forget('stats:products:global');
Cache::tags(['products'])->flush();
// Clear search indexes if using
if (Cache::has('search:products:index')) {
Cache::forget('search:products:index');
}
}
public function invalidateUserCache(User $user)
{
Cache::forget("user:{$user->id}:profile");
Cache::forget("user:{$user->id}:orders");
Cache::forget("user:{$user->id}:stats");
// Clear user lists if this user is in them
$this->invalidateUserLists();
Cache::tags(['users', "user:{$user->id}"])->flush();
}
public function invalidateOrderCache(Order $order)
{
Cache::forget("order:{$order->id}:full");
Cache::forget("order:{$order->id}:basic");
// Clear user's order list
Cache::forget("user:{$order->user_id}:orders");
// Clear order statistics
Cache::forget('stats:orders:daily');
Cache::forget('stats:orders:monthly');
Cache::forget('stats:revenue:daily');
Cache::tags(['orders'])->flush();
}
protected function invalidateUserLists()
{
Cache::forget('users:top:spenders');
Cache::forget('users:top:active');
Cache::forget('users:recent');
}
public function clearStaleCache($olderThanHours = 24)
{
// This is more relevant for file/database cache
$cachePath = storage_path('framework/cache/data');
if (is_dir($cachePath)) {
$files = glob($cachePath . '/*');
$now = time();
foreach ($files as $file) {
if (is_file($file)) {
// Delete files older than specified hours
if ($now - filemtime($file) >= $olderThanHours * 3600) {
unlink($file);
}
}
}
}
}
}
You've now mastered Laravel caching! From basic operations to advanced Redis features and performance optimization, you have the complete toolkit to dramatically improve your application's performance and scalability.