Skip to content

marko/session-database

Database session driver --- stores session data in a SQL table for shared access across multiple application servers. Sessions are stored in a sessions table with columns for the session ID, serialized payload, and last activity timestamp. This enables session sharing across multiple web servers behind a load balancer. Garbage collection deletes rows where last_activity exceeds the configured session lifetime.

Implements SessionHandlerInterface from marko/session and requires marko/database for the database connection.

Terminal window
composer require marko/session-database

Requires marko/database for the database connection.

Installing marko/session-database is all that is required to activate database-backed sessions. The package registers DatabaseSessionHandler as the SessionHandlerInterface implementation, binds SessionInterface to Session as a singleton, and adds SessionMiddleware globally --- no manual configuration is needed.

Use SessionInterface as usual:

use Marko\Session\Contracts\SessionInterface;
public function __construct(
private readonly SessionInterface $session,
) {}
public function handle(): void
{
$this->session->start();
$this->session->set('cart_id', $cartId);
$this->session->save();
}

The package ships a DatabaseSession entity (Marko\Session\Database\Entity\DatabaseSession) that owns the sessions table schema. Create the table with marko db:migrate:

Terminal window
marko db:migrate

In development, db:migrate generates a migration for the table in database/migrations/ and applies it. Commit that migration; db:migrate on staging and production applies it, because generation only runs in development. The table has these columns on MySQL, MariaDB and PostgreSQL:

ColumnTypeNotes
idVARCHAR(128)Primary key
payloadTEXTSerialized session data
last_activityINTUnix timestamp of the last write or refresh

The entity only owns the schema: DatabaseSessionHandler reads and writes the table with its own SQL. Because the table belongs to an entity, db:diff reports drift in it and TruncateDatabase empties it between tests.

A sessions table created from the SQL these docs gave before the entity existed already matches it, so db:migrate generates nothing. For a table with a different shape (for example id VARCHAR(255)), db:migrate in development generates a migration that changes it to the columns above, and db:diff shows that change first. Review the migration before you commit it.

Remove expired sessions via CLI:

Terminal window
marko session:gc

This deletes rows where last_activity is older than the configured session lifetime.

A session cookie only resumes a row that exists and is within the lifetime (see Strict session IDs). Replaying an unknown or expired cookie inserts nothing, and a resumed session that wasn’t modified only updates last_activity.

With marko/database-readwrite, read() and validateId() run on the primary through onPrimary(), never on a replica. Logout and ID regeneration delete the old row on the primary, and a lagging replica would otherwise still return it, letting a replayed old cookie resume the logged-out session.

Implements SessionHandlerInterface. Takes a ConnectionInterface connection, the SessionConfig (for the lifetime) and a PSR-20 clock; all three are autowired.

MethodDescription
open(string $path, string $name): boolOpen the session store. Returns true.
close(): boolClose the session store. Returns true.
read(string $id): string|falseRead session data by ID. Returns an empty string if the session does not exist.
write(string $id, string $data): boolWrite session data using a single atomic upsert. MySQL uses ON DUPLICATE KEY UPDATE; PostgreSQL uses ON CONFLICT (id) DO UPDATE. No separate delete-then-insert.
destroy(string $id): boolDelete a session by ID.
gc(int $max_lifetime): int|falseDelete sessions where last_activity is older than max_lifetime seconds. Returns the number of deleted rows.
validateId(string $id): boolSELECT 1 FROM sessions WHERE id = ? AND last_activity >= ?: true only for a row within the configured lifetime. Unknown and expired IDs get a fresh session; expired rows are left for gc().
updateTimestamp(string $id, string $data): boolUPDATE sessions SET last_activity = ? WHERE id = ? for a resumed session whose data didn’t change. Never rewrites the payload and never inserts a row.