> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Drivers de sesión personalizados en paquetes

> Analiza el SessionManager de Laravel 13 y explica el contrato de almacenamiento de SessionHandlerInterface, la selección explícita del driver, el TTL y la serialización, los Store retenidos y las pruebas.

Para ofrecer un almacenamiento de sesión propio desde un paquete, separa **el registro de la lógica de almacenamiento** de **la selección por parte de la aplicación**. La simple instalación no debe cambiar dónde se guarda el estado de inicio de sesión; diséñalo para que los usuarios puedan revisar el impacto en las sesiones existentes antes de activarlo.

Esta página se basa en la revisión del código fuente oficial de Laravel Framework `v13.39.0`. Para las operaciones básicas, consulta [Sesión](https://kawax.biz/es/session.md), y para la carga automática de providers, [Descubrimiento de paquetes](https://kawax.biz/es/advanced/package-discovery.md).

## extend recibe un handler, no un Store

El callback de `Session::extend()` devuelve un **handler de almacenamiento** que implementa la `SessionHandlerInterface` de PHP. No es una API para devolver directamente un Store que implemente `Illuminate\Contracts\Session\Session` ni un Repository de caché.

`SessionManager::callCustomCreator()` pasa el valor devuelto por el callback a `buildSession()`, que lo envuelve en un `Store` o en un `EncryptedStore` según `session.encrypt`. En ese momento también aplica el nombre de la cookie y `session.serialization`.

```mermaid theme={null}
flowchart TD
    A["Registrar un nombre de driver propio en boot()"] --> B["El usuario selecciona session.driver"]
    B --> C["La primera llamada a driver() ejecuta el callback de creación"]
    C --> D["Devolver una SessionHandlerInterface"]
    D --> E["SessionManager la envuelve en un Store o<br>EncryptedStore"]
    E --> F["StartSession inicia la carga"]
    F --> G["La aplicación opera con put() y otros métodos"]
    G --> H["El Store serializa y<br>pasa los datos a handler.write()"]
```

En el procesamiento HTTP habitual, el middleware `StartSession` gestiona el inicio y el guardado de la sesión. Para ofrecer un handler, tu paquete no necesita llamar a `session_start()` ni a `session_set_save_handler()`.

## Considera el contrato de almacenamiento por partes

Si implementas tu propio backend, cumple las siguientes funciones.

| Método | Responsabilidad del handler |
| - | - |
| `open($savePath, $sessionName)` | La inicialización necesaria. Si no hace falta, devuelve `true` |
| `close()` | Las tareas de cierre necesarias. Si no hacen falta, devuelve `true` |
| `read($sessionId)` | Devuelve la cadena guardada. Si no hay datos o han expirado, devuelve una cadena vacía |
| `write($sessionId, $data)` | Guarda la cadena recibida y devuelve el éxito o el fallo |
| `destroy($sessionId)` | Elimina los datos del ID indicado |
| `gc($lifetime)` | Elimina los datos expirados según una duración en segundos. No es necesario si el almacenamiento los expira automáticamente |

Como el `Store` se encarga de la serialización y la restauración, el handler no debe volver a aplicar `serialize()` ni `json_encode()` al array de la sesión. Con el cifrado activado, guarda tal cual la cadena procesada por `EncryptedStore`.

> [!WARNING]
>
> Distingue entre la ausencia de datos y un fallo del backend. En el código revisado, `Store::readFromHandler()` trata un resultado de lectura vacío o `false` como una sesión vacía. Además, `Store::save()` no comprueba el valor devuelto por `write()` del handler.
>
> Si tu paquete debe tratar siempre los fallos como excepciones, define la política de forma explícita y pruébala: no silencies las excepciones del almacenamiento y, en los backends que devuelven `false`, detéctalo en el handler y conviértelo en una excepción. Laravel no necesariamente reintenta de forma automática ni notifica un guardado fallido.

## Registra solo la lógica de creación en boot

El siguiente ejemplo mínimo reutiliza el `CacheBasedSessionHandler` integrado. No implementa un nuevo protocolo de almacenamiento, sino que muestra cómo registrar un driver propio que usa un store de caché con nombre. `Acme\SessionBridge\` corresponde a `src/` mediante PSR-4 de Composer.

Crea el provider en `src/SessionBridgeServiceProvider.php`.

```php theme={null}
<?php

namespace Acme\SessionBridge;

use Illuminate\Contracts\Container\Container;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use SessionHandlerInterface;

final class SessionBridgeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Session::extend('acme-cache', function (Container $app): SessionHandlerInterface {
            $config = $app->make('config');
            $store = $config->get('session.store');
            $minutes = (int) $config->get('session.lifetime', 120);

            if (! is_string($store) || $store === '' || $minutes <= 0) {
                throw new InvalidArgumentException(
                    'acme-cache requires a named cache store and a positive session lifetime.'
                );
            }

            return new CacheBasedSessionHandler(
                $app->make('cache')->store($store),
                $minutes,
            );
        });
    }
}
```

`Session::extend()` obtiene el SessionManager y registra la lógica de creación, pero en ese momento todavía no resuelve el store de caché del callback. El almacenamiento se crea cuando ese driver se selecciona por primera vez. No ejecutes comprobaciones de conexión ni guardes datos en el `boot()` del provider.

Si usas tu propia implementación de `SessionHandlerInterface`, sustituye el handler que devuelve el callback. Si llegas a hacer `new Store(...)`, el valor devuelto deja de coincidir con lo que espera el SessionManager.

* Carga el provider como un provider normal, no diferido.
* Completa el registro antes de que la sesión se use por primera vez. Si otro provider resuelve tu driver antes, revisa también el orden de carga.
* Usa un nombre de driver propio del paquete. Un `extend()` con el mismo nombre sobrescribe la lógica de creación, y registrar el nombre de un driver integrado tiene prioridad sobre la implementación integrada.

## La aplicación consumidora lo selecciona explícitamente

Los usuarios cambian las claves correspondientes de su `config/session.php` existente y conservan el resto de la configuración.

```php theme={null}
'driver' => 'acme-cache',
'store' => 'acme-sessions',
```

Para el almacenamiento, añade un store con nombre al array `stores` existente de `config/cache.php`. El siguiente ejemplo usa la conexión Redis `default`. La aplicación se encarga de preparar [la configuración de conexión de Redis y el cliente necesario](https://kawax.biz/es/redis.md).

```php theme={null}
'acme-sessions' => [
    'driver' => 'redis',
    'connection' => 'default',
    'prefix' => 'acme-sessions:',
],
```

En este ejemplo, la conexión y el prefijo dependen de la **configuración del store de caché**. Como no pasa por `createRedisDriver()` del driver de sesión Redis integrado, añadir `session.connection` o `session.prefix` no hace que este callback los aplique automáticamente.

Este callback no modifica el Repository ni la configuración del almacenamiento; usa el store con nombre tal cual. Incluso un store `array` de pruebas comparte el mismo espacio de almacenamiento dentro de la misma aplicación.

No sobrescribas incondicionalmente desde tu paquete `session.driver`, el nombre de la cookie ni el prefijo de caché de toda la aplicación. Confirma también con los usuarios que el almacenamiento no se comparte con otros usos y que varios servidores pueden usar el mismo almacenamiento.

> [!TIP]
>
> Si solo necesitas un almacenamiento habitual en Redis o en base de datos, usa primero los drivers de sesión integrados. Un driver propio es un punto de extensión para cuando necesitas un nuevo almacenamiento o convenciones de almacenamiento adicionales.

## Mantén la unidad del TTL y el formato de almacenamiento

El constructor de `CacheBasedSessionHandler` recibe la duración en **minutos**. `write()` pasa `$minutes * 60` a `put()` del Repository de caché en cada llamada, de modo que en el ejemplo anterior el TTL se renueva con cada escritura. `gc()` devuelve `0` y delega la expiración en la caché.

En cambio, la duración que `StartSession` pasa a `gc($lifetime)` del handler está en **segundos**. No implementes nada que guarde el valor de `session.lifetime` tal cual como si fueran segundos.

Además, los siguientes cambios no se resuelven solo cambiando el nombre de un ajuste.

| Cambio | Impacto en las sesiones existentes |
| - | - |
| Almacenamiento, conexión o prefijo de claves | Los datos antiguos dejan de ser visibles desde el nuevo almacenamiento |
| `session.serialization` | Los datos en el formato antiguo ya no se pueden restaurar con el nuevo formato |
| `session.encrypt` | La lectura y la escritura de texto plano y texto cifrado dejan de coincidir |
| `session.cookie` | Cambia la correspondencia con la cookie de ID de sesión existente que envía el navegador |

Si una actualización del paquete cambia alguno de estos elementos, considera cómo avisar de la reautenticación, si hace falta una migración y los despliegues en los que conviven la versión antigua y la nueva. Igual que en [Gestión de la compatibilidad de versiones de paquetes](https://kawax.biz/es/advanced/package-versioning.md), incluye el formato de almacenamiento y las claves de configuración en la especificación pública que mantienes.

## Cambiar solo la configuración no reemplaza un Store ya creado

`Manager`, la clase padre de SessionManager, retiene por nombre los drivers que ha creado. Además, `SessionServiceProvider` registra `session.store` como singleton.

| Operación | Qué ocurre | Qué no ocurre |
| - | - | - |
| Cambiar solo valores de configuración | Las creaciones futuras pueden leer los nuevos valores | Cambiar el TTL o la conexión de un handler ya creado |
| Volver a registrar `extend()` con el mismo nombre | Se reemplaza el callback de creación | Descartar el Store retenido con ese mismo nombre |
| `forgetDrivers()` | Se eliminan todos los drivers retenidos dentro del Manager | Reemplazar el `session.store` ya resuelto o las referencias al Store retenidas externamente |

`forgetDrivers()` tampoco es una API para eliminar datos de sesión. Recrear el Store durante una petición en curso provoca incoherencias con los datos ya cargados y con otras referencias.

El CacheManager de este ejemplo también retiene stores con nombre. Si cambias su configuración de conexión, no basta con volver a resolver solo el SessionManager.

En despliegues que usan la caché de configuración, reconstruye la caché y reinicia los workers de larga duración para que carguen el código y la configuración nuevos. Reiniciar la aplicación y migrar los datos guardados son tareas distintas.

## El almacenamiento de la sesión y el bloqueo de peticiones son cosas distintas

Implementar un handler propio no serializa por sí solo las peticiones concurrentes a la misma sesión. El bloqueo de `StartSession` usa los locks del store de caché seleccionado con `session.block_store`, que no es necesariamente el mismo que el almacenamiento de la propia sesión.

Si lo controlas con algo como `Route::post(...)->block()`, verifica por separado un store de caché compatible con locks, el uso compartido entre varios servidores y los tiempos de espera y retención. No dejes que tu paquete active por su cuenta la configuración de bloqueo de toda la aplicación.

## Prueba hasta el guardado y la recarga

En los paquetes que usan [Orchestra Testbench](https://kawax.biz/es/advanced/package-testing.md), carga el provider y escribe pruebas que seleccionen realmente tu driver propio. El siguiente es un ejemplo de `tests/SessionDriverTest.php`. La caché `array` de pruebas no usa conexiones externas.

```php theme={null}
<?php

