Skip to content

Commit f85255e

Browse files
authored
feat: added Redis-ready session scaling
1 parent 002c695 commit f85255e

15 files changed

Lines changed: 320 additions & 11 deletions

File tree

.docker/apache/Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#
2-
# This image uses a php:8.4-apache base image and do not have any phpMyFAQ code with it.
2+
# This image uses a php:8.5-apache base image and do not have any phpMyFAQ code with it.
33
# It's for development only, it's meant to be run with docker-compose
44
#
55

66
#####################################
77
#=== Unique stage without payload ===
88
#####################################
9-
FROM php:8.4-apache
9+
FROM php:8.5-apache
1010

1111
#=== Install gd PHP dependencie ===
1212
RUN set -x \
@@ -61,6 +61,10 @@ RUN set -ex \
6161
RUN pecl install xdebug-3.5.0 \
6262
&& docker-php-ext-enable xdebug
6363

64+
#=== Install redis PHP extension ===
65+
RUN pecl install redis-6.3.0 \
66+
&& docker-php-ext-enable redis
67+
6468
#=== php default ===
6569
ENV PMF_TIMEZONE="Europe/Berlin" \
6670
PMF_ENABLE_UPLOADS=On \

.docker/frankenphp/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
4141
RUN pecl install xdebug \
4242
&& docker-php-ext-enable xdebug
4343

44+
#=== Install redis PHP extension ===
45+
RUN pecl install redis \
46+
&& docker-php-ext-enable redis
47+
4448
#=== Environment variables ===
4549
ENV PMF_TIMEZONE="Europe/Berlin" \
4650
PMF_ENABLE_UPLOADS=On \

.docker/php-fpm/Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#
2-
# This image uses a php:8.4-fpm base image and does not have any phpMyFAQ code with it.
2+
# This image uses a php:8.5-fpm base image and does not have any phpMyFAQ code with it.
33
# It's for development only, it's meant to be run with docker-compose
44
#
55

66
#####################################
77
#=== Unique stage without payload ===
88
#####################################
9-
FROM php:8.4-fpm
9+
FROM php:8.5-fpm
1010

1111
#=== Install gd PHP dependencies ===
1212
RUN set -x \
@@ -61,6 +61,10 @@ RUN set -ex \
6161
RUN pecl install xdebug-3.5.0 \
6262
&& docker-php-ext-enable xdebug
6363

64+
#=== Install redis PHP extension ===
65+
RUN pecl install redis \
66+
&& docker-php-ext-enable redis
67+
6468
#=== php default ===
6569
ENV PMF_TIMEZONE="Europe/Berlin" \
6670
PMF_ENABLE_UPLOADS=On \

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ and can be run on almost any web hosting provider or deployed in the cloud using
3333

3434
## Requirements
3535

36-
phpMyFAQ is only supported on PHP 8.3 and up, you need a database as well. Supported databases are MySQL, MariaDB,
36+
phpMyFAQ is only supported on PHP 8.4+, you need a database as well. Supported databases are MySQL, MariaDB,
3737
Percona Server, PostgreSQL, Microsoft SQL Server, and SQLite3. If you want to use Elasticsearch or Opensearch as the
3838
main search engine, you need Elasticsearch v6+ or OpenSearch v1+. Check our detailed requirements on
3939
[phpmyfaq.de](https://www.phpmyfaq.de/requirements) for more information.
@@ -75,17 +75,18 @@ _Running using named volumes:_
7575
- **sqlserver**: image with Microsoft SQL Server for Linux
7676
- **elasticsearch**: Open Source Software image (it means it does not have XPack installed)
7777
- **opensearch**: OpenSearch image (it means it does not have XPack installed)
78+
- **redis**: image with a Redis database
7879

79-
_Running apache web server with PHP 8.4 support:_
80+
_Running apache web server with PHP 8.5 support:_
8081

8182
- **apache**: mounts the `phpmyfaq` folder in place of `/var/www/html`.
8283

83-
_Running nginx web server with PHP 8.4 support:_
84+
_Running nginx web server with PHP 8.5 support:_
8485

8586
- **nginx**: mounts the `phpmyfaq` folder in place of `/var/www/html`.
86-
- **php-fpm**: PHP-FPM image with PHP 8.4 support
87+
- **php-fpm**: PHP-FPM image with PHP 8.5 support
8788

88-
_Running FrankenPHP web server with PHP 8.4 support:_
89+
_Running FrankenPHP web server with PHP 8.5 support:_
8990

9091
- **frankenphp**: mounts the `phpmyfaq` folder in place of `/var/www/html`.
9192

docker-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ services:
2828
volumes:
2929
- ./volumes/postgres:/var/lib/postgresql/data
3030

31+
redis:
32+
image: redis:7-alpine
33+
restart: always
34+
command: redis-server --appendonly yes
35+
ports:
36+
- '6379:6379'
37+
volumes:
38+
- ./volumes/redis:/data
39+
3140
#sqlserver:
3241
# image: mcr.microsoft.com/mssql/server:2022-latest
3342
# ports:
@@ -60,6 +69,7 @@ services:
6069
links:
6170
- mariadb:db
6271
- postgres
72+
- redis
6373
- elasticsearch
6474
- opensearch
6575
ports:
@@ -107,6 +117,7 @@ services:
107117
links:
108118
- mariadb:db
109119
- postgres
120+
- redis
110121
- elasticsearch
111122
- opensearch
112123
volumes:
@@ -136,6 +147,7 @@ services:
136147
links:
137148
- mariadb:db
138149
- postgres
150+
- redis
139151
- elasticsearch
140152
- opensearch
141153
ports:

docs/installation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ The PDO extension is the preferred way to connect to your database server.
6767
- [Elasticsearch](https://www.elastic.co/products/elasticsearch) 7.x or 8.x
6868
- [OpenSearch](https://opensearch.org/) 2.x
6969

70+
### Optional In-Memory Data Store
71+
72+
- [Redis](https://redis.io/) 8.x or later
73+
7074
### Additional requirements
7175

7276
- correctly set: access permissions, owner, group

phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
namespace phpMyFAQ\Bootstrap;
2121

22+
use phpMyFAQ\Configuration;
23+
use phpMyFAQ\Session\RedisSessionHandler;
24+
use RuntimeException;
25+
2226
class PhpConfigurator
2327
{
2428
/**
@@ -53,7 +57,7 @@ public static function registerErrorHandlers(): void
5357
/**
5458
* Configures secure session settings if no session is active yet.
5559
*/
56-
public static function configureSession(): void
60+
public static function configureSession(?Configuration $configuration = null): void
5761
{
5862
if (session_status() !== PHP_SESSION_ACTIVE) {
5963
ini_set('session.use_only_cookies', value: '1');
@@ -65,6 +69,23 @@ public static function configureSession(): void
6569
if (defined('PMF_SESSION_SAVE_PATH') && PMF_SESSION_SAVE_PATH !== '') {
6670
ini_set('session.save_path', value: PMF_SESSION_SAVE_PATH);
6771
}
72+
73+
$sessionHandler = strtolower((string) ($configuration?->get('session.handler') ?? 'files'));
74+
$redisDsn = trim((string) ($configuration?->get('session.redisDsn') ?? ''));
75+
76+
switch ($sessionHandler) {
77+
case 'files':
78+
ini_set('session.save_handler', value: 'files');
79+
break;
80+
case 'redis':
81+
RedisSessionHandler::configure($redisDsn);
82+
break;
83+
default:
84+
throw new RuntimeException(sprintf(
85+
'Unsupported session handler "%s". Allowed values: files, redis.',
86+
$sessionHandler,
87+
));
88+
}
6889
}
6990
}
7091
}

phpmyfaq/src/phpMyFAQ/Bootstrapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function run(): self
8585
$this->connectDatabase($databaseFile);
8686

8787
// 12. Session configuration
88-
PhpConfigurator::configureSession();
88+
PhpConfigurator::configureSession($this->faqConfig);
8989

9090
// 13. LDAP
9191
$this->configureLdap();
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/**
4+
* Configures native PHP Redis-backed sessions with connectivity checks.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
12+
* @copyright 2026 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2026-02-14
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Session;
21+
22+
use RuntimeException;
23+
24+
class RedisSessionHandler
25+
{
26+
public const string DEFAULT_DSN = 'tcp://redis:6379?database=0';
27+
28+
public static function configure(string $dsn = '', bool $validate = false): void
29+
{
30+
if (!extension_loaded('redis')) {
31+
throw new RuntimeException('Redis session handler requires the PHP redis extension (ext-redis).');
32+
}
33+
34+
$redisDsn = trim($dsn) !== '' ? trim($dsn) : self::DEFAULT_DSN;
35+
36+
if ($validate) {
37+
self::validateConnection($redisDsn);
38+
}
39+
40+
ini_set('session.save_handler', value: 'redis');
41+
ini_set('session.save_path', value: $redisDsn);
42+
}
43+
44+
public static function validateConnection(string $dsn, float $timeoutSeconds = 1.0): void
45+
{
46+
[$socketTarget, $displayTarget] = self::buildSocketTarget($dsn);
47+
48+
$errno = 0;
49+
$errorString = '';
50+
$connection = @stream_socket_client(
51+
$socketTarget,
52+
$errno,
53+
$errorString,
54+
$timeoutSeconds,
55+
STREAM_CLIENT_CONNECT,
56+
);
57+
58+
if ($connection === false) {
59+
throw new RuntimeException(sprintf(
60+
'Redis session handler is configured but unreachable (%s): %s',
61+
$displayTarget,
62+
$errorString !== '' ? $errorString : 'connection failed',
63+
));
64+
}
65+
66+
fclose($connection);
67+
}
68+
69+
/**
70+
* @return array{0: string, 1: string}
71+
*/
72+
private static function buildSocketTarget(string $dsn): array
73+
{
74+
$parsedUrl = parse_url($dsn);
75+
if ($parsedUrl === false || !isset($parsedUrl['scheme'])) {
76+
throw new RuntimeException('Invalid Redis DSN for sessions.');
77+
}
78+
79+
$scheme = strtolower((string) $parsedUrl['scheme']);
80+
if ($scheme === 'redis' || $scheme === 'tcp') {
81+
$host = $parsedUrl['host'] ?? '127.0.0.1';
82+
$port = (int) ($parsedUrl['port'] ?? 6379);
83+
return [sprintf('tcp://%s:%d', $host, $port), sprintf('%s:%d', $host, $port)];
84+
}
85+
86+
if ($scheme === 'unix') {
87+
$path = $parsedUrl['path'] ?? '';
88+
if ($path === '') {
89+
throw new RuntimeException('Invalid Redis unix socket DSN for sessions.');
90+
}
91+
92+
return ['unix://' . $path, $path];
93+
}
94+
95+
throw new RuntimeException(sprintf('Unsupported Redis DSN scheme "%s" for sessions.', $scheme));
96+
}
97+
}

phpmyfaq/src/phpMyFAQ/Setup/Installation/DefaultDataSeeder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ private static function buildDefaultConfig(): array
285285
'api.onlyPublicQuestions' => 'true',
286286
'api.ignoreOrphanedFaqs' => 'true',
287287
'queue.transport' => 'database',
288+
'session.handler' => 'files',
289+
'session.redisDsn' => 'tcp://redis:6379?database=0',
288290
'upgrade.dateLastChecked' => '',
289291
'upgrade.lastDownloadedPackage' => '',
290292
'upgrade.onlineUpdateEnabled' => 'false',

0 commit comments

Comments
 (0)