From 3c8ea89ddf3024bf042814c957c7a6191a6846db Mon Sep 17 00:00:00 2001 From: "Kamshory, MT" Date: Wed, 17 Sep 2025 02:51:45 +0700 Subject: [PATCH] New Feature: `SqliteSessionHandler` --- CHANGELOG.md | 24 ++ docs/doc.html | 376 +++++++++++++++++- src/Exceptions/InvalidFileAccessException.php | 47 +++ src/Session/PicoSession.php | 35 +- src/Session/SqliteSessionHandler.php | 167 ++++++++ src/Util/Database/PicoDatabaseUtilBase.php | 2 +- 6 files changed, 611 insertions(+), 40 deletions(-) create mode 100644 src/Exceptions/InvalidFileAccessException.php create mode 100644 src/Session/SqliteSessionHandler.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d6af74..b8bb2822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1367,3 +1367,27 @@ Example: tcp://localhost:6379?db=3 ``` +## New Feature: `SqliteSessionHandler` + +A new **`SqliteSessionHandler`** class has been introduced under `MagicObject\Session`. +This provides a **persistent session storage** mechanism using **SQLite** as the backend. + +### Features + +* Stores sessions in a **SQLite database file** instead of filesystem or memory. +* Automatically **creates the session table** if it does not exist. +* Implements the full session lifecycle: + + * **open** — Initializes session. + * **read** — Reads serialized session data. + * **write** — Writes or updates session data. + * **destroy** — Removes a session by ID. + * **gc** — Garbage collects expired sessions. +* Ensures **safe storage** even when multiple PHP processes are running. + +### Why It Matters? + +* **Portability:** No dependency on Redis or Memcached — only requires SQLite. +* **Lightweight:** Suitable for shared hosting or small applications. +* **Reliability:** Prevents session loss when PHP restarts, unlike file-based sessions. + diff --git a/docs/doc.html b/docs/doc.html index 7d045753..57a70e4a 100644 --- a/docs/doc.html +++ b/docs/doc.html @@ -11,7 +11,7 @@
+

MagicObject\DataTable

@@ -18335,6 +18335,50 @@

Return

+
+

MagicObject\Exceptions\InvalidFileAccessException

+

Declaration

+
class InvalidFileAccessException extends Exception implements Throwable, Stringable +{ +}
+
+

Package

+MagicObject\Exceptions

Authors

+
    +
  1. Kamshory
  2. +
+

Links

+
    +
  1. https://github.com/Planetbiru/MagicObject
  2. +
+

Description

+

Class InvalidFileAccessException

+

Custom exception class for handling errors related to invalid file access.

+
+

Properties

+
+
1. previous
+

Declaration

+
private Throwable $previous;
+
+

Description

+

Previous exception

+
+
+

Methods

+
+
2. getPreviousException
+

Declaration

+
public function getPreviousException() : Throwable|null
{
}
+
+

Description

+

Get the previous exception.

+

Return

+
Throwable|null
+

The previous exception

+
+
+

MagicObject\Exceptions\InvalidFileFormatException

Declaration

@@ -26833,36 +26877,19 @@

Declaration

Description

Extracts Redis connection parameters from a session configuration object.

Parses the Redis save_path (in URL format) from the given SecretObject instance -and returns a stdClass object containing the Redis connection details.

-

Supported save path formats:

+and returns a stdClass object containing the Redis host, port, and optional authentication.

+

Example save path formats:

Parameters

$sessConf

Session configuration object containing the Redis save path.

Return

stdClass
-

An object with the properties:

-
    -
  • host (string) : Redis hostname or IP address
  • -
  • port (int) : Redis port number
  • -
  • auth (string|null) : Authentication password (if any)
  • -
  • db (int) : Redis database index (default: 0)
  • -
  • options (array) : All parsed query parameters
  • -
+

An object with the properties: host (string), port (int), and auth (string|null).

@@ -27244,6 +27271,164 @@

Return

+
+

MagicObject\Session\SqliteSessionHandler

+

Declaration

+
class SqliteSessionHandler +{ +}
+
+

Description

+

Class SqliteSessionHandler

+

