Cache
El sistema de cache de Laravel te permite almacenar datos costosos de calcular para recuperarlos rápidamente en peticiones posteriores. Esto mejora significativamente el rendimiento de tu aplicación.
¿Por qué usar cache?
Algunas operaciones son lentas: consultas complejas a la base de datos, llamadas a APIs externas, cálculos intensivos. Si el resultado no cambia frecuentemente, puedes almacenarlo en cache y servirlo instantáneamente.
Casos de uso comunes:
- Consultas frecuentes: listas de categorías, configuraciones
- APIs externas: tasas de cambio, datos meteorológicos
- Cálculos costosos: estadísticas, reportes agregados
- Contenido estático: menús de navegación, footer
Configuración
La configuración de cache está en config/cache.php.
El driver por defecto se define en .env:
CACHE_STORE=file
Drivers disponibles:
file: almacena en archivos (default, simple pero lento)database: usa una tabla de base de datosredis: muy rápido, ideal para producciónmemcached: similar a Redisarray: solo durante la petición (testing)
Recomendación: Usa file
en desarrollo y redis o database
en producción. Redis es la opción más rápida.
Operaciones básicas
Usa la facade Cache o el helper
cache() para interactuar con el sistema:
<?php
use Illuminate\Support\Facades\Cache;
// Guardar un valor (expira en 10 minutos)
Cache::put('key', 'value', now()->addMinutes(10));
// Guardar indefinidamente
Cache::forever('key', 'value');
// Obtener un valor
$value = Cache::get('key');
// Obtener con valor por defecto si no existe
$value = Cache::get('key', 'default');
// Verificar si existe
if (Cache::has('key')) {
// ...
}
// Eliminar un valor
Cache::forget('key');
// Eliminar todo el cache
Cache::flush();
Obtener o almacenar
El patrón más común es: si el valor existe en cache,
devolverlo; si no, calcularlo, guardarlo y devolverlo.
Laravel lo simplifica con remember():
<?php
use App\Models\Category;
use Illuminate\Support\Facades\Cache;
// remember: obtiene del cache o ejecuta el closure y guarda el resultado
$categories = Cache::remember('categories', now()->addHours(24), function () {
return Category::orderBy('name')->get();
});
// rememberForever: lo mismo pero sin expiración
$settings = Cache::rememberForever('app_settings', function () {
return Setting::all()->pluck('value', 'key');
});
La primera vez que se ejecuta, consulta la base de datos y guarda el resultado. Las siguientes veces devuelve el valor cacheado sin tocar la base de datos.
Ejemplo práctico en controlador
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class HomeController extends Controller
{
public function index(): View
{
// Posts populares: se consultan cada hora
$popularPosts = Cache::remember('popular_posts', now()->addHour(), function () {
return Post::withCount('views')
->orderByDesc('views_count')
->take(5)
->get();
});
// Estadísticas: se calculan cada día
$stats = Cache::remember('site_stats', now()->addDay(), function () {
return [
'total_posts' => Post::count(),
'total_views' => Post::sum('views_count'),
'posts_this_month' => Post::whereMonth('created_at', now()->month)->count(),
];
});
return view('home', compact('popularPosts', 'stats'));
}
}
Invalidar cache
Cuando los datos cambian, debes invalidar el cache. Hazlo cuando se modifica el dato original:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\StoreCategoryRequest;
use App\Models\Category;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Cache;
class CategoryController extends Controller
{
public function store(StoreCategoryRequest $request): RedirectResponse
{
Category::create($request->validated());
// Invalidar cache de categorías
Cache::forget('categories');
return redirect()
->route('categories.index')
->with('success', 'Categoría creada');
}
public function update(StoreCategoryRequest $request, Category $category): RedirectResponse
{
$category->update($request->validated());
Cache::forget('categories');
return redirect()
->route('categories.index')
->with('success', 'Categoría actualizada');
}
public function destroy(Category $category): RedirectResponse
{
$category->delete();
Cache::forget('categories');
return redirect()
->route('categories.index')
->with('success', 'Categoría eliminada');
}
}
Cache tags
Los tags permiten agrupar elementos de cache para invalidarlos juntos. Solo funcionan con drivers que soporten tags (Redis, Memcached):
<?php
use Illuminate\Support\Facades\Cache;
// Guardar con tags
Cache::tags(['products', 'featured'])->put('featured_products', $products, now()->addHour());
Cache::tags(['products'])->put('all_products', $allProducts, now()->addHour());
Cache::tags(['categories'])->put('categories', $categories, now()->addDay());
// Obtener
$featured = Cache::tags(['products', 'featured'])->get('featured_products');
// Invalidar todo lo que tenga el tag 'products'
Cache::tags(['products'])->flush();
// Esto borra 'featured_products' y 'all_products', pero no 'categories'
Cache con claves dinámicas
Para datos específicos de un registro, usa claves que incluyan el identificador:
<?php
use App\Models\User;
use Illuminate\Support\Facades\Cache;
// Cache específico por usuario
$userStats = Cache::remember("user_stats_{$user->id}", now()->addMinutes(30), function () use ($user) {
return [
'posts_count' => $user->posts()->count(),
'comments_count' => $user->comments()->count(),
'followers_count' => $user->followers()->count(),
];
});
// Invalidar cuando el usuario actualiza algo
Cache::forget("user_stats_{$user->id}");
Cache de vistas
Blade compila las vistas a PHP y las cachea automáticamente. Si modificas una vista en producción, limpia el cache:
# Limpiar cache de vistas compiladas
php artisan view:clear
# Precompilar todas las vistas (útil en deploy)
php artisan view:cache
Cache de configuración y rutas
En producción, cachea la configuración y rutas para mejorar el rendimiento:
# Cachear configuración (un solo archivo)
php artisan config:cache
# Cachear rutas
php artisan route:cache
# Cachear eventos
php artisan event:cache
# Limpiar todo el cache de la aplicación
php artisan cache:clear
# Optimizar todo para producción (config, routes, views)
php artisan optimize
# Limpiar optimizaciones
php artisan optimize:clear
Importante: No uses config:cache
en desarrollo. Si lo haces, los cambios en .env
no se reflejarán hasta que limpies el cache.
Locks atómicos
Los locks permiten asegurar que solo un proceso ejecute una operación a la vez:
<?php
use Illuminate\Support\Facades\Cache;
// Obtener lock por 10 segundos
$lock = Cache::lock('processing-order-' . $order->id, 10);
if ($lock->get()) {
try {
// Procesar el pedido (solo un proceso a la vez)
$this->processOrder($order);
} finally {
$lock->release();
}
} else {
// El pedido ya se está procesando
return response()->json(['message' => 'Pedido en proceso'], 409);
}
// Alternativa: esperar hasta obtener el lock
$lock = Cache::lock('report-generation', 60);
$lock->block(30, function () {
// Esto espera hasta 30 segundos para obtener el lock
// Si lo consigue, ejecuta el código
$this->generateReport();
});
Helper cache()
El helper cache() ofrece una sintaxis
más concisa:
<?php
// Obtener valor
$value = cache('key');
// Obtener con default
$value = cache('key', 'default');
// Guardar (array con clave => valor y TTL)
cache(['key' => 'value'], now()->addMinutes(10));
// Acceder al store para operaciones avanzadas
cache()->remember('key', 60, fn () => 'value');
cache()->forget('key');
Resumen
- El cache almacena datos costosos para servirlos rápidamente
- Configura el driver en
.envconCACHE_STORE - Usa
Cache::remember()para el patrón obtener-o-calcular - Invalida el cache con
Cache::forget()cuando los datos cambian - Los tags permiten invalidar grupos de cache (solo Redis/Memcached)
- Usa
php artisan optimizeen producción para cachear config, rutas y vistas - Los locks atómicos previenen condiciones de carrera
Ejercicios
Ejercicio 1: Cache de productos destacados
Crea un método en ProductController
que devuelva los 10 productos más vendidos. Usa
cache con expiración de 1 hora. Invalida el cache
cuando se actualice un producto.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class ProductController extends Controller
{
public function featured(): View
{
$featuredProducts = Cache::remember('featured_products', now()->addHour(), function () {
return Product::orderByDesc('sales_count')
->take(10)
->get();
});
return view('products.featured', compact('featuredProducts'));
}
public function update(UpdateProductRequest $request, Product $product): RedirectResponse
{
$product->update($request->validated());
// Invalidar cache de productos destacados
Cache::forget('featured_products');
return redirect()
->route('products.show', $product)
->with('success', 'Producto actualizado');
}
}
Ejercicio 2: Cache de configuración de usuario
Implementa un método que obtenga las preferencias de un usuario del cache. Usa una clave dinámica que incluya el ID del usuario. Las preferencias deben expirar en 30 minutos.
Ver solución
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class UserPreferencesService
{
public function get(User $user): array
{
return Cache::remember(
"user_preferences_{$user->id}",
now()->addMinutes(30),
function () use ($user) {
return $user->preferences ?? [
'theme' => 'light',
'language' => 'es',
'notifications' => true,
];
}
);
}
public function update(User $user, array $preferences): void
{
$user->update(['preferences' => $preferences]);
// Invalidar cache
Cache::forget("user_preferences_{$user->id}");
}
public function clearCache(User $user): void
{
Cache::forget("user_preferences_{$user->id}");
}
}
Ejercicio 3: API con rate limiting usando cache
Crea un método que consulte una API externa de tasas de cambio. Cachea el resultado por 1 hora para no hacer llamadas excesivas. Usa un lock para evitar que múltiples peticiones simultáneas consulten la API.
Ver solución
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class ExchangeRateService
{
public function getRates(string $baseCurrency = 'EUR'): array
{
$cacheKey = "exchange_rates_{$baseCurrency}";
// Intentar obtener del cache
$rates = Cache::get($cacheKey);
if ($rates !== null) {
return $rates;
}
// Usar lock para evitar múltiples llamadas simultáneas
$lock = Cache::lock("fetching_{$cacheKey}", 30);
return $lock->block(10, function () use ($cacheKey, $baseCurrency) {
// Verificar de nuevo (otro proceso pudo haberlo cacheado)
$rates = Cache::get($cacheKey);
if ($rates !== null) {
return $rates;
}
// Llamar a la API
$response = Http::get("https://api.exchangerate.host/latest", [
'base' => $baseCurrency,
]);
if ($response->successful()) {
$rates = $response->json('rates');
// Guardar en cache por 1 hora
Cache::put($cacheKey, $rates, now()->addHour());
return $rates;
}
// Si falla, devolver array vacío
return [];
});
}
public function clearCache(string $baseCurrency = 'EUR'): void
{
Cache::forget("exchange_rates_{$baseCurrency}");
}
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.
Descubrir cursos premium