Lección 37 de 45 10 min de lectura

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:

env
CACHE_STORE=file

Drivers disponibles:

  • file: almacena en archivos (default, simple pero lento)
  • database: usa una tabla de base de datos
  • redis: muy rápido, ideal para producción
  • memcached: similar a Redis
  • array: 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
<?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
<?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
<?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
<?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
<?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
<?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:

bash
# 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:

bash
# 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
<?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
<?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 .env con CACHE_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 optimize en 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}");
    }
}

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.

Descubrir cursos premium