diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b440ad..19f7612 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,24 +1,25 @@
name: CI
-on: [push, pull_request]
+on:
+ push:
+ pull_request:
permissions:
contents: read
actions: read
- id-token: none
jobs:
composer:
runs-on: ubuntu-latest
strategy:
matrix:
- php: [ 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4, 8.5 ]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Cache Composer dependencies
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: /tmp/composer-cache
key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
@@ -27,6 +28,7 @@ jobs:
uses: php-actions/composer@v6
with:
php_version: ${{ matrix.php }}
+ php_extensions: pcntl
- name: Archive build
run: mkdir /tmp/github-actions/ && tar --exclude=".git" -cvf /tmp/github-actions/build.tar ./
@@ -36,13 +38,14 @@ jobs:
with:
name: build-artifact-${{ matrix.php }}
path: /tmp/github-actions
+ retention-days: 1
phpunit:
runs-on: ubuntu-latest
needs: [ composer ]
strategy:
matrix:
- php: [ 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4, 8.5 ]
outputs:
coverage: ${{ steps.store-coverage.outputs.coverage_text }}
@@ -77,7 +80,7 @@ jobs:
needs: [ phpunit ]
strategy:
matrix:
- php: [ 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4, 8.5 ]
steps:
- uses: actions/checkout@v4
@@ -92,13 +95,16 @@ jobs:
- name: Upload to Codecov
uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ slug: ${{ github.repository }}
phpstan:
runs-on: ubuntu-latest
needs: [ composer ]
strategy:
matrix:
- php: [ 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4, 8.5 ]
steps:
- uses: actions/download-artifact@v4
@@ -114,13 +120,15 @@ jobs:
with:
php_version: ${{ matrix.php }}
path: src/
+ level: 6
+ memory_limit: 256M
phpmd:
runs-on: ubuntu-latest
needs: [ composer ]
strategy:
matrix:
- php: [ 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4, 8.5 ]
steps:
- uses: actions/download-artifact@v4
@@ -132,7 +140,7 @@ jobs:
run: tar -xvf /tmp/github-actions/build.tar ./
- name: PHP Mess Detector
- uses: php-actions/phpmd@v2
+ uses: php-actions/phpmd@v1
with:
php_version: ${{ matrix.php }}
path: src/
@@ -144,7 +152,7 @@ jobs:
needs: [ composer ]
strategy:
matrix:
- php: [ 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4, 8.5 ]
steps:
- uses: actions/download-artifact@v4
@@ -161,21 +169,3 @@ jobs:
php_version: ${{ matrix.php }}
path: src/
standard: phpcs.xml
-
- remove_old_artifacts:
- runs-on: ubuntu-latest
-
- permissions:
- actions: write
-
- steps:
- - name: Remove old artifacts for prior workflow runs on this repository
- env:
- GH_TOKEN: ${{ github.token }}
- run: |
- gh api "/repos/${{ github.repository }}/actions/artifacts" | jq ".artifacts[] | select(.name | startswith(\"build-artifact\")) | .id" > artifact-id-list.txt
- while read id
- do
- echo -n "Deleting artifact ID $id ... "
- gh api --method DELETE /repos/${{ github.repository }}/actions/artifacts/$id && echo "Done"
- done
-
+
## Example usage: Welcome a user by their first name or log out the user
@@ -42,6 +42,43 @@ else {
}
```
+## Redis session storage
+
+This package now includes `GT\Session\RedisHandler` for shared session storage.
+It works with Redis-compatible backends such as Redis and Valkey, and is intended
+for deployments where application nodes are disposable and session state needs to
+survive traffic moving between servers.
+
+`RedisHandler` expects `save_path` to be a DSN rather than a filesystem path.
+It uses the `phpredis` extension at runtime.
+
+Example production config:
+
+```ini
+[session]
+handler=GT\Session\RedisHandler
+save_path=rediss://default:secret@example-redis.internal:25061/0?prefix=GT:&ttl=1440
+name=GT
+use_cookies=true
+```
+
+Supported DSN forms:
+
+- `redis://host:6379`
+- `redis://:password@host:6379/0`
+- `redis://username:password@host:6379/0`
+- `rediss://username:password@host:6379/0`
+
+Useful query parameters:
+
+- `prefix`: key prefix for stored sessions, defaults to `:`
+- `ttl`: session lifetime in seconds, defaults to `session.gc_maxlifetime`
+- `timeout`: connection timeout in seconds
+- `read_timeout`: socket read timeout in seconds
+- `persistent=1`: enable persistent connections
+- `persistent_id`: optional persistent connection pool id
+- `verify_peer=0` / `verify_peer_name=0`: optional TLS verification flags
+
# Proudly sponsored by
[JetBrains Open Source sponsorship program](https://www.jetbrains.com/community/opensource/)
diff --git a/composer.json b/composer.json
index 48ab401..d11c730 100644
--- a/composer.json
+++ b/composer.json
@@ -7,6 +7,9 @@
"php": ">=8.2",
"phpgt/typesafegetter": "^1.3"
},
+ "suggest": {
+ "ext-redis": "Required to use GT\\Session\\RedisHandler."
+ },
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.1",
@@ -14,14 +17,28 @@
"squizlabs/php_codesniffer": "^3.7"
},
+ "scripts": {
+ "phpunit": "vendor/bin/phpunit --configuration phpunit.xml",
+ "phpstan": "vendor/bin/phpstan analyse --level 6 src --memory-limit 256M",
+ "phpcs": "vendor/bin/phpcs src --standard=phpcs.xml",
+ "phpmd": "vendor/bin/phpmd src/ text phpmd.xml",
+ "test": [
+ "@phpunit",
+ "@phpstan",
+ "@phpcs",
+ "@phpmd"
+ ]
+ },
+
"autoload": {
"psr-4": {
+ "GT\\Session\\": "./src",
"Gt\\Session\\": "./src"
}
},
"autoload-dev": {
"psr-4": {
- "Gt\\Session\\Test\\": "./test/phpunit"
+ "GT\\Session\\Test\\": "./test/phpunit"
}
},
diff --git a/composer.lock b/composer.lock
index 49e9755..807a21f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "90f742569783bb1b522aa97fe7d3d045",
+ "content-hash": "5bf8b129b89ebdbd2f4b53a66a2b077e",
"packages": [
{
"name": "phpgt/typesafegetter",
- "version": "v1.3.2",
+ "version": "v1.3.3",
"source": {
"type": "git",
- "url": "https://github.com/PhpGt/TypeSafeGetter.git",
- "reference": "f760c05a37b1cc188dcbf800c5fdfab8a926b4b0"
+ "url": "https://github.com/phpgt/TypeSafeGetter.git",
+ "reference": "a0d339103828791989cbb81f760d252f3c2f8b8c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PhpGt/TypeSafeGetter/zipball/f760c05a37b1cc188dcbf800c5fdfab8a926b4b0",
- "reference": "f760c05a37b1cc188dcbf800c5fdfab8a926b4b0",
+ "url": "https://api.github.com/repos/phpgt/TypeSafeGetter/zipball/a0d339103828791989cbb81f760d252f3c2f8b8c",
+ "reference": "a0d339103828791989cbb81f760d252f3c2f8b8c",
"shasum": ""
},
"require": {
@@ -47,8 +47,8 @@
],
"description": "An interface for objects that expose type-safe getter methods.",
"support": {
- "issues": "https://github.com/PhpGt/TypeSafeGetter/issues",
- "source": "https://github.com/PhpGt/TypeSafeGetter/tree/v1.3.2"
+ "issues": "https://github.com/phpgt/TypeSafeGetter/issues",
+ "source": "https://github.com/phpgt/TypeSafeGetter/tree/v1.3.3"
},
"funding": [
{
@@ -56,7 +56,7 @@
"type": "github"
}
],
- "time": "2023-04-28T14:42:27+00:00"
+ "time": "2026-03-10T22:28:01+00:00"
}
],
"packages-dev": [
@@ -207,16 +207,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.13.3",
+ "version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "faed855a7b5f4d4637717c2b3863e277116beb36"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36",
- "reference": "faed855a7b5f4d4637717c2b3863e277116beb36",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@@ -255,7 +255,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@@ -263,20 +263,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-05T12:25:42+00:00"
+ "time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.6.0",
+ "version": "v5.7.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56"
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56",
- "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": ""
},
"require": {
@@ -295,7 +295,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
@@ -319,9 +319,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
},
- "time": "2025-07-27T20:03:57+00:00"
+ "time": "2025-12-06T11:56:16+00:00"
},
{
"name": "pdepend/pdepend",
@@ -589,16 +589,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.21",
- "source": {
- "type": "git",
- "url": "https://github.com/phpstan/phpstan.git",
- "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6"
- },
+ "version": "2.1.51",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6",
- "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
+ "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
"shasum": ""
},
"require": {
@@ -643,7 +638,7 @@
"type": "github"
}
],
- "time": "2025-07-28T19:35:08+00:00"
+ "time": "2026-04-21T18:22:01+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -968,16 +963,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "10.5.48",
+ "version": "10.5.63",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541"
+ "reference": "33198268dad71e926626b618f3ec3966661e4d90"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e0a2bc39f6fae7617989d690d76c48e6d2eb541",
- "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90",
+ "reference": "33198268dad71e926626b618f3ec3966661e4d90",
"shasum": ""
},
"require": {
@@ -987,7 +982,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.13.3",
+ "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@@ -998,13 +993,13 @@
"phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0",
- "sebastian/comparator": "^5.0.3",
+ "sebastian/comparator": "^5.0.5",
"sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0",
- "sebastian/exporter": "^5.1.2",
+ "sebastian/exporter": "^5.1.4",
"sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0",
- "sebastian/recursion-context": "^5.0.0",
+ "sebastian/recursion-context": "^5.0.1",
"sebastian/type": "^4.0.0",
"sebastian/version": "^4.0.1"
},
@@ -1049,7 +1044,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.48"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63"
},
"funding": [
{
@@ -1073,7 +1068,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-11T04:07:17+00:00"
+ "time": "2026-01-27T05:48:37+00:00"
},
{
"name": "psr/container",
@@ -1348,16 +1343,16 @@
},
{
"name": "sebastian/comparator",
- "version": "5.0.3",
+ "version": "5.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
+ "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
- "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
+ "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
"shasum": ""
},
"require": {
@@ -1413,15 +1408,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
}
],
- "time": "2024-10-18T14:56:07+00:00"
+ "time": "2026-01-24T09:25:16+00:00"
},
{
"name": "sebastian/complexity",
@@ -1614,16 +1621,16 @@
},
{
"name": "sebastian/exporter",
- "version": "5.1.2",
+ "version": "5.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "955288482d97c19a372d3f31006ab3f37da47adf"
+ "reference": "0735b90f4da94969541dac1da743446e276defa6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
- "reference": "955288482d97c19a372d3f31006ab3f37da47adf",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
+ "reference": "0735b90f4da94969541dac1da743446e276defa6",
"shasum": ""
},
"require": {
@@ -1632,7 +1639,7 @@
"sebastian/recursion-context": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -1680,15 +1687,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2024-03-02T07:17:12+00:00"
+ "time": "2025-09-24T06:09:11+00:00"
},
{
"name": "sebastian/global-state",
@@ -1924,23 +1943,23 @@
},
{
"name": "sebastian/recursion-context",
- "version": "5.0.0",
+ "version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "05909fb5bc7df4c52992396d0116aed689f93712"
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712",
- "reference": "05909fb5bc7df4c52992396d0116aed689f93712",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
@@ -1975,15 +1994,28 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0"
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T07:05:40+00:00"
+ "time": "2025-08-10T07:50:56+00:00"
},
{
"name": "sebastian/type",
@@ -2096,16 +2128,16 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.13.2",
+ "version": "3.13.5",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "5b5e3821314f947dd040c70f7992a64eac89025c"
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c",
- "reference": "5b5e3821314f947dd040c70f7992a64eac89025c",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
"shasum": ""
},
"require": {
@@ -2122,11 +2154,6 @@
"bin/phpcs"
],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- }
- },
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
@@ -2176,26 +2203,26 @@
"type": "thanks_dev"
}
],
- "time": "2025-06-17T22:17:01+00:00"
+ "time": "2025-11-04T16:30:35+00:00"
},
{
"name": "symfony/config",
- "version": "v7.3.2",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "faef36e271bbeb74a9d733be4b56419b157762e2"
+ "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/faef36e271bbeb74a9d733be4b56419b157762e2",
- "reference": "faef36e271bbeb74a9d733be4b56419b157762e2",
+ "url": "https://api.github.com/repos/symfony/config/zipball/2d19dde43fa2ff720b9a40763ace7226594f503b",
+ "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/filesystem": "^7.1",
+ "symfony/filesystem": "^7.1|^8.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
@@ -2203,11 +2230,11 @@
"symfony/service-contracts": "<2.5"
},
"require-dev": {
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/yaml": "^6.4|^7.0"
+ "symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -2235,7 +2262,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/config/tree/v7.3.2"
+ "source": "https://github.com/symfony/config/tree/v7.4.8"
},
"funding": [
{
@@ -2255,28 +2282,28 @@
"type": "tidelift"
}
],
- "time": "2025-07-26T13:55:06+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/dependency-injection",
- "version": "v7.3.2",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "6cd2a1a77e8a0676a26e8bcddf10acfe7b0ba352"
+ "reference": "f7025fd7b687c240426562f86ada06a93b1e771d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6cd2a1a77e8a0676a26e8bcddf10acfe7b0ba352",
- "reference": "6cd2a1a77e8a0676a26e8bcddf10acfe7b0ba352",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7025fd7b687c240426562f86ada06a93b1e771d",
+ "reference": "f7025fd7b687c240426562f86ada06a93b1e771d",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/service-contracts": "^3.5",
- "symfony/var-exporter": "^6.4.20|^7.2.5"
+ "symfony/service-contracts": "^3.6",
+ "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0"
},
"conflict": {
"ext-psr": "<1.1|>=2",
@@ -2289,9 +2316,9 @@
"symfony/service-implementation": "1.1|2.0|3.0"
},
"require-dev": {
- "symfony/config": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/yaml": "^6.4|^7.0"
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -2319,7 +2346,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dependency-injection/tree/v7.3.2"
+ "source": "https://github.com/symfony/dependency-injection/tree/v7.4.8"
},
"funding": [
{
@@ -2339,7 +2366,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-30T17:31:46+00:00"
+ "time": "2026-03-31T06:50:29+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -2410,16 +2437,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v7.3.2",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd"
+ "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd",
- "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5",
+ "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5",
"shasum": ""
},
"require": {
@@ -2428,7 +2455,7 @@
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "symfony/process": "^6.4|^7.0"
+ "symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -2456,7 +2483,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.3.2"
+ "source": "https://github.com/symfony/filesystem/tree/v7.4.8"
},
"funding": [
{
@@ -2476,20 +2503,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-07T08:17:47+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.32.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
- "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
@@ -2539,7 +2566,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
},
"funding": [
{
@@ -2550,25 +2577,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.36.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
@@ -2620,7 +2651,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0"
},
"funding": [
{
@@ -2631,25 +2662,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-12-23T08:48:59+00:00"
+ "time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v3.6.0",
+ "version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
"shasum": ""
},
"require": {
@@ -2703,7 +2738,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
},
"funding": [
{
@@ -2714,25 +2749,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-25T09:37:31+00:00"
+ "time": "2025-07-15T11:30:57+00:00"
},
{
"name": "symfony/var-exporter",
- "version": "v7.3.2",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
- "reference": "05b3e90654c097817325d6abd284f7938b05f467"
+ "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-exporter/zipball/05b3e90654c097817325d6abd284f7938b05f467",
- "reference": "05b3e90654c097817325d6abd284f7938b05f467",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/398907e89a2a56fe426f7955c6fa943ec0c77225",
+ "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225",
"shasum": ""
},
"require": {
@@ -2740,9 +2779,9 @@
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -2780,7 +2819,7 @@
"serialize"
],
"support": {
- "source": "https://github.com/symfony/var-exporter/tree/v7.3.2"
+ "source": "https://github.com/symfony/var-exporter/tree/v7.4.8"
},
"funding": [
{
@@ -2800,20 +2839,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:47:49+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "theseer/tokenizer",
- "version": "1.2.3",
+ "version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
@@ -2842,7 +2881,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@@ -2850,7 +2889,7 @@
"type": "github"
}
],
- "time": "2024-03-03T12:36:25+00:00"
+ "time": "2025-11-17T20:03:58+00:00"
}
],
"aliases": [],
@@ -2859,8 +2898,8 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=8.1"
+ "php": ">=8.2"
},
"platform-dev": {},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}
diff --git a/phpcs.xml b/phpcs.xml
index ad96b86..2d2aff4 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -19,7 +19,11 @@
-
+
+
+
+
+
diff --git a/phpmd.xml b/phpmd.xml
index c30e0b4..d125c4d 100644
--- a/phpmd.xml
+++ b/phpmd.xml
@@ -25,7 +25,16 @@
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/FileHandler.php b/src/FileHandler.php
index 34eb12c..71f82c1 100644
--- a/src/FileHandler.php
+++ b/src/FileHandler.php
@@ -1,5 +1,5 @@
session->get("queue.$name");
diff --git a/src/FlashMessage.php b/src/FlashMessage.php
index 7475eff..a3a3394 100644
--- a/src/FlashMessage.php
+++ b/src/FlashMessage.php
@@ -1,5 +1,5 @@
parseSavePath($savePath, $name);
+ $this->config = $config;
+ return $this->connect($config);
+ }
+
+ /** @param array{
+ * host:string,
+ * port:int,
+ * timeout:float,
+ * readTimeout:float,
+ * persistentId:?string,
+ * prefix:string,
+ * ttl:int,
+ * database:int,
+ * auth:array{string,string}|string|null,
+ * context:array{stream:array{verify_peer:bool,verify_peer_name:bool}}|null
+ * } $config
+ */
+ private function connect(array $config):bool {
+ $client = $this->createClient();
+
+ $connected = $client->connect(
+ $config["host"],
+ $config["port"],
+ $config["timeout"],
+ $config["persistentId"],
+ 0,
+ $config["readTimeout"],
+ $config["context"],
+ );
+
+ if(!$connected) {
+ return false;
+ }
+
+ if($config["auth"] !== null && !$client->auth($config["auth"])) {
+ return false;
+ }
+
+ if($config["database"] > 0 && !$client->select($config["database"])) {
+ return false;
+ }
+
+ $this->client = $client;
+ $this->prefix = $config["prefix"];
+ $this->ttl = $config["ttl"];
+ return true;
+ }
+
+ public function close():bool {
+ if(is_null($this->client)) {
+ return true;
+ }
+
+ try {
+ return $this->client->close();
+ }
+ catch(Throwable) {
+ return true;
+ }
+ finally {
+ $this->client = null;
+ }
+ }
+
+ public function read(string $sessionId):string {
+ $value = $this->retryOnce(
+ fn() => $this->requireClient()->get($this->getKey($sessionId))
+ );
+ return is_string($value) ? $value : "";
+ }
+
+ public function write(string $sessionId, string $sessionData):bool {
+ if($sessionData === self::EMPTY_PHP_ARRAY) {
+ return true;
+ }
+
+ $key = $this->getKey($sessionId);
+
+ if($this->ttl > 0) {
+ return $this->retryOnce(
+ fn() => $this->requireClient()->setEx($key, $this->ttl, $sessionData)
+ );
+ }
+
+ return $this->retryOnce(
+ fn() => $this->requireClient()->set($key, $sessionData)
+ );
+ }
+
+ public function destroy(string $id = ""):bool {
+ return $this->retryOnce(
+ fn() => $this->requireClient()->del($this->getKey($id))
+ ) >= 0;
+ }
+
+ // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
+ public function gc(int $maxLifeTime):int|false {
+ return 0;
+ }
+ // phpcs:enable
+
+ /**
+ * @return array{
+ * host:string,
+ * port:int,
+ * timeout:float,
+ * readTimeout:float,
+ * persistentId:?string,
+ * prefix:string,
+ * ttl:int,
+ * database:int,
+ * auth:array{string,string}|string|null,
+ * context:array{stream:array{verify_peer:bool,verify_peer_name:bool}}|null
+ * }
+ */
+ private function parseSavePath(string $savePath, string $name):array {
+ $parts = parse_url($savePath);
+ if($parts === false || !isset($parts["host"])) {
+ throw new RuntimeException("Invalid Redis save_path DSN.");
+ }
+
+ parse_str($parts["query"] ?? "", $query);
+
+ [$host, $context] = $this->parseHostAndContext($parts, $query);
+
+ return [
+ "host" => $host,
+ "port" => (int)($parts["port"] ?? self::DEFAULT_PORT),
+ "timeout" => (float)($query["timeout"] ?? 0),
+ "readTimeout" => (float)($query["read_timeout"] ?? 0),
+ "persistentId" => $this->parsePersistentId($query),
+ "prefix" => $this->parsePrefix($query, $name),
+ "ttl" => (int)($query["ttl"] ?? ini_get("session.gc_maxlifetime")),
+ "database" => $this->parseDatabase($parts),
+ "auth" => $this->parseAuth($parts),
+ "context" => $context,
+ ];
+ }
+
+ /**
+ * @param array $parts
+ * @param array $query
+ * @return array{
+ * 0:string,
+ * 1:array{stream:array{verify_peer:bool,verify_peer_name:bool}}|null
+ * }
+ */
+ private function parseHostAndContext(array $parts, array $query):array {
+ $scheme = strtolower($parts["scheme"] ?? "redis");
+ $host = $parts["host"];
+
+ if(!in_array($scheme, ["rediss", "tls"], true)) {
+ return [$host, null];
+ }
+
+ return [
+ "tls://$host",
+ [
+ "stream" => [
+ "verify_peer" => filter_var(
+ $query["verify_peer"] ?? true,
+ FILTER_VALIDATE_BOOL
+ ),
+ "verify_peer_name" => filter_var(
+ $query["verify_peer_name"] ?? true,
+ FILTER_VALIDATE_BOOL
+ ),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @param array $parts
+ * @return array{string,string}|string|null
+ */
+ private function parseAuth(array $parts):array|string|null {
+ if(isset($parts["user"]) && $parts["user"] !== "" && isset($parts["pass"])) {
+ return [rawurldecode($parts["user"]), rawurldecode($parts["pass"])];
+ }
+
+ if(isset($parts["pass"])) {
+ return rawurldecode($parts["pass"]);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array $query
+ */
+ private function parsePersistentId(array $query):?string {
+ if(
+ !isset($query["persistent"])
+ || !filter_var($query["persistent"], FILTER_VALIDATE_BOOL)
+ ) {
+ return null;
+ }
+
+ return is_string($query["persistent_id"] ?? null)
+ ? $query["persistent_id"]
+ : "phpgt-session";
+ }
+
+ /**
+ * @param array $query
+ */
+ private function parsePrefix(array $query, string $name):string {
+ return is_string($query["prefix"] ?? null)
+ ? $query["prefix"]
+ : $name . self::DEFAULT_PREFIX_SEPARATOR;
+ }
+
+ /**
+ * @param array $parts
+ */
+ private function parseDatabase(array $parts):int {
+ return isset($parts["path"])
+ ? (int)trim($parts["path"], "/")
+ : 0;
+ }
+
+ private function getKey(string $sessionId):string {
+ return $this->prefix . $sessionId;
+ }
+
+ protected function createClient():Redis {
+ if(!class_exists(Redis::class)) {
+ throw new RuntimeException(
+ "The phpredis extension is required to use GT\\Session\\RedisHandler."
+ );
+ }
+
+ return new Redis();
+ }
+
+ private function requireClient():Redis {
+ if(is_null($this->client)) {
+ throw new RuntimeException("RedisHandler::open() must be called before use.");
+ }
+
+ return $this->client;
+ }
+
+ private function reconnect():void {
+ if(is_null($this->config)) {
+ throw new RuntimeException("RedisHandler::open() must be called before reconnect.");
+ }
+
+ $this->close();
+ $this->connect($this->config);
+ }
+
+ /**
+ * @template T
+ * @param callable():T $callback
+ * @return T
+ */
+ private function retryOnce(callable $callback):mixed {
+ try {
+ return $callback();
+ }
+ catch(Throwable) {
+ $this->reconnect();
+ return $callback();
+ }
+ }
+}
diff --git a/src/Session.php b/src/Session.php
index 0d23eea..eab4aa7 100644
--- a/src/Session.php
+++ b/src/Session.php
@@ -1,5 +1,5 @@
id = $id;
- $sessionPath = $this->getAbsolutePath(
+ $sessionPath = $this->normaliseSavePath(
$config["save_path"] ?? self::DEFAULT_SESSION_PATH
);
$sessionName = $config["name"] ?? self::DEFAULT_SESSION_NAME;
@@ -81,7 +81,7 @@ public function kill():void {
public function getStore(
string $namespace,
bool $createIfNotExists = false
- ):?SessionStore {
+ ):?SessionStoreInterface {
return $this->store->getStore(
$namespace,
$createIfNotExists
@@ -113,7 +113,11 @@ public function getId():string {
return session_id() ?: "";
}
- protected function getAbsolutePath(string $path):string {
+ protected function normaliseSavePath(string $path):string {
+ if($this->isDsn($path)) {
+ return $path;
+ }
+
$path = str_replace(
["/", "\\"],
DIRECTORY_SEPARATOR,
@@ -130,6 +134,10 @@ protected function getAbsolutePath(string $path):string {
return $path;
}
+ protected function isDsn(string $path):bool {
+ return (bool)preg_match('/^[a-z][a-z0-9+.-]*:\/\//i', $path);
+ }
+
/** @SuppressWarnings("PHPMD.Superglobals") */
protected function createNewId():string {
if(($this->config["use_trans_sid"] ?? null)
@@ -166,12 +174,15 @@ public function write():bool {
* @param string $sessionPath
* @param string $sessionName
* @param array $config
+ * @SuppressWarnings("PHPMD.UnusedFormalParameter")
* @return void
*/
+ // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
private function attemptStart(
string $sessionPath,
string $sessionName,
array $config,
+ ?string $unusedContext = null,
):void {
$sessionOptions = $this->getSessionOptions(
$sessionPath,
@@ -232,14 +243,20 @@ private function tryStartSession(array $sessionOptions):void {
$success = false;
try {
- $success = session_start($sessionOptions);
+ if(session_status() !== PHP_SESSION_ACTIVE) {
+ $success = session_start($sessionOptions);
+ }
}
// PHPCS:ignore
catch(Throwable) {}
if(!$success) {
- session_destroy();
- session_regenerate_id(true);
+ if(session_status() === PHP_SESSION_ACTIVE) {
+ session_destroy();
+ }
+ if(session_id()) {
+ session_regenerate_id(true);
+ }
}
$startAttempts++;
}
diff --git a/src/SessionArrayWrapper.php b/src/SessionArrayWrapper.php
index a484668..ed2ab60 100644
--- a/src/SessionArrayWrapper.php
+++ b/src/SessionArrayWrapper.php
@@ -1,5 +1,5 @@
*/
diff --git a/src/SessionContainer.php b/src/SessionContainer.php
index aab32f8..68d121c 100644
--- a/src/SessionContainer.php
+++ b/src/SessionContainer.php
@@ -1,5 +1,5 @@
* @SuppressWarnings("PHPMD.TooManyPublicMethods")
*/
-class SessionStore
- extends ArrayIterator
- implements SessionContainer, TypeSafeGetter, Countable {
+class SessionStore extends ArrayIterator implements SessionStoreInterface {
use NullableTypeSafeGetter;
protected string $name;
diff --git a/src/SessionStoreFactory.php b/src/SessionStoreFactory.php
index aaf6da0..2bf6d5b 100644
--- a/src/SessionStoreFactory.php
+++ b/src/SessionStoreFactory.php
@@ -1,11 +1,11 @@
expects(self::once())
->method("get")
->with("queue.test")
@@ -28,7 +28,7 @@ public function testConsume_none():void {
$queue->expects(self::once())
->method("dequeue")
->willThrowException(new RuntimeException("Can't shift from an empty datastructure"));
- $sessionStore = self::createMock(SessionStore::class);
+ $sessionStore = self::createMock(SessionStoreInterface::class);
$sessionStore->expects(self::once())
->method("get")
->with("queue.test")
@@ -47,7 +47,7 @@ public function testConsume():void {
->method("dequeue")
->willReturnOnConsecutiveCalls($fm1, $fm2);
- $sessionStore = self::createMock(SessionStore::class);
+ $sessionStore = self::createMock(SessionStoreInterface::class);
$sessionStore->expects(self::exactly(3))
->method("get")
->with("queue.test")
diff --git a/test/phpunit/Helper/DataProvider/ConfigProvider.php b/test/phpunit/Helper/DataProvider/ConfigProvider.php
index 576e42a..1f6d440 100644
--- a/test/phpunit/Helper/DataProvider/ConfigProvider.php
+++ b/test/phpunit/Helper/DataProvider/ConfigProvider.php
@@ -1,7 +1,7 @@
client;
+ }
+ };
+
+ $sut->open(
+ "redis://default:secret@example.internal:25061/2?prefix=prod:session:&ttl=1800&timeout=1.5&read_timeout=2.5&persistent=1&persistent_id=pool-a",
+ "GT",
+ );
+
+ self::assertSame("example.internal", $client->connectParameters["host"]);
+ self::assertSame(25061, $client->connectParameters["port"]);
+ self::assertSame(1.5, $client->connectParameters["timeout"]);
+ self::assertSame(2.5, $client->connectParameters["readTimeout"]);
+ self::assertSame("pool-a", $client->connectParameters["persistentId"]);
+ self::assertSame([["default", "secret"]], $client->authCalls);
+ self::assertSame([2], $client->selectCalls);
+
+ $sut->write("abc123", "payload");
+ self::assertSame("payload", $sut->read("abc123"));
+ self::assertSame(
+ [
+ "key" => "prod:session:abc123",
+ "ttl" => 1800,
+ "value" => "payload",
+ ],
+ $client->setExCalls[0],
+ );
+ }
+
+ public function testOpenParsesTlsDsn():void {
+ $client = new TestRedisClient();
+ $sut = new class($client) extends RedisHandler {
+ public function __construct(private readonly TestRedisClient $client) {}
+
+ protected function createClient():Redis {
+ /** @phpstan-ignore-next-line */
+ return $this->client;
+ }
+ };
+
+ $sut->open(
+ "rediss://:secret@example.internal?verify_peer=0&verify_peer_name=0",
+ "GT",
+ );
+ $sut->write("abc123", "payload");
+
+ self::assertSame("tls://example.internal", $client->connectParameters["host"]);
+ self::assertSame(
+ [
+ "stream" => [
+ "verify_peer" => false,
+ "verify_peer_name" => false,
+ ],
+ ],
+ $client->connectParameters["context"],
+ );
+ self::assertSame(["secret"], $client->authCalls);
+ self::assertSame("payload", $client->data["GT:abc123"]);
+ }
+
+ public function testDestroyAndClose():void {
+ $client = new TestRedisClient();
+ $sut = new class($client) extends RedisHandler {
+ public function __construct(private readonly TestRedisClient $client) {}
+
+ protected function createClient():Redis {
+ /** @phpstan-ignore-next-line */
+ return $this->client;
+ }
+ };
+ $sut->open("redis://cache.internal", "GT");
+ $sut->write("abc123", "payload");
+
+ self::assertTrue($sut->destroy("abc123"));
+ self::assertSame("", $sut->read("abc123"));
+ self::assertTrue($sut->close());
+ self::assertTrue($client->closed);
+ }
+
+ public function testEmptyNativePhpSessionWriteDoesNotOverwriteStoredSession():void {
+ $client = new TestRedisClient();
+ $sut = new class($client) extends RedisHandler {
+ public function __construct(private readonly TestRedisClient $client) {}
+
+ protected function createClient():Redis {
+ /** @phpstan-ignore-next-line */
+ return $this->client;
+ }
+ };
+ $sut->open("redis://cache.internal", "GT");
+ $sut->write("abc123", "stored-session");
+ $sut->write("abc123", "a:0:{}");
+
+ self::assertSame("stored-session", $sut->read("abc123"));
+ }
+
+ public function testCommandRetriesAfterDisconnectedClient():void {
+ $client = new TestRedisClient();
+ $sut = new class($client) extends RedisHandler {
+ public function __construct(private readonly TestRedisClient $client) {}
+
+ protected function createClient():Redis {
+ /** @phpstan-ignore-next-line */
+ return $this->client;
+ }
+ };
+ $sut->open("redis://cache.internal", "GT");
+ $client->failNextSetEx = true;
+
+ self::assertTrue($sut->write("abc123", "payload"));
+ self::assertSame("payload", $sut->read("abc123"));
+ self::assertSame(2, $client->connectCount);
+ }
+}
+
+class TestRedisClient extends Redis {
+ /** @var array */
+ public array $connectParameters = [];
+ /** @var array */
+ public array $setExCalls = [];
+ /** @var array */
+ public array $setCalls = [];
+ /** @var array */
+ public array $authCalls = [];
+ /** @var array */
+ public array $selectCalls = [];
+ /** @var array */
+ public array $data = [];
+ public int $deleted = 0;
+ public int $connectCount = 0;
+ public bool $failNextSetEx = false;
+ public bool $closed = false;
+
+ /**
+ * @param array{auth?:array{0:string|false|null,1?:string},stream?:array}|null $context
+ */
+ public function connect(
+ string $host,
+ int $port = 6379,
+ float $timeout = 0,
+ ?string $persistent_id = null,
+ int $retry_interval = 0,
+ float $read_timeout = 0,
+ ?array $context = null,
+ ):bool {
+ $this->connectCount++;
+ $this->connectParameters = [
+ "host" => $host,
+ "port" => $port,
+ "timeout" => $timeout,
+ "persistentId" => $persistent_id,
+ "retryInterval" => $retry_interval,
+ "readTimeout" => $read_timeout,
+ "context" => $context,
+ ];
+ return true;
+ }
+
+ /**
+ * @param array{string,string}|string $credentials
+ */
+ public function auth(mixed $credentials):Redis|bool {
+ $this->authCalls []= $credentials;
+ return true;
+ }
+
+ public function select(int $database):Redis|bool {
+ $this->selectCalls []= $database;
+ return true;
+ }
+
+ public function get(string $key):string|false {
+ return $this->data[$key] ?? false;
+ }
+
+ public function set(string $key, mixed $value, mixed $options = null):Redis|string|bool {
+ $this->setCalls []= $key;
+ $this->data[$key] = (string)$value;
+ return true;
+ }
+
+ public function setEx(string $key, int $ttl, mixed $value) {
+ if($this->failNextSetEx) {
+ $this->failNextSetEx = false;
+ throw new \RedisException("Redis server went away");
+ }
+
+ $this->setExCalls []= [
+ "key" => $key,
+ "ttl" => $ttl,
+ "value" => (string)$value,
+ ];
+ $this->data[$key] = (string)$value;
+ return true;
+ }
+
+ public function del(array|string $key, string ...$otherKeys):Redis|int|false {
+ if(is_array($key)) {
+ $key = reset($key);
+ }
+
+ unset($this->data[$key]);
+ return ++$this->deleted;
+ }
+
+ public function close():bool {
+ $this->closed = true;
+ return true;
+ }
+}
diff --git a/test/phpunit/SessionArrayWrapperTest.php b/test/phpunit/SessionArrayWrapperTest.php
index e74aff5..5ca7b31 100644
--- a/test/phpunit/SessionArrayWrapperTest.php
+++ b/test/phpunit/SessionArrayWrapperTest.php
@@ -1,7 +1,7 @@
createMock(Session::class);
+ $sut = new SessionStore("test", $session);
+ self::assertInstanceOf(SessionStoreInterface::class, $sut);
+ }
}
diff --git a/test/phpunit/SessionTest.php b/test/phpunit/SessionTest.php
index 9d061e5..bb4ec01 100644
--- a/test/phpunit/SessionTest.php
+++ b/test/phpunit/SessionTest.php
@@ -1,13 +1,13 @@
$savePath,
+ ]);
+
+ $sessionStartParameter = FunctionMocker::$mockCalls["session_start"][0][0];
+ self::assertSame($savePath, $sessionStartParameter["save_path"]);
+ }
+
public function testWriteSessionDataCalled() {
$handler = self::createMock(Handler::class);
$handler->expects($this->exactly(2))