Una invalidación de caché sobre la que se puede razonar
La convención es simple: cada lugar que escribe datos sabe exactamente qué clave de caché invalida esa escritura, y la olvida ahí mismo.
// app/Services/License/StatamicDriver.php
public function create(
string $name,
string $domain,
): License {
try {
$data = $this->http()->post('/sites', [
'name' => $name,
'domain' => $domain,
])->throw()->json('data');
} finally {
Cache::forget($this->cacheKey());
}
return License::fromProvider((array) $data);
}
El finally está ahí por el peor caso del Capítulo 8: una creación que salió bien en el proveedor y falló en el camino de vuelta. Si la lista siguiera cacheada tras ese fallo, el reintento buscaría la licencia en una lista obsoleta, no la encontraría y crearía una segunda. Así que la lista se olvida tanto si la llamada salió bien como si no.
delete() olvida la misma clave. La lectura, la escritura y la invalidación viven en una sola clase, a pocas líneas de distancia, nombrando la misma clave. Quien lea create() dentro de un año lo ve todo sin tener que buscar en qué otro sitio se toca esa clave.
Compáralo con un helper genérico de «limpiar cachés relacionadas» que se ramifica hacia claves que no nombra. Parece más sofisticado. También es lo que nadie sabe explicar en mitad de un incidente. Si no puedes rastrear una clave de caché hasta la escritura que debería limpiarla, no puedes fiarte de lo que contiene.
El N+1 con el que de verdad te vas a topar
La License API tiene pocas consultas propias. La mayoría de las APIs son lo contrario: su tiempo se va en la base de datos, y el primer error que cometen ahí es siempre el mismo.
Una consulta que parece razonable por separado se ejecuta una vez por cada fila de una colección, cuando debería ejecutarse una vez para la colección entera. Aquí está en un servicio hermano que gestiona nombres de dominio, en un comando que busca dominios cuya suscripción ha vencido:
// Mal: una consulta por iteración
Domain::query()
->where('auto_renew', true)
->lazyById()
->each(function (Domain $domain) {
$active = Subscription::query()
->where('stripe_id', $domain->subscription_id)
->whereNull('ends_at')
->exists();
// ...
});
Mil dominios significan mil búsquedas de suscripción. No tiene por qué ser una relación perezosa escondida detrás de una propiedad. Cualquier consulta lanzada desde dentro de un bucle tiene la misma enfermedad.
// Bien: una consulta por lote
Domain::query()
->where('auto_renew', true)
->chunkById(500, function (Collection $domains) {
$active = Subscription::query()
->whereIn(
'stripe_id',
$domains->pluck('subscription_id'),
)
->whereNull('ends_at')
->pluck('stripe_id')
->flip();
// ... marca cada dominio cuya suscripción
// no está en $active
});
Quinientos dominios cuestan ahora dos consultas en lugar de quinientas. El arreglo no es gratis: mantiene quinientos modelos en memoria a la vez. Casi siempre compensa.
Cuando la consulta es una relación, di por adelantado cuáles necesitas. El listado del Capítulo 5 solo pedía un recuento, con withCount. Una página que muestra los tokens mismos los pide:
$consumers = Consumer::query()
->with('tokens')
->paginate(25);
Dos consultas, haya los consumidores que haya en la página: una para los consumidores y otra para todos sus tokens. La carga perezosa habría ejecutado una más por cada consumidor. Esta es la otra mitad de la regla del Capítulo 5. El Resource usa whenLoaded('tokens') y no puede ejecutar una consulta. El controlador decide, aquí, qué se carga.
El problema del N+1 es que nadie lo nota hasta que un job que terminaba en segundos tarda minutos. Así que haz que Laravel lo note por ti:
// app/Providers/AppServiceProvider.php, en boot()
Model::shouldBeStrict(! $this->app->isProduction());
Con esa línea, una relación cargada de forma perezosa lanza una excepción en desarrollo y en los tests. El N+1 hace fallar el build, antes de que ningún consumidor tenga que esperarlo.
shouldBeStrict() activa de paso dos comprobaciones más. Asignar un atributo que no es asignable en bloque lanza una excepción, donde Eloquent descartaría el valor sin decir palabra, y lo mismo ocurre al leer un atributo que la consulta nunca cargó. El Capítulo 17 se encuentra con la primera. (Documentación de Laravel: Eloquent: Getting Started › Configuring Eloquent Strictness.)