Saltar contenido

Contexto

Introducción

Las capacidades de "contexto" de Laravel te permiten capturar, recuperar y compartir información a lo largo de solicitudes, trabajos y comandos que se ejecutan dentro de tu aplicación. Esta información capturada también se incluye en los registros escritos por tu aplicación, brindándote una visión más profunda de la historia de ejecución del código circundante que ocurrió antes de que se escribiera una entrada de registro y permitiéndote rastrear flujos de ejecución a lo largo de un sistema distribuido.

Cómo Funciona

La mejor manera de entender las capacidades de contexto de Laravel es verlo en acción utilizando las funciones de registro integradas. Para comenzar, puedes agregar información al contexto usando la facade Context. En este ejemplo, utilizaremos un middleware para añadir la URL de la solicitud y un ID de seguimiento único al contexto en cada solicitud entrante:

<?php
 
namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
 
class AddContext
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
Context::add('url', $request->url());
Context::add('trace_id', Str::uuid()->toString());
 
return $next($request);
}
}

La información añadida al contexto se apena automáticamente como metadatos a cualquier entrada de log que se escriba a lo largo de la solicitud. Añadir contexto como metadatos permite que la información pasada a las entradas de log individuales se diferencie de la información compartida a través de Context. Por ejemplo, imagina que escribimos la siguiente entrada de log:

Log::info('User authenticated.', ['auth_id' => Auth::id()]);

El registro escrito contendrá el auth_id pasado a la entrada del registro, pero también incluirá la url del contexto y trace_id como metadatos:

User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

La información añadida al contexto también se pone a disposición de los trabajos despachados a la cola. Por ejemplo, imagina que despachamos un trabajo ProcessPodcast a la cola después de añadir información al contexto:

// In our middleware...
Context::add('url', $request->url());
Context::add('trace_id', Str::uuid()->toString());
 
// In our controller...
ProcessPodcast::dispatch($podcast);

Cuando se despacha el trabajo, cualquier información que actualmente se almacene en el contexto se captura y se comparte con el trabajo. La información capturada se rehidrata de nuevo en el contexto actual mientras se ejecuta el trabajo. Así que, si el método handle de nuestro trabajo escribiera en el registro:

class ProcessPodcast implements ShouldQueue
{
use Queueable;
 
// ...
 
/**
* Execute the job.
*/
public function handle(): void
{
Log::info('Processing podcast.', [
'podcast_id' => $this->podcast->id,
]);
 
// ...
}
}

La entrada de registro resultante contendría la información que se añadió al contexto durante la solicitud que despachó originalmente el trabajo:

Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

Aunque nos hemos centrado en las características de registro integradas relacionadas con el contexto de Laravel, la documentación siguiente ilustrará cómo el contexto te permite compartir información a través del límite de la solicitud HTTP / el trabajo en cola y incluso cómo añadir datos de contexto ocultos que no se escriben junto con las entradas de registro.

Capturando Contexto

Puedes almacenar información en el contexto actual utilizando el método add de la fachada Context:

use Illuminate\Support\Facades\Context;
 
Context::add('key', 'value');

Para añadir múltiples elementos a la vez, puedes pasar un array asociativo al método add:

Context::add([
'first_key' => 'value',
'second_key' => 'value',
]);

El método add sobrescribirá cualquier valor existente que comparta la misma clave. Si solo deseas agregar información al contexto si la clave no existe ya, puedes usar el método addIf:

Context::add('key', 'first');
 
Context::get('key');
// "first"
 
Context::addIf('key', 'second');
 
Context::get('key');
// "first"

Contexto Condicional

El método when se puede utilizar para añadir datos al contexto en función de una condición dada. La primera función anónima proporcionada al método when se invocará si la condición dada evalúa a true, mientras que la segunda función anónima se invocará si la condición evalúa a false:

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Context;
 
Context::when(
Auth::user()->isAdmin(),
fn ($context) => $context->add('permissions', Auth::user()->permissions),
fn ($context) => $context->add('permissions', []),
);

Pilas

El contexto ofrece la capacidad de crear "pilas", que son listas de datos almacenadas en el orden en que se añadieron. Puedes añadir información a una pila invocando el método push:

use Illuminate\Support\Facades\Context;
 
Context::push('breadcrumbs', 'first_value');
 
Context::push('breadcrumbs', 'second_value', 'third_value');
 
Context::get('breadcrumbs');
// [
// 'first_value',
// 'second_value',
// 'third_value',
// ]

Las pilas pueden ser útiles para capturar información histórica sobre una solicitud, como eventos que están ocurriendo a lo largo de tu aplicación. Por ejemplo, podrías crear un oyente de eventos para añadir a una pila cada vez que se ejecute una consulta, capturando la SQL de la consulta y la duración como una tupla:

use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\DB;
 
DB::listen(function ($event) {
Context::push('queries', [$event->time, $event->sql]);
});