namespace Acme\SessionBridge\Tests;

use Acme\SessionBridge\SessionBridgeServiceProvider;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Session\Store;
use Orchestra\Testbench\TestCase;

final class SessionDriverTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [SessionBridgeServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('session.driver', 'acme-cache');
        $app['config']->set('session.store', 'session-test');
        $app['config']->set('session.lifetime', 2);
        $app['config']->set('session.cookie', 'acme_session');
        $app['config']->set('session.serialization', 'json');
        $app['config']->set('session.encrypt', false);
        $app['config']->set('cache.stores.session-test', ['driver' => 'array']);
    }

    public function test_it_wraps_the_registered_handler(): void
    {
        $store = $this->app->make('session')->driver();

        $this->assertInstanceOf(Store::class, $store);
        $this->assertInstanceOf(CacheBasedSessionHandler::class, $store->getHandler());
        $this->assertSame($store, $this->app->make('session.store'));
    }

    public function test_it_saves_and_reloads_the_payload(): void
    {
        $manager = $this->app->make('session');
        $first = $manager->driver();
        $first->start();
        $first->put('cart.quantity', 3);
        $first->save();

        $id = $first->getId();
        $payload = $first->getHandler()->read($id);
        $this->assertSame(3, json_decode($payload, true)['cart']['quantity']);

        $manager->forgetDrivers();
        $second = $manager->driver();
        $second->setId($id);
        $second->start();

        $this->assertNotSame($first, $second);
        $this->assertSame(3, $second->get('cart.quantity'));

        $second->invalidate();
        $this->assertSame('', $second->getHandler()->read($id));
    }

    public function test_forgetting_drivers_does_not_replace_the_store_binding(): void
    {
        $old = $this->app->make('session.store');
        $manager = $this->app->make('session');

        $this->app['config']->set('session.lifetime', 5);
        $manager->forgetDrivers();

        $this->assertNotSame($old, $manager->driver());
        $this->assertSame($old, $this->app->make('session.store'));
    }
}
```

Esta prueba es una **prueba aislada** que recrea el Store dentro de la misma aplicación. No verifica el middleware HTTP ni el ida y vuelta de las cookies, la persistencia entre procesos ni la expiración y los locks reales de Redis. No uses `array` como almacenamiento compartido en producción.

Antes de publicar tu propio handler, añade también lo siguiente.

* Los IDs no guardados o expirados devuelven una cadena vacía
* La cadena guardada se restaura sin cambios y no se puede recuperar tras eliminarla
* La unidad y la renovación del TTL, y la expiración en el backend real
* Los fallos de lectura, escritura y eliminación producen los valores devueltos o las excepciones definidos
* El ida y vuelta con `session.encrypt` activado y los formatos de serialización admitidos
* Cargar el provider por sí solo no accede al almacenamiento ni se apropia de la selección de los drivers integrados
* Si hace falta, pruebas HTTP que pasen por el middleware `web` real y las cookies, y los locks para peticiones concurrentes

## Páginas relacionadas

> [card] [Sesión](https://kawax.biz/es/session.md)
>
> Revisa la lectura y escritura de sesiones, los datos flash y la regeneración de IDs.

> [card] [Implementación interna de Manager](https://kawax.biz/es/advanced/manager.md)
>
> Revisa la creación y retención de drivers personalizados y la delegación al driver predeterminado.

> [card] [Stores de caché personalizados en paquetes](https://kawax.biz/es/advanced/package-cache-drivers.md)
>
> Revisa el reparto de funciones entre el Store de caché y el Repository que sirven de almacenamiento, y el contrato del TTL.

> [card] [Ciclo de vida de los servicios de paquetes](https://kawax.biz/es/advanced/package-service-lifetimes.md)
>
> Revisa las dependencias retenidas por los workers de larga duración y el aislamiento del estado.

## Fuentes primarias consultadas

* [Documentación oficial de HTTP Session: drivers personalizados y bloqueo](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/session.md)
* [Documentación oficial de desarrollo de paquetes](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/packages.md)
* [SessionManager: creación personalizada y construcción del Store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionManager.php)
* [Store: carga, serialización, guardado y regeneración de IDs](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Store.php)
* [CacheBasedSessionHandler: conversión del TTL y contrato de almacenamiento](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/CacheBasedSessionHandler.php)
* [CacheManager: stores con nombre y configuración de Redis](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Manager: extend, driver, forgetDrivers](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Manager.php)
* [SessionServiceProvider: registro singleton del Manager y session.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionServiceProvider.php)
* [StartSession: guardado, GC y bloqueo mediante la caché](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Middleware/StartSession.php)
* [CacheBasedSessionHandlerTest oficial: verificación de datos inexistentes, TTL y eliminación](https://github.com/laravel/framework/blob/v13.39.0/tests/Session/CacheBasedSessionHandlerTest.php)


## Related topics

- [Stores de caché personalizados y contratos en paquetes](https://kawax.biz/es/advanced/package-cache-drivers.md)
- [Registro y nueva resolución de drivers de Storage en paquetes](https://kawax.biz/es/advanced/package-filesystem-drivers.md)
- [Canales de notificación personalizados en paquetes y verificación de la entrega](https://kawax.biz/es/advanced/package-notification-channels.md)
- [Agentes personalizados](https://kawax.biz/es/packages/laravel-copilot-sdk/custom-agents.md)
- [Hooks de sesión](https://kawax.biz/es/packages/laravel-copilot-sdk/hooks.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.