An open API service indexing awesome lists of open source software.

https://github.com/baraja-core/session

Simple performance package for storage your sessions to database by native PDO.
https://github.com/baraja-core/session

database doctrine handler storage

Last synced: 2 months ago
JSON representation

Simple performance package for storage your sessions to database by native PDO.

Awesome Lists containing this project

README

          

# PHP Native PDO Session Storage

Simple, high-performance package for storing PHP sessions in a MySQL database using native `\PDO`. This package provides a robust database-backed session handler that works seamlessly with both native PHP applications and the Nette Framework.

## 🎯 Key Features

- **Native PDO Implementation** - Uses PHP's native PDO extension for reliable database connections with prepared statements
- **Framework Agnostic** - Works with native PHP applications out of the box
- **Nette Framework Integration** - Includes DI extension for seamless Nette integration
- **Doctrine ORM Support** - Optional entity mapping for Doctrine-based applications
- **Automatic Garbage Collection** - Probabilistic GC with configurable session lifetime (14 days default)
- **CLI Safe** - Automatically skips session operations in CLI mode to prevent errors
- **Binary Data Support** - Automatic Base64 encoding fallback for problematic session data
- **Tracy Debugger Integration** - Logs errors to Tracy when available
- **Custom Table Names** - Configurable database table name for multi-tenant applications

## πŸ—οΈ Architecture Overview

The package consists of three main components that work together to provide database session storage:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Application Layer β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ Native PHP Nette Framework β”‚
β”‚ ─────────── ─────────────── β”‚
β”‚ session_set_save_handler() SessionExtension β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β–Ό β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ SessionStorage β”‚ β”‚
β”‚ β”‚ implements \SessionHandlerInterface β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β€’ open() - Initialize connection β”‚ β”‚
β”‚ β”‚ β€’ close() - Cleanup β”‚ β”‚
β”‚ β”‚ β€’ read() - Load session data β”‚ β”‚
β”‚ β”‚ β€’ write() - Persist session data β”‚ β”‚
β”‚ β”‚ β€’ destroy() - Remove session β”‚ β”‚
β”‚ β”‚ β€’ gc() - Garbage collection β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ PDO β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ MySQL Database β”‚ β”‚
β”‚ β”‚ core__session_storage table β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β€’ id (varchar 26) - Session identifier β”‚ β”‚
β”‚ β”‚ β€’ haystack (longtext) - Serialized data β”‚ β”‚
β”‚ β”‚ β€’ last_update (datetime) - Last modification β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ Optional: SessionEntity (Doctrine ORM mapping) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

## 🧩 Components

### SessionStorage

The core component implementing PHP's `\SessionHandlerInterface`. It handles all session operations:

- **Connection Management**: Creates a dedicated PDO connection with proper character encoding (UTF-8) and error handling
- **Session Reading**: Retrieves session data from database, handles Base64-encoded data transparently
- **Session Writing**: Persists session data with automatic timestamp updates, falls back to Base64 encoding for binary data
- **Session Destruction**: Removes session records from the database
- **Garbage Collection**: Probabilistic cleanup (0.1% chance per request) removing sessions older than 14 days, limited to 500 records per run

### SessionExtension

A Nette Framework DI extension that provides:

- **Automatic Service Registration**: Registers `SessionStorage` as a service
- **Doctrine Integration**: Automatically registers entity paths when Doctrine is present
- **DBAL Connection Reuse**: Can inherit database credentials from existing Doctrine DBAL connection
- **Session Handler Setup**: Automatically configures Nette's Session to use the database handler

### SessionEntity

A Doctrine ORM entity class providing:

- **Entity Mapping**: Proper ORM annotations for the session table
- **Type Safety**: Strongly typed properties for session data
- **Immutable Timestamps**: Uses `DateTimeImmutable` for the last update field

## πŸ“¦ Installation

It's best to use [Composer](https://getcomposer.org) for installation, and you can also find the package on
[Packagist](https://packagist.org/packages/baraja-core/session) and
[GitHub](https://github.com/baraja-core/session).

To install, simply use the command:

```shell
$ composer require baraja-core/session
```

You can use the package manually by creating an instance of the internal classes, or register a DIC extension to link the services directly to the Nette Framework.

### Requirements

- PHP 8.1 or higher
- PDO extension (`ext-PDO`)
- MySQL database

## πŸ—„οΈ Database Setup

Create the database table `core__session_storage` (table name can be configured) or use Doctrine for automatic schema generation.

### MySQL Table Schema

```sql
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';

CREATE TABLE `core__session_storage` (
`id` varchar(26) COLLATE utf8_unicode_ci NOT NULL,
`haystack` longtext COLLATE utf8_unicode_ci NOT NULL,
`last_update` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
```

> **Note**: The table can be used with Doctrine ORM or as a standalone MySQL table.

### Using Doctrine Migrations

If you're using Doctrine ORM, the schema can be generated automatically from the `SessionEntity` class. Simply run your standard Doctrine migration commands:

```shell
$ php bin/console doctrine:schema:update --force
```

## βš™οΈ Configuration

### Native PHP Usage

For standalone PHP applications, create a `SessionStorage` instance and register it as the session handler:

```php
$handler = new \Baraja\Session\SessionStorage(
'127.0.0.1', // MySQL host
'my_application', // Database name
'root', // Username
'****' // Password
);

session_set_save_handler($handler);
session_start();
```

> **Warning**: The session handler must be registered **before** calling `session_start()`!

### Nette Framework Usage

Register the extension in your NEON configuration file:

```yaml
extensions:
barajaPdoSession: Baraja\Session\SessionExtension
```

The session storage will be configured automatically.

#### Manual Configuration

If you need to specify database credentials explicitly:

```yaml
extensions:
barajaPdoSession: Baraja\Session\SessionExtension

barajaPdoSession:
host: 127.0.0.1
dbName: my_application
username: root
password: ****
table: core__session_storage # optional, this is the default
```

#### Automatic Doctrine DBAL Integration

When using the `baraja-core/doctrine` package, the extension automatically inherits database credentials from your existing DBAL connection. No additional configuration is required!

## πŸ”§ Advanced Configuration

### Custom Table Name

You can customize the database table name in two ways:

**1. Via Constructor Parameter:**

```php
$handler = new \Baraja\Session\SessionStorage(
'localhost',
'mydb',
'user',
'password',
'my_custom_sessions_table' // Custom table name
);
```

**2. Via Setter Method:**

```php
$handler = new \Baraja\Session\SessionStorage(/* ... */);
$handler->setTable('my_custom_sessions_table');
```

**3. Via NEON Configuration (Nette):**

```yaml
barajaPdoSession:
table: my_custom_sessions_table
```

> **Note**: The default table name is `core__session_storage`. While the table name can be changed at runtime, it is not recommended.

### Garbage Collection

The garbage collector runs automatically with a 0.1% probability on each request. It:

- Removes sessions that haven't been updated in 14 days
- Processes a maximum of 500 records per run to prevent long-running queries
- Is skipped entirely in CLI mode

## πŸ”’ Security Considerations

### Session Data Encoding

When session data contains characters incompatible with MySQL's UTF-8 encoding, the package automatically:

1. Detects the encoding failure via PDO exception
2. Re-encodes the data using Base64 with a `_BASE:` prefix
3. Transparently decodes on read

This ensures binary data and special characters are safely stored without data loss.

### CLI Mode Protection

Session operations are automatically skipped in CLI mode (detected by absence of `$_SERVER['REMOTE_ADDR']`). This prevents:

- Errors when running console commands
- Unnecessary database connections in CLI scripts
- Accidental session manipulation from cron jobs

### Error Handling

- Database query failures throw `\RuntimeException` with helpful error messages
- Session corruption errors are logged to Tracy (if available) and display a user-friendly error message
- Failed session writes gracefully degrade without crashing the application

## πŸ” How It Works

### Session Lifecycle

1. **Initialization**: When a request arrives, `SessionStorage` creates a PDO connection
2. **Reading**: The `read()` method fetches session data by ID, creating a new record if none exists
3. **Processing**: Your application uses `$_SESSION` as normal
4. **Writing**: At request end, `write()` updates the session record with new data and timestamp
5. **Cleanup**: GC randomly triggers to remove old sessions (0.1% probability)

### Data Flow

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Request │────▢│ SessionStorage │────▢│ Database β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ read SID β”‚ β”‚ SELECT by ID β”‚ β”‚ Return row β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Decode haystack β”‚ β”‚
β”‚ β”‚ (Base64 if needed)β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β–Ό β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Response │◀────│ SessionStorage │◀────│ Database β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ write data β”‚ β”‚ UPDATE record β”‚ β”‚ Store row β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

### Retry Mechanism

The `loadById()` method includes a retry mechanism (up to 5 attempts) for handling race conditions when creating new session records. This ensures reliability under concurrent access.

## πŸ“š API Reference

### SessionStorage

```php
class SessionStorage implements \SessionHandlerInterface
{
public function __construct(
string $host,
string $dbName,
string $username,
?string $password = null,
?string $table = null,
);

public function setTable(string $table): void;
public function open($savePath, $sessionName): bool;
public function close(): bool;
public function read($id): string;
public function write($id, $data): bool;
public function destroy($id): bool;
public function gc($maxlifetime): int|false;
}
```

### SessionExtension (Nette)

Configuration schema:

| Option | Type | Required | Default | Description |
|------------|----------|----------|------------------------|-----------------------|
| `host` | `string` | No* | - | MySQL host |
| `dbName` | `string` | No* | - | Database name |
| `username` | `string` | No* | - | Database username |
| `password` | `string` | No* | - | Database password |
| `table` | `string` | No | `core__session_storage`| Table name |

\* Required unless Doctrine DBAL is available (credentials are then inherited automatically).

## πŸ› Troubleshooting

### "Session was corrupted" Error

This error appears when session data cannot be written due to encoding issues. The package attempts to re-encode using Base64, but if this also fails:

1. Check your MySQL character set configuration
2. Ensure the `haystack` column uses `utf8_unicode_ci` or `utf8mb4_unicode_ci`
3. Clear corrupted sessions: `DELETE FROM core__session_storage WHERE id = 'problematic_id'`

### "mb_substr" Function Not Available

The error `Function "mb_substr" is not available` indicates the `mbstring` extension is not installed. Install it:

```shell
# Debian/Ubuntu
sudo apt-get install php-mbstring

# CentOS/RHEL
sudo yum install php-mbstring
```

### Session Not Persisting

1. Verify the handler is registered before `session_start()`
2. Check database credentials and connectivity
3. Ensure the session table exists with correct schema
4. Verify you're not in CLI mode (sessions are disabled in CLI)

## πŸ‘€ Author

**Jan BarΓ‘Ε‘ek**

- Website: [https://baraja.cz](https://baraja.cz)
- GitHub: [https://github.com/baraja-core](https://github.com/baraja-core)

## πŸ“„ License

`baraja-core/session` is licensed under the MIT license. See the [LICENSE](https://github.com/baraja-core/session/blob/master/LICENSE) file for more details.