A custom session handler implementation using SQLite as the storage backend. +This class manages session lifecycle including create, read, update, +destroy, and garbage collection.

+
+

Properties

+
+
1. pdo
+

Declaration

+
private PDO $pdo;
+
+

Description

+

PDO instance for SQLite connection.

+
+
+
+
2. table
+

Declaration

+
private string $table = 'sessions';
+
+

Description

+

Table name used for storing session data.

+
+
+

Methods

+
+
1. __construct
+

Declaration

+
public function __construct(
+    string $path
+)
{
}
+
+

Description

+

Constructor.

+

Ensures the database file and its parent directory exist, +then initializes the SQLite connection and creates the +sessions table if it does not exist.

+

Parameters

+
$path
+

Absolute path to the SQLite database file.

+

Throws

+
\RuntimeException
+

If the target directory is not writable.

+
+
+
+
2. open
+

Declaration

+
public function open(
+    string $savePath,
+    string $sessionName
+) : bool
{
}
+
+

Description

+

Open the session.

+

Parameters

+
$savePath
+

Session save path.

+
$sessionName
+

Session name.

+

Return

+
bool
+

Always true.

+
+
+
+
3. close
+

Declaration

+
public function close() : bool
{
}
+
+

Description

+

Close the session.

+

Return

+
bool
+

Always true.

+
+
+
+
4. read
+

Declaration

+
public function read(
+    string $id
+) : string
{
}
+
+

Description

+

Read session data by session ID.

+

Parameters

+
$id
+

Session ID.

+

Return

+
string
+

Serialized session data, or empty string if not found.

+
+
+
+
5. write
+

Declaration

+
public function write(
+    string $id,
+    string $data
+) : bool
{
}
+
+

Description

+

Write session data.

+

Parameters

+
$id
+

Session ID.

+
$data
+

Serialized session data.

+

Return

+
bool
+

True on success.

+
+
+
+
6. destroy
+

Declaration

+
public function destroy(
+    string $id
+) : bool
{
}
+
+

Description

+

Destroy a session by ID.

+

Parameters

+
$id
+

Session ID.

+

Return

+
bool
+

True on success.

+
+
+
+
7. gc
+

Declaration

+
public function gc(
+    int $maxlifetime
+) : bool
{
}
+
+

Description

+

Perform garbage collection.

+

Removes expired sessions older than max lifetime.

+

Parameters

+
$maxlifetime
+

Maximum lifetime in seconds.

+

Return

+
bool
+

True if query executed successfully.

+
+
+

MagicObject\Util\AttrUtil

Declaration

@@ -41273,6 +41458,155 @@

Return

The encoded WebSocket frame on success, or false on failure.

+ +
+

MagicObject\Util\XmlToJsonParser

+

Declaration

+
class XmlToJsonParser +{ +}
+
+

Description

+

XmlToJsonParser

+

Utility class for converting between XML and JSON-friendly PHP arrays.

+
    +
  • Converts XML strings to PHP arrays, supporting flattening specified child elements into arrays, +preserving attributes, and casting values to appropriate types.
  • +
  • Converts PHP arrays back to indented XML strings, supporting custom root and item names.
  • +
+
+

Properties

+
+
1. arrayItemNames
+

Declaration

+
private array List of element names to be treated as array items. $arrayItemNames;
+
+
+
+

Methods

+
+
1. __construct
+

Declaration

+
public function __construct(
+    array $arrayItemNames = array ( + 0 => 'item', +)
+)
{
}
+
+

Description

+

Constructor

+

Parameters

+
$arrayItemNames
+

Names of XML elements to be flattened as arrays.

+
+
+
+
2. parse
+

Declaration

+
public function parse(
+    string $xmlString
+) : mixed
{
}
+
+

Description

+

Parse XML string into PHP array.

+

Parameters

+
$xmlString
+

XML content as string.

+

Return

+
mixed
+

Parsed array structure.

+
+
+
+
3. convertElement
+

Declaration

+
private function convertElement(
+    SimpleXMLElement $element
+) : mixed
{
}
+
+

Description

+

Recursively convert SimpleXMLElement to array.

+

Parameters

+
$element
+
+