Puedes determinar si un valor está en una pila utilizando los métodos stackContains y hiddenStackContains:

if (Context::stackContains('breadcrumbs', 'first_value')) {
//
}
 
if (Context::hiddenStackContains('secrets', 'first_value')) {
//
}

Los métodos stackContains y hiddenStackContains también aceptan una función anónima como su segundo argumento, lo que permite un mayor control sobre la operación de comparación de valores:

use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
 
return Context::stackContains('breadcrumbs', function ($value) {
return Str::startsWith($value, 'query_');
});

Recuperando Contexto

Puedes recuperar información del contexto utilizando el método get de la fachada Context.

use Illuminate\Support\Facades\Context;
 
$value = Context::get('key');

El método only se puede utilizar para recuperar un subconjunto de la información en el contexto:

$data = Context::only(['first_key', 'second_key']);

El método pull se puede usar para recuperar información del contexto y eliminarla inmediatamente del contexto:

$value = Context::pull('key');

Si deseas recuperar toda la información almacenada en el contexto, puedes invocar el método all:

$data = Context::all();

Determinando la Existencia de un Elemento

Puedes usar el método has para determinar si el contexto tiene algún valor almacenado para la clave dada:

use Illuminate\Support\Facades\Context;
 
if (Context::has('key')) {
// ...
}

El método has devolverá true independientemente del valor almacenado. Entonces, por ejemplo, una clave con un valor null será considerada presente:

Context::add('key', null);
 
Context::has('key');
// true

Eliminando Contexto

El método forget se puede utilizar para eliminar una clave y su valor del contexto actual:

use Illuminate\Support\Facades\Context;
 
Context::add(['first_key' => 1, 'second_key' => 2]);
 
Context::forget('first_key');
 
Context::all();
 
// ['second_key' => 2]

Puedes olvidar varias claves a la vez proporcionando un array al método forget:

Context::forget(['first_key', 'second_key']);

Contexto Oculto

El contexto ofrece la posibilidad de almacenar datos "ocultos". Esta información oculta no se añade a los registros y no es accesible a través de los métodos de recuperación de datos documentados arriba. El contexto proporciona un conjunto diferente de métodos para interactuar con la información de contexto oculta:

use Illuminate\Support\Facades\Context;
 
Context::addHidden('key', 'value');
 
Context::getHidden('key');
// 'value'
 
Context::get('key');
// null

Los métodos "ocultos" reflejan la funcionalidad de los métodos no ocultos documentados arriba:

Context::addHidden(/* ... */);
Context::addHiddenIf(/* ... */);
Context::pushHidden(/* ... */);
Context::getHidden(/* ... */);
Context::pullHidden(/* ... */);
Context::onlyHidden(/* ... */);
Context::allHidden(/* ... */);
Context::hasHidden(/* ... */);
Context::forgetHidden(/* ... */);

Eventos

El contexto despacha dos eventos que te permiten engancharte en el proceso de hidratación y deshidratación del contexto. Para ilustrar cómo se pueden utilizar estos eventos, imagina que en un middleware de tu aplicación estableces el valor de configuración app.locale basado en el encabezado Accept-Language de la solicitud HTTP entrante. Los eventos del contexto te permiten capturar este valor durante la solicitud y restaurarlo en la cola, asegurando que las notificaciones enviadas en la cola tengan el valor app.locale correcto. Podemos usar los eventos del contexto y datos ocultos para lograr esto, lo cual ilustrará la siguiente documentación.

Deshidratación

Cada vez que un trabajo se envía a la cola, los datos en el contexto son "deshidratados" y capturados junto con la carga útil del trabajo. El método Context::dehydrating te permite registrar una función anónima que se invocará durante el proceso de deshidratación. Dentro de esta función anónima, puedes hacer cambios en los datos que se compartirán con el trabajo en cola. Típicamente, deberías registrar los callbacks dehydrating dentro del método boot de la clase AppServiceProvider de tu aplicación:

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Context::dehydrating(function (Repository $context) {
$context->addHidden('locale', Config::get('app.locale'));
});
}

[!NOTA] No debes usar la fachada Context dentro del callback dehydrating, ya que eso cambiará el contexto del proceso actual. Asegúrate de solo hacer cambios en el repositorio pasado al callback.

Hidratado

Siempre que un trabajo en cola comience a ejecutarse en la cola, cualquier contexto que se haya compartido con el trabajo será "hidratado" de vuelta en el contexto actual. El método Context::hydrated te permite registrar una función anónima que se invocará durante el proceso de hidratación. Típicamente, debes registrar callbacks hydrated dentro del método boot de la clase AppServiceProvider de tu aplicación:

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Context::hydrated(function (Repository $context) {
if ($context->hasHidden('locale')) {
Config::set('app.locale', $context->getHidden('locale'));
}
});
}

[!NOTA] No debes usar la facade Context dentro del callback hydrated y en su lugar, asegúrate de que solo hagas cambios en el repositorio pasado al callback.