From 7d18f02cee50e6f3605bc8198ca437b0baa30572 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Sat, 14 Feb 2026 14:46:38 +0100 Subject: [PATCH 1/3] feat: added Redis-ready session scaling --- .docker/apache/Dockerfile | 8 +- .docker/frankenphp/Dockerfile | 4 + .docker/php-fpm/Dockerfile | 8 +- README.md | 1 + docker-compose.yml | 12 ++ .../phpMyFAQ/Bootstrap/PhpConfigurator.php | 24 +++- phpmyfaq/src/phpMyFAQ/Bootstrapper.php | 2 +- .../phpMyFAQ/Session/RedisSessionHandler.php | 94 +++++++++++++ .../Setup/Installation/DefaultDataSeeder.php | 2 + .../Migration/Versions/Migration420Alpha.php | 2 + .../Bootstrap/PhpConfiguratorTest.php | 127 ++++++++++++++++++ .../Session/RedisSessionHandlerTest.php | 27 ++++ .../Installation/DefaultDataSeederTest.php | 2 + .../Versions/Migration420AlphaTest.php | 2 + 14 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php create mode 100644 tests/phpMyFAQ/Session/RedisSessionHandlerTest.php diff --git a/.docker/apache/Dockerfile b/.docker/apache/Dockerfile index 7615f69b8e..dcaad04d36 100644 --- a/.docker/apache/Dockerfile +++ b/.docker/apache/Dockerfile @@ -1,12 +1,12 @@ # -# This image uses a php:8.4-apache base image and do not have any phpMyFAQ code with it. +# This image uses a php:8.5-apache base image and do not have any phpMyFAQ code with it. # It's for development only, it's meant to be run with docker-compose # ##################################### #=== Unique stage without payload === ##################################### -FROM php:8.4-apache +FROM php:8.5-apache #=== Install gd PHP dependencie === RUN set -x \ @@ -61,6 +61,10 @@ RUN set -ex \ RUN pecl install xdebug-3.5.0 \ && docker-php-ext-enable xdebug +#=== Install redis PHP extension === +RUN pecl install redis \ + && docker-php-ext-enable redis + #=== php default === ENV PMF_TIMEZONE="Europe/Berlin" \ PMF_ENABLE_UPLOADS=On \ diff --git a/.docker/frankenphp/Dockerfile b/.docker/frankenphp/Dockerfile index 4600d1661c..7da9e91517 100644 --- a/.docker/frankenphp/Dockerfile +++ b/.docker/frankenphp/Dockerfile @@ -41,6 +41,10 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ RUN pecl install xdebug \ && docker-php-ext-enable xdebug +#=== Install redis PHP extension === +RUN pecl install redis \ + && docker-php-ext-enable redis + #=== Environment variables === ENV PMF_TIMEZONE="Europe/Berlin" \ PMF_ENABLE_UPLOADS=On \ diff --git a/.docker/php-fpm/Dockerfile b/.docker/php-fpm/Dockerfile index 6eed33c2f4..1a64879962 100644 --- a/.docker/php-fpm/Dockerfile +++ b/.docker/php-fpm/Dockerfile @@ -1,12 +1,12 @@ # -# This image uses a php:8.4-fpm base image and does not have any phpMyFAQ code with it. +# This image uses a php:8.5-fpm base image and does not have any phpMyFAQ code with it. # It's for development only, it's meant to be run with docker-compose # ##################################### #=== Unique stage without payload === ##################################### -FROM php:8.4-fpm +FROM php:8.5-fpm #=== Install gd PHP dependencies === RUN set -x \ @@ -61,6 +61,10 @@ RUN set -ex \ RUN pecl install xdebug-3.5.0 \ && docker-php-ext-enable xdebug +#=== Install redis PHP extension === +RUN pecl install redis \ + && docker-php-ext-enable redis + #=== php default === ENV PMF_TIMEZONE="Europe/Berlin" \ PMF_ENABLE_UPLOADS=On \ diff --git a/README.md b/README.md index 29f9ff3ea8..78a71050ea 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ _Running using named volumes:_ - **sqlserver**: image with Microsoft SQL Server for Linux - **elasticsearch**: Open Source Software image (it means it does not have XPack installed) - **opensearch**: OpenSearch image (it means it does not have XPack installed) +- **redis**: image with a Redis database _Running apache web server with PHP 8.4 support:_ diff --git a/docker-compose.yml b/docker-compose.yml index 53b5cee1a3..4abeb2c225 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,15 @@ services: volumes: - ./volumes/postgres:/var/lib/postgresql/data + redis: + image: redis:7-alpine + restart: always + command: redis-server --appendonly yes + ports: + - '6379:6379' + volumes: + - ./volumes/redis:/data + #sqlserver: # image: mcr.microsoft.com/mssql/server:2022-latest # ports: @@ -60,6 +69,7 @@ services: links: - mariadb:db - postgres + - redis - elasticsearch - opensearch ports: @@ -107,6 +117,7 @@ services: links: - mariadb:db - postgres + - redis - elasticsearch - opensearch volumes: @@ -136,6 +147,7 @@ services: links: - mariadb:db - postgres + - redis - elasticsearch - opensearch ports: diff --git a/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php b/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php index 509f906150..44b4d207a3 100644 --- a/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php +++ b/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php @@ -19,6 +19,10 @@ namespace phpMyFAQ\Bootstrap; +use phpMyFAQ\Configuration; +use phpMyFAQ\Session\RedisSessionHandler; +use RuntimeException; + class PhpConfigurator { /** @@ -53,7 +57,7 @@ public static function registerErrorHandlers(): void /** * Configures secure session settings if no session is active yet. */ - public static function configureSession(): void + public static function configureSession(?Configuration $configuration = null): void { if (session_status() !== PHP_SESSION_ACTIVE) { ini_set('session.use_only_cookies', value: '1'); @@ -65,6 +69,24 @@ public static function configureSession(): void if (defined('PMF_SESSION_SAVE_PATH') && PMF_SESSION_SAVE_PATH !== '') { ini_set('session.save_path', value: PMF_SESSION_SAVE_PATH); } + + $sessionHandler = strtolower((string) ($configuration?->get('session.handler') ?? 'files')); + $redisDsn = trim((string) ($configuration?->get('session.redisDsn') ?? '')); + + switch ($sessionHandler) { + case 'files': + case 'database': + ini_set('session.save_handler', value: 'files'); + break; + case 'redis': + RedisSessionHandler::configure($redisDsn); + break; + default: + throw new RuntimeException(sprintf( + 'Unsupported session handler "%s". Allowed values: files, database, redis.', + $sessionHandler, + )); + } } } } diff --git a/phpmyfaq/src/phpMyFAQ/Bootstrapper.php b/phpmyfaq/src/phpMyFAQ/Bootstrapper.php index ec49b54f92..a4ae6ecc11 100644 --- a/phpmyfaq/src/phpMyFAQ/Bootstrapper.php +++ b/phpmyfaq/src/phpMyFAQ/Bootstrapper.php @@ -85,7 +85,7 @@ public function run(): self $this->connectDatabase($databaseFile); // 12. Session configuration - PhpConfigurator::configureSession(); + PhpConfigurator::configureSession($this->faqConfig); // 13. LDAP $this->configureLdap(); diff --git a/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php b/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php new file mode 100644 index 0000000000..4a71039612 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php @@ -0,0 +1,94 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-02-14 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Session; + +use RuntimeException; + +class RedisSessionHandler +{ + public const string DEFAULT_DSN = 'tcp://redis:6379?database=0'; + + public static function configure(string $dsn = ''): void + { + if (!extension_loaded('redis')) { + throw new RuntimeException('Redis session handler requires the PHP redis extension (ext-redis).'); + } + + $redisDsn = trim($dsn) !== '' ? trim($dsn) : self::DEFAULT_DSN; + self::validateConnection($redisDsn); + + ini_set('session.save_handler', value: 'redis'); + ini_set('session.save_path', value: $redisDsn); + } + + public static function validateConnection(string $dsn, float $timeoutSeconds = 1.0): void + { + [$socketTarget, $displayTarget] = self::buildSocketTarget($dsn); + + $errno = 0; + $errorString = ''; + $connection = @stream_socket_client( + $socketTarget, + $errno, + $errorString, + $timeoutSeconds, + STREAM_CLIENT_CONNECT, + ); + + if ($connection === false) { + throw new RuntimeException(sprintf( + 'Redis session handler is configured but unreachable (%s): %s', + $displayTarget, + $errorString !== '' ? $errorString : 'connection failed', + )); + } + + fclose($connection); + } + + /** + * @return array{0: string, 1: string} + */ + private static function buildSocketTarget(string $dsn): array + { + $parsedUrl = parse_url($dsn); + if ($parsedUrl === false || !isset($parsedUrl['scheme'])) { + throw new RuntimeException('Invalid Redis DSN for sessions.'); + } + + $scheme = strtolower((string) $parsedUrl['scheme']); + if ($scheme === 'redis' || $scheme === 'tcp') { + $host = $parsedUrl['host'] ?? '127.0.0.1'; + $port = (int) ($parsedUrl['port'] ?? 6379); + return [sprintf('tcp://%s:%d', $host, $port), sprintf('%s:%d', $host, $port)]; + } + + if ($scheme === 'unix') { + $path = $parsedUrl['path'] ?? ''; + if ($path === '') { + throw new RuntimeException('Invalid Redis unix socket DSN for sessions.'); + } + + return ['unix://' . $path, $path]; + } + + throw new RuntimeException(sprintf('Unsupported Redis DSN scheme "%s" for sessions.', $scheme)); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Installation/DefaultDataSeeder.php b/phpmyfaq/src/phpMyFAQ/Setup/Installation/DefaultDataSeeder.php index 1b9e59b419..02e3a6ad06 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Installation/DefaultDataSeeder.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Installation/DefaultDataSeeder.php @@ -285,6 +285,8 @@ private static function buildDefaultConfig(): array 'api.onlyPublicQuestions' => 'true', 'api.ignoreOrphanedFaqs' => 'true', 'queue.transport' => 'database', + 'session.handler' => 'files', + 'session.redisDsn' => 'tcp://redis:6379?database=0', 'upgrade.dateLastChecked' => '', 'upgrade.lastDownloadedPackage' => '', 'upgrade.onlineUpdateEnabled' => 'false', diff --git a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php index 73f44efd91..9ff611481b 100644 --- a/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php +++ b/phpmyfaq/src/phpMyFAQ/Setup/Migration/Versions/Migration420Alpha.php @@ -210,6 +210,8 @@ public function up(OperationRecorder $recorder): void $recorder->addConfig('api.rateLimit.requests', '100'); $recorder->addConfig('api.rateLimit.interval', '3600'); $recorder->addConfig('queue.transport', 'database'); + $recorder->addConfig('session.handler', 'files'); + $recorder->addConfig('session.redisDsn', 'tcp://redis:6379?database=0'); $recorder->addConfig('mail.useQueue', 'true'); $recorder->addConfig('mail.provider', 'smtp'); $recorder->addConfig('mail.sendgridApiKey', ''); diff --git a/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php b/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php index a08336de8e..5f13ecd0da 100644 --- a/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php +++ b/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php @@ -2,12 +2,57 @@ namespace phpMyFAQ\Bootstrap; +use phpMyFAQ\Configuration; +use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use RuntimeException; +#[AllowMockObjectsWithoutExpectations] #[CoversClass(PhpConfigurator::class)] class PhpConfiguratorTest extends TestCase { + /** @var array */ + private array $iniBackup = []; + + protected function setUp(): void + { + parent::setUp(); + + if (session_status() === PHP_SESSION_ACTIVE) { + session_write_close(); + } + + $this->iniBackup = [ + 'session.save_handler' => ini_get('session.save_handler'), + 'session.save_path' => ini_get('session.save_path'), + 'session.use_only_cookies' => ini_get('session.use_only_cookies'), + 'session.use_trans_sid' => ini_get('session.use_trans_sid'), + 'session.cookie_samesite' => ini_get('session.cookie_samesite'), + 'session.cookie_httponly' => ini_get('session.cookie_httponly'), + 'session.cookie_secure' => ini_get('session.cookie_secure'), + ]; + } + + protected function tearDown(): void + { + if (session_status() === PHP_SESSION_ACTIVE) { + session_write_close(); + } + + foreach ($this->iniBackup as $name => $value) { + if ($name === 'session.save_handler') { + continue; + } + + if ($value !== false) { + ini_set($name, (string) $value); + } + } + + parent::tearDown(); + } + public function testFixIncludePathEnsuresDotIsPresent(): void { PhpConfigurator::fixIncludePath(); @@ -23,4 +68,86 @@ public function testConfigurePcreSetsLimits(): void $this->assertEquals('100000000', ini_get('pcre.backtrack_limit')); $this->assertEquals('100000000', ini_get('pcre.recursion_limit')); } + + public function testConfigureSessionDefaultsToFilesHandler(): void + { + $configuration = $this->createMock(Configuration::class); + $configuration + ->method('get') + ->willReturnMap([ + ['session.handler', 'files'], + ['session.redisDsn', ''], + ]); + + PhpConfigurator::configureSession($configuration); + + $this->assertEquals('files', ini_get('session.save_handler')); + } + + public function testConfigureSessionUsesFilesForDatabaseHandlerInLiteMode(): void + { + $configuration = $this->createMock(Configuration::class); + $configuration + ->method('get') + ->willReturnMap([ + ['session.handler', 'database'], + ['session.redisDsn', ''], + ]); + + PhpConfigurator::configureSession($configuration); + + $this->assertEquals('files', ini_get('session.save_handler')); + } + + public function testConfigureSessionThrowsForUnsupportedHandler(): void + { + $configuration = $this->createMock(Configuration::class); + $configuration + ->method('get') + ->willReturnMap([ + ['session.handler', 'invalid'], + ['session.redisDsn', ''], + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unsupported session handler'); + + PhpConfigurator::configureSession($configuration); + } + + public function testConfigureSessionPersistsDataWithFilesHandler(): void + { + $sessionDirectory = sys_get_temp_dir() . '/pmf-session-test-' . uniqid('', true); + mkdir($sessionDirectory, 0777, true); + ini_set('session.save_path', $sessionDirectory); + + $configuration = $this->createMock(Configuration::class); + $configuration + ->method('get') + ->willReturnMap([ + ['session.handler', 'files'], + ['session.redisDsn', ''], + ]); + + PhpConfigurator::configureSession($configuration); + + session_id(''); + session_start(); + $_SESSION['phase7'] = 'ok'; + $sessionId = session_id(); + session_write_close(); + + session_id($sessionId); + session_start(); + $value = $_SESSION['phase7'] ?? null; + session_write_close(); + + $this->assertSame('ok', $value); + + $sessionFile = $sessionDirectory . '/sess_' . $sessionId; + if (file_exists($sessionFile)) { + unlink($sessionFile); + } + rmdir($sessionDirectory); + } } diff --git a/tests/phpMyFAQ/Session/RedisSessionHandlerTest.php b/tests/phpMyFAQ/Session/RedisSessionHandlerTest.php new file mode 100644 index 0000000000..c19e42de18 --- /dev/null +++ b/tests/phpMyFAQ/Session/RedisSessionHandlerTest.php @@ -0,0 +1,27 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('Unsupported Redis DSN scheme'); + + RedisSessionHandler::validateConnection('http://redis:6379'); + } + + public function testValidateConnectionThrowsForUnreachableTarget(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Redis session handler is configured but unreachable'); + + RedisSessionHandler::validateConnection('tcp://127.0.0.1:1', 0.1); + } +} diff --git a/tests/phpMyFAQ/Setup/Installation/DefaultDataSeederTest.php b/tests/phpMyFAQ/Setup/Installation/DefaultDataSeederTest.php index a58885b833..236753c0e0 100644 --- a/tests/phpMyFAQ/Setup/Installation/DefaultDataSeederTest.php +++ b/tests/phpMyFAQ/Setup/Installation/DefaultDataSeederTest.php @@ -29,6 +29,8 @@ public function testGetMainConfigContainsRequiredKeys(): void $this->assertArrayHasKey('main.phpMyFAQToken', $config); $this->assertArrayHasKey('security.permLevel', $config); $this->assertArrayHasKey('spam.enableCaptchaCode', $config); + $this->assertArrayHasKey('session.handler', $config); + $this->assertArrayHasKey('session.redisDsn', $config); } public function testGetMainConfigHasDynamicValues(): void diff --git a/tests/phpMyFAQ/Setup/Migration/Versions/Migration420AlphaTest.php b/tests/phpMyFAQ/Setup/Migration/Versions/Migration420AlphaTest.php index 72d48e4361..9ec7e84763 100644 --- a/tests/phpMyFAQ/Setup/Migration/Versions/Migration420AlphaTest.php +++ b/tests/phpMyFAQ/Setup/Migration/Versions/Migration420AlphaTest.php @@ -104,6 +104,8 @@ public function testUpAddsRateLimitConfigEntries(): void $this->assertContains('api.rateLimit.requests', $addedConfigKeys); $this->assertContains('api.rateLimit.interval', $addedConfigKeys); $this->assertContains('queue.transport', $addedConfigKeys); + $this->assertContains('session.handler', $addedConfigKeys); + $this->assertContains('session.redisDsn', $addedConfigKeys); } public function testUpAddsFaqrateLimitsTableSql(): void From d3ff548b39f00c952b3b4c380f4d6f26aeae796b Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Sat, 14 Feb 2026 15:08:55 +0100 Subject: [PATCH 2/3] fix: corrected review notes --- .docker/apache/Dockerfile | 2 +- README.md | 10 +++++----- phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php | 3 +-- phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php | 7 +++++-- tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.docker/apache/Dockerfile b/.docker/apache/Dockerfile index dcaad04d36..df9e99722c 100644 --- a/.docker/apache/Dockerfile +++ b/.docker/apache/Dockerfile @@ -62,7 +62,7 @@ RUN pecl install xdebug-3.5.0 \ && docker-php-ext-enable xdebug #=== Install redis PHP extension === -RUN pecl install redis \ +RUN pecl install redis-6.3.0 \ && docker-php-ext-enable redis #=== php default === diff --git a/README.md b/README.md index 78a71050ea..872224bea8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## What is phpMyFAQ? phpMyFAQ is a comprehensive, multilingual FAQ system that is entirely database-driven. -It is compatible with a variety of databases for data storage and requires PHP 8.4+ for data access. +It is compatible with a variety of databases for data storage and requires PHP 8.5+ for data access. The system features a multi-language Content Management System equipped with a WYSIWYG editor and an Image Manager. It also provides real-time search capabilities with Elasticsearch or OpenSearch. @@ -77,16 +77,16 @@ _Running using named volumes:_ - **opensearch**: OpenSearch image (it means it does not have XPack installed) - **redis**: image with a Redis database -_Running apache web server with PHP 8.4 support:_ +_Running apache web server with PHP 8.5 support:_ - **apache**: mounts the `phpmyfaq` folder in place of `/var/www/html`. -_Running nginx web server with PHP 8.4 support:_ +_Running nginx web server with PHP 8.5 support:_ - **nginx**: mounts the `phpmyfaq` folder in place of `/var/www/html`. -- **php-fpm**: PHP-FPM image with PHP 8.4 support +- **php-fpm**: PHP-FPM image with PHP 8.5 support -_Running FrankenPHP web server with PHP 8.4 support:_ +_Running FrankenPHP web server with PHP 8.5 support:_ - **frankenphp**: mounts the `phpmyfaq` folder in place of `/var/www/html`. diff --git a/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php b/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php index 44b4d207a3..698262fe8f 100644 --- a/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php +++ b/phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php @@ -75,7 +75,6 @@ public static function configureSession(?Configuration $configuration = null): v switch ($sessionHandler) { case 'files': - case 'database': ini_set('session.save_handler', value: 'files'); break; case 'redis': @@ -83,7 +82,7 @@ public static function configureSession(?Configuration $configuration = null): v break; default: throw new RuntimeException(sprintf( - 'Unsupported session handler "%s". Allowed values: files, database, redis.', + 'Unsupported session handler "%s". Allowed values: files, redis.', $sessionHandler, )); } diff --git a/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php b/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php index 4a71039612..fa1897d978 100644 --- a/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php +++ b/phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php @@ -25,14 +25,17 @@ class RedisSessionHandler { public const string DEFAULT_DSN = 'tcp://redis:6379?database=0'; - public static function configure(string $dsn = ''): void + public static function configure(string $dsn = '', bool $validate = false): void { if (!extension_loaded('redis')) { throw new RuntimeException('Redis session handler requires the PHP redis extension (ext-redis).'); } $redisDsn = trim($dsn) !== '' ? trim($dsn) : self::DEFAULT_DSN; - self::validateConnection($redisDsn); + + if ($validate) { + self::validateConnection($redisDsn); + } ini_set('session.save_handler', value: 'redis'); ini_set('session.save_path', value: $redisDsn); diff --git a/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php b/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php index 5f13ecd0da..d812b41891 100644 --- a/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php +++ b/tests/phpMyFAQ/Bootstrap/PhpConfiguratorTest.php @@ -90,7 +90,7 @@ public function testConfigureSessionUsesFilesForDatabaseHandlerInLiteMode(): voi $configuration ->method('get') ->willReturnMap([ - ['session.handler', 'database'], + ['session.handler', 'files'], ['session.redisDsn', ''], ]); From 7aa9de675bedbe0604f5de9234dfd54f4252d157 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Sat, 14 Feb 2026 15:20:37 +0100 Subject: [PATCH 3/3] docs: added some more documentation --- README.md | 4 ++-- docs/installation.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 872224bea8..1167c4b67c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## What is phpMyFAQ? phpMyFAQ is a comprehensive, multilingual FAQ system that is entirely database-driven. -It is compatible with a variety of databases for data storage and requires PHP 8.5+ for data access. +It is compatible with a variety of databases for data storage and requires PHP 8.4+ for data access. The system features a multi-language Content Management System equipped with a WYSIWYG editor and an Image Manager. It also provides real-time search capabilities with Elasticsearch or OpenSearch. @@ -33,7 +33,7 @@ and can be run on almost any web hosting provider or deployed in the cloud using ## Requirements -phpMyFAQ is only supported on PHP 8.3 and up, you need a database as well. Supported databases are MySQL, MariaDB, +phpMyFAQ is only supported on PHP 8.4+, you need a database as well. Supported databases are MySQL, MariaDB, Percona Server, PostgreSQL, Microsoft SQL Server, and SQLite3. If you want to use Elasticsearch or Opensearch as the main search engine, you need Elasticsearch v6+ or OpenSearch v1+. Check our detailed requirements on [phpmyfaq.de](https://www.phpmyfaq.de/requirements) for more information. diff --git a/docs/installation.md b/docs/installation.md index b9e9c335d3..8f52e201ed 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -67,6 +67,10 @@ The PDO extension is the preferred way to connect to your database server. - [Elasticsearch](https://www.elastic.co/products/elasticsearch) 7.x or 8.x - [OpenSearch](https://opensearch.org/) 2.x +### Optional In-Memory Data Store + +- [Redis](https://redis.io/) 8.x or later + ### Additional requirements - correctly set: access permissions, owner, group