Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .docker/apache/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down Expand Up @@ -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-6.3.0 \
&& docker-php-ext-enable redis

#=== php default ===
ENV PMF_TIMEZONE="Europe/Berlin" \
PMF_ENABLE_UPLOADS=On \
Expand Down
4 changes: 4 additions & 0 deletions .docker/frankenphp/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
8 changes: 6 additions & 2 deletions .docker/php-fpm/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -75,17 +75,18 @@ _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:_
_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`.

Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -60,6 +69,7 @@ services:
links:
- mariadb:db
- postgres
- redis
- elasticsearch
- opensearch
ports:
Expand Down Expand Up @@ -107,6 +117,7 @@ services:
links:
- mariadb:db
- postgres
- redis
- elasticsearch
- opensearch
volumes:
Expand Down Expand Up @@ -136,6 +147,7 @@ services:
links:
- mariadb:db
- postgres
- redis
- elasticsearch
- opensearch
ports:
Expand Down
4 changes: 4 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion phpmyfaq/src/phpMyFAQ/Bootstrap/PhpConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

namespace phpMyFAQ\Bootstrap;

use phpMyFAQ\Configuration;
use phpMyFAQ\Session\RedisSessionHandler;
use RuntimeException;

class PhpConfigurator
{
/**
Expand Down Expand Up @@ -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');
Expand All @@ -65,6 +69,23 @@ 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':
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, redis.',
$sessionHandler,
));
}
}
}
}
2 changes: 1 addition & 1 deletion phpmyfaq/src/phpMyFAQ/Bootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function run(): self
$this->connectDatabase($databaseFile);

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

// 13. LDAP
$this->configureLdap();
Expand Down
97 changes: 97 additions & 0 deletions phpmyfaq/src/phpMyFAQ/Session/RedisSessionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* Configures native PHP Redis-backed sessions with connectivity checks.
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*
* @package phpMyFAQ
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
* @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 = '', 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;

if ($validate) {
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', '');
Expand Down
Loading