Return

+
mixed
+
+
+
+
+
4. castValue
+

Declaration

+
private function castValue(
+    string $value
+) : mixed
{
}
+
+

Description

+

Cast string value to appropriate PHP type.

+

Parameters

+
$value
+
+

Return

+
mixed
+
+
+
+
+
5. toXml
+

Declaration

+
public function toXml(
+    array $array,
+    string $rootName = 'root',
+    string $itemName = 'item'
+) : string
{
}
+
+

Description

+

Converts a PHP array to an XML string.

+

Parameters

+
$array
+

The input array to convert.

+
$rootName
+

The name of the root XML element.

+
$itemName
+

The name to use for array items.

+

Return

+
string
+

The resulting XML string.

+
+
+
+
6. arrayToXml
+

Declaration

+
private function arrayToXml(
+    mixed $data,
+    SimpleXMLElement,
+    string $currentName,
+    string $itemName
+) : void
{
}
+
+

Description

+

Recursively adds array data to a SimpleXMLElement.

+

Parameters

+
$data
+

The data to convert (array or scalar).

+
&$xmlElement
+

The XML element to append to.

+
$currentName
+

The current element name.

+
$itemName
+

The name to use for array items.

+

Return

+
void
+

This function traverses the input array or scalar value and appends it to the given XML element.

+
    +
  • If the value is an array, it checks if it is associative or sequential.
  • +
  • Sequential arrays are wrapped using the provided itemName.
  • +
  • Associative arrays are processed by key, handling text nodes ("#text") and attributes ("@attr").
  • +
  • Scalar values are added as text nodes.
  • +
  • Boolean false is converted to string "false" instead of null.
  • +
+
+
diff --git a/src/Exceptions/InvalidFileAccessException.php b/src/Exceptions/InvalidFileAccessException.php new file mode 100644 index 00000000..220a3a8e --- /dev/null +++ b/src/Exceptions/InvalidFileAccessException.php @@ -0,0 +1,47 @@ +previous = $previous; + } + + /** + * Get the previous exception. + * + * @return Throwable|null The previous exception + */ + public function getPreviousException() + { + return $this->previous; + } +} diff --git a/src/Session/PicoSession.php b/src/Session/PicoSession.php index e512398b..619e4a00 100644 --- a/src/Session/PicoSession.php +++ b/src/Session/PicoSession.php @@ -16,7 +16,7 @@ * @package MagicObject\Session * @link https://github.com/Planetbiru/MagicObject */ -class PicoSession +class PicoSession // NOSONAR { const SESSION_STARTED = true; const SESSION_NOT_STARTED = false; @@ -55,6 +55,7 @@ class PicoSession */ public function __construct($sessConf = null) { + error_log(json_encode($sessConf)); if ($sessConf && $sessConf->getName() != "") { $this->setSessionName($sessConf->getName()); } @@ -66,6 +67,18 @@ public function __construct($sessConf = null) $this->saveToRedis($redisParams->host, $redisParams->port, $redisParams->auth, $redisParams->db); } elseif ($sessConf && $sessConf->getSaveHandler() == "files" && $sessConf->getSavePath() != "") { $this->saveToFiles($sessConf->getSavePath()); + } elseif ($sessConf && $sessConf->getSaveHandler() == "sqlite" && $sessConf->getSavePath() != "") { + error_log($sessConf->getSavePath()); + $handler = new SqliteSessionHandler($sessConf->getSavePath()); + session_set_save_handler( + [$handler, 'open'], + [$handler, 'close'], + [$handler, 'read'], + [$handler, 'write'], + [$handler, 'destroy'], + [$handler, 'gc'] + ); + register_shutdown_function('session_write_close'); } } @@ -73,29 +86,15 @@ public function __construct($sessConf = null) * Extracts Redis connection parameters from a session configuration object. * * Parses the Redis `save_path` (in URL format) from the given SecretObject instance - * and returns a stdClass object containing the Redis connection details. + * and returns a stdClass object containing the Redis host, port, and optional authentication. * - * Supported save path formats: + * Example save path formats: * - tcp://127.0.0.1:6379 * - tcp://[::1]:6379 * - tcp://localhost:6379?auth=yourpassword - * - tcp://localhost:6379?password=yourpassword - * - tcp://localhost:6379?db=3 - * - tcp://localhost:6379?dbindex=3 - * - tcp://localhost:6379?database=3 - * - * Recognized query parameters: - * - `auth` or `password` : Redis authentication password - * - `db`, `dbindex`, or `database` : Redis logical database index (default: 0) - * - Any other query parameters are preserved in `options` * * @param SecretObject $sessConf Session configuration object containing the Redis save path. - * @return stdClass An object with the properties: - * - `host` (string) : Redis hostname or IP address - * - `port` (int) : Redis port number - * - `auth` (string|null) : Authentication password (if any) - * - `db` (int) : Redis database index (default: 0) - * - `options` (array) : All parsed query parameters + * @return stdClass An object with the properties: `host` (string), `port` (int), and `auth` (string|null). */ private function getRedisParams($sessConf) // NOSONAR { diff --git a/src/Session/SqliteSessionHandler.php b/src/Session/SqliteSessionHandler.php new file mode 100644 index 00000000..3fa68e98 --- /dev/null +++ b/src/Session/SqliteSessionHandler.php @@ -0,0 +1,167 @@ +pdo = new PDO($dsn); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Create session table if it does not exist + $this->pdo->exec(" + CREATE TABLE IF NOT EXISTS {$this->table} ( + id TEXT PRIMARY KEY, + data TEXT, + timestamp INTEGER + ) + "); + } + + /** + * Open the session. + * + * @param string $savePath Session save path. + * @param string $sessionName Session name. + * + * @return bool Always true. + */ + public function open($savePath, $sessionName) // NOSONAR + { + return true; + } + + /** + * Close the session. + * + * @return bool Always true. + */ + public function close() + { + return true; + } + + /** + * Read session data by session ID. + * + * @param string $id Session ID. + * + * @return string Serialized session data, or empty string if not found. + */ + public function read($id) + { + $stmt = $this->pdo->prepare("SELECT data FROM {$this->table} WHERE id = :id"); + $stmt->execute(array(':id' => $id)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + return isset($row['data']) ? $row['data'] : ''; + } + + /** + * Write session data. + * + * @param string $id Session ID. + * @param string $data Serialized session data. + * + * @return bool True on success. + */ + public function write($id, $data) + { + $time = time(); + $stmt = $this->pdo->prepare(" + INSERT INTO {$this->table} (id, data, timestamp) + VALUES (:id, :data, :time) + ON CONFLICT(id) DO UPDATE SET data = :data, timestamp = :time + "); + + return $stmt->execute(array( + ':id' => $id, + ':data' => $data, + ':time' => $time + )); + } + + /** + * Destroy a session by ID. + * + * @param string $id Session ID. + * + * @return bool True on success. + */ + public function destroy($id) + { + $stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE id = :id"); + return $stmt->execute(array(':id' => $id)); + } + + /** + * Perform garbage collection. + * + * Removes expired sessions older than max lifetime. + * + * @param int $maxlifetime Maximum lifetime in seconds. + * + * @return bool True if query executed successfully. + */ + public function gc($maxlifetime) + { + $old = time() - $maxlifetime; + return $this->pdo->exec("DELETE FROM {$this->table} WHERE timestamp < $old") !== false; + } +} diff --git a/src/Util/Database/PicoDatabaseUtilBase.php b/src/Util/Database/PicoDatabaseUtilBase.php index 86c2faec..4323e04d 100644 --- a/src/Util/Database/PicoDatabaseUtilBase.php +++ b/src/Util/Database/PicoDatabaseUtilBase.php @@ -457,7 +457,7 @@ public function importDataTable( * @return bool True if there may still be more data remaining; false if the import * process has finished and post-import scripts have been executed. */ - public function importData($config, $callbackFunction, $tableName = null, $limit = null, $offset = null) + public function importData($config, $callbackFunction, $tableName = null, $limit = null, $offset = null) // NOSONAR { $databaseConfigSource = $config->getDatabaseSource(); $databaseConfigTarget = $config->getDatabaseTarget();