Сессии и загрузка файлов
Хранилище сессий (session.save_handler)
По умолчанию PHP хранит данные сессий в файлах. Для production-приложений с несколькими серверами нужно централизованное хранилище.
Файловое хранилище (по умолчанию)
; Default: files stored on disk
session.save_handler = files
; Directory for session files
; Default: /tmp (INSECURE on shared hosting!)
session.save_path = /var/lib/php/sessions
; File permissions for session files (octal)
; 600 = owner read/write only
session.save_path = "600;/var/lib/php/sessions"
<?php
// Session files are named: sess_<session_id>
// Example: /var/lib/php/sessions/sess_abc123def456
// Check current handler
echo ini_get('session.save_handler'); // "files"
echo ini_get('session.save_path'); // "/var/lib/php/sessions"
// Session file content is serialized data:
// username|s:5:"admin";role|s:4:"user";last_active|i:1708617600;
Проблема файлов: На высоконагруженных серверах тысячи session-файлов в одной директории замедляют файловую систему. Также невозможно масштабировать на несколько серверов.
Redis-хранилище
; Redis -- recommended for production
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?auth=secret&database=1"
; With Redis Sentinel
session.save_path = "tcp://sentinel1:26379?auth=secret&sentinel=mymaster,tcp://sentinel2:26379?auth=secret&sentinel=mymaster"
; With TLS
session.save_path = "tls://redis.example.com:6380?auth=secret"
Memcached-хранилище
; Memcached
session.save_handler = memcached
session.save_path = "127.0.0.1:11211"
; Multiple servers
session.save_path = "server1:11211,server2:11211"
Сравнение хранилищ
| Хранилище | Скорость | Масштабирование | Персистентность | Когда использовать |
|---|---|---|---|---|
| files | Средняя | Один сервер | Да | Разработка, малые проекты |
| Redis | Быстрая | Кластер | Настраивается | Production (рекомендуется) |
| Memcached | Быстрая | Кластер | Нет | Когда потеря сессий допустима |
| Database | Медленная | Да | Да | Когда нужен аудит сессий |
Сборщик мусора сессий (Garbage Collection)
PHP периодически удаляет устаревшие файлы сессий. Частота определяется вероятностной формулой.
Конфигурация GC
; Session lifetime in seconds
; After this time inactive session data becomes eligible for GC
session.gc_maxlifetime = 1440 ; 24 minutes (default)
; GC probability = gc_probability / gc_divisor
; Default: 1/100 = 1% chance on each request
session.gc_probability = 1
session.gc_divisor = 100
; For high-traffic sites: reduce probability
session.gc_probability = 1
session.gc_divisor = 1000 ; 0.1% chance
; Disable GC entirely (use external cron job instead)
session.gc_probability = 0
Вероятность запуска GC
<?php
// GC runs with probability = gc_probability / gc_divisor
// Default: 1/100 = 1% of requests trigger GC
// On Debian/Ubuntu: GC is DISABLED by default!
// gc_probability = 0
// Session cleanup is done via cron: /etc/cron.d/php
// This is intentional -- prevents random request slowdowns
// Check if GC is enabled
$prob = (int) ini_get('session.gc_probability');
$div = (int) ini_get('session.gc_divisor');
if ($prob === 0) {
echo "GC disabled -- check cron jobs\n";
} else {
echo "GC probability: " . ($prob / $div * 100) . "%\n";
}
Подвох Debian/Ubuntu: На этих системах
session.gc_probability = 0по умолчанию. Сборка мусора вообще не запускается через PHP! Вместо этого работает системный cron-скрипт. Если вы используете нестандартныйsession.save_path, старые сессии НЕ будут удаляться.
Правильная очистка через cron
# Cron job for session cleanup (better than PHP GC)
# Runs every 30 minutes, deletes sessions older than 24 minutes
*/30 * * * * find /var/lib/php/sessions -name 'sess_*' -mmin +24 -delete
# For Redis: sessions expire automatically via TTL
# No cron needed!
Время жизни сессии
; gc_maxlifetime: when INACTIVE session data is eligible for deletion
session.gc_maxlifetime = 1440 ; 24 minutes
; cookie_lifetime: when browser deletes the session cookie
; 0 = session cookie (deleted when browser closes)
session.cookie_lifetime = 0
; IMPORTANT: these are DIFFERENT things!
; gc_maxlifetime: server-side data expiration
; cookie_lifetime: client-side cookie expiration
<?php
// Common mistake: setting only one
// If gc_maxlifetime = 3600 (1 hour)
// but cookie_lifetime = 0 (session cookie)
// Then: user closes browser -> cookie gone -> session data stays 1 hour on server
// If gc_maxlifetime = 1440 (24 min)
// but cookie_lifetime = 86400 (1 day)
// Then: cookie valid but server data deleted after 24 min -> empty session!
// Best practice: gc_maxlifetime >= cookie_lifetime
// Or cookie_lifetime = 0 (session cookies)
// Programmatic session management
session_start([
'gc_maxlifetime' => 3600,
'cookie_lifetime' => 3600,
]);
Сериализация данных сессии
; Serialization handler for session data
; php: default PHP serialization (key|serialized_value)
; php_serialize: standard serialize() format (PHP 5.5.4+)
; php_binary: binary format
session.serialize_handler = php_serialize
<?php
// "php" format (default, legacy):
// name|s:5:"admin";age|i:25;
// WARNING: pipes (|) in keys cause issues!
// "php_serialize" format (recommended):
// a:2:{s:4:"name";s:5:"admin";s:3:"age";i:25;}
// Uses standard serialize() -- no character restrictions
// IMPORTANT: changing handler with existing sessions
// will corrupt all active sessions! Plan migration.
Загрузка файлов
Основные директивы
; Enable file uploads
file_uploads = On
; Maximum size of a SINGLE uploaded file
upload_max_filesize = 2M ; Default (very small!)
; Maximum size of entire POST body (files + form data)
post_max_size = 8M ; Must be >= upload_max_filesize
; Maximum number of files in single request
max_file_uploads = 20 ; Default
; Temporary directory for uploaded files
upload_tmp_dir = /tmp ; Default: system temp dir
Иерархия лимитов
memory_limit >= post_max_size >= upload_max_filesize
Example for 100MB file uploads:
upload_max_filesize = 100M
post_max_size = 110M ; Extra space for form data
memory_limit = 256M ; Larger than post_max_size
max_execution_time = 300 ; Time for upload processing
max_input_time = 300 ; Time for receiving upload
Критическое правило: Если
post_max_size < upload_max_filesize, при загрузке файла между этими размерами$_FILESи$_POSTбудут полностью пустыми без какой-либо ошибки.
Уровни доступа файловых директив
<?php
// IMPORTANT: access levels differ!
// file_uploads -- PHP_INI_SYSTEM (only php.ini)
// upload_max_filesize -- PHP_INI_PERDIR (.user.ini, php.ini)
// post_max_size -- PHP_INI_PERDIR (.user.ini, php.ini)
// max_file_uploads -- PHP_INI_SYSTEM (only php.ini)
// NONE of these can be changed via ini_set()!
ini_set('upload_max_filesize', '100M'); // Returns false!
ini_set('post_max_size', '100M'); // Returns false!
Обработка ошибок загрузки
<?php
// Always check upload error code first
$errorMessages = [
UPLOAD_ERR_OK => 'Upload successful',
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize (' . ini_get('upload_max_filesize') . ')',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE in HTML form',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary directory',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload',
];
function handleUpload(array $file): string
{
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException(
$errorMessages[$file['error']] ?? 'Unknown upload error'
);
}
// CRITICAL: Always verify with is_uploaded_file()
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('Security violation: not an uploaded file');
}
// Move to permanent location
$destination = '/var/www/app/uploads/' . bin2hex(random_bytes(16)) . '.dat';
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Failed to move uploaded file');
}
return $destination;
}
Проверка конфигурации загрузок
<?php
function checkUploadConfig(int $requiredSizeMB): array
{
$issues = [];
$uploadMax = convertToBytes(ini_get('upload_max_filesize'));
$postMax = convertToBytes(ini_get('post_max_size'));
$memoryLimit = ini_get('memory_limit');
$memoryBytes = $memoryLimit === '-1' ? PHP_INT_MAX : convertToBytes($memoryLimit);
$required = $requiredSizeMB * 1024 * 1024;
if ($uploadMax < $required) {
$issues[] = "upload_max_filesize too small: " . ini_get('upload_max_filesize');
}
if ($postMax < $uploadMax) {
$issues[] = "post_max_size < upload_max_filesize (broken!)";
}
if ($memoryBytes < $postMax) {
$issues[] = "memory_limit < post_max_size";
}
if (!ini_get('file_uploads')) {
$issues[] = "file_uploads is disabled!";
}
return $issues;
}
function convertToBytes(string $value): int
{
$unit = strtoupper(substr(trim($value), -1));
$bytes = (int) $value;
return match ($unit) {
'G' => $bytes * 1024 * 1024 * 1024,
'M' => $bytes * 1024 * 1024,
'K' => $bytes * 1024,
default => $bytes,
};
}
Настройка для разных сценариев
API-сервер (без загрузок)
file_uploads = Off
upload_max_filesize = 0
post_max_size = 2M
max_file_uploads = 0
session.save_handler = redis
session.save_path = "tcp://redis:6379"
session.gc_maxlifetime = 900
Файловый сервис (большие загрузки)
file_uploads = On
upload_max_filesize = 500M
post_max_size = 550M
memory_limit = 1G
max_file_uploads = 50
max_execution_time = 600
max_input_time = 600
session.save_handler = redis
session.save_path = "tcp://redis:6379"
CMS / админ-панель
file_uploads = On
upload_max_filesize = 50M
post_max_size = 55M
memory_limit = 256M
max_file_uploads = 20
session.save_handler = files
session.save_path = /var/lib/php/sessions
session.gc_maxlifetime = 3600
session.cookie_lifetime = 0