Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 30 · Практика

Сессии и вход: cookies, блокировки и смена идентификатора

Как сервер узнаёт покупателя и почему медленный AJAX способен остановить соседнюю вкладку.

Зачем сессия: узнаём пользователя в следующем запросе

Нужен простой результат: на первой странице выбрать русский язык, на второй увидеть тот же выбор. Локальные переменные первого запроса исчезнут; сессия сохранит значение на сервере. Браузер получает только непрозрачный ключ cookie, а не весь массив данных.

Два запроса одного браузера
  1. GET /save.php → $_SESSION[language] = Русский
  2. Ответ Set-Cookie: PHPSESSID=случайный ключ
  3. GET /read.php с Cookie → PHP читает language
  4. Страница: Ваш язык: Русский.

session_start() вызывают до тела HTML: скобки запускают функцию. $_SESSION — массив текущей сессии; ['language'] выбирает ключ, = сохраняет значение. session_write_close() записывает изменения и закрывает хранилище. Cookie браузер отправит автоматически следующему запросу в подходящей области.

Распакуйте архив. Выполните php -S 127.0.0.1:8793 из его папки, откройте http://127.0.0.1:8793/save.php, затем ссылку «Следующая страница». Это намеренно заданный несекретный выбор для изучения механизма; пользовательские изменения и вход далее отправляются POST-формой.

Первая страница сохраняет языкПример для указанного окружения
<?php
declare(strict_types=1);
session_start(['use_strict_mode' => true]);
$_SESSION['language'] = 'Русский';
session_write_close();
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html lang="ru"><meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>Выбор языка</title><p>Язык сохранён: Русский.</p><a href="/read.php">Следующая страница</a></html>';
Ожидаемый результат
Первая страница: Язык сохранён: Русский.
По ссылке: Ваш язык: Русский.
В новом приватном окне: Ваш язык: ещё не выбран.

Запуск проверен реальными HTTP-запросами с cookie и без неё. Эта первая ступень не проверяет пароль и не объявляет наличие сессии входом.

Во втором файле ?? выбирает запасной текст, если language отсутствует. htmlspecialchars превращает значение в безопасный HTML-текст. __DIR__ далее означает каталог текущего PHP-файла; require подключает общий файл до продолжения страницы.

Полный файл read.php
<?php
declare(strict_types=1);
session_start(['use_strict_mode' => true]);
$language = $_SESSION['language'] ?? 'ещё не выбран';
session_write_close();
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html lang="ru"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Язык</title>';
echo '<p>Ваш язык: ', htmlspecialchars($language, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '.</p></html>';

Авторизация отвечает на другой вопрос: разрешено ли этому пользователю конкретное действие. user_id записывает только проверенный серверный обработчик. Корзина не хранит окончательные цены/права: они могли измениться и читаются из актуального источника.

CSRF до большого проекта: зачем hidden-поле и что именно сравнивается

Чужая страница способна отправить форму на ваш домен: браузер приложит cookie входа автоматически. Синхронизирующий токен добавляет значение, которого у чужой страницы нет. Сервер создаёт его случайно, хранит в своей сессии, помещает в hidden-поле собственной формы и проверяет в каждом запросе, меняющем данные.

Одна форма и один POST
  1. GET формы → сервер создаёт случайный csrf и сохраняет в $_SESSION
  2. HTML: hidden name="csrf" получает это значение
  3. Браузер отправляет POST: обычные поля + csrf + cookie сессии
  4. Сервер принимает mixed → требует строку → hash_equals(эталон, полученное)
  5. Совпало → выполняет действие; нет → HTTP 403 без изменения данных

В коде ниже случайное значение намеренно не печатается. Мы наблюдаем решение, а не секрет. mixed нужен на границе запроса: злоумышленник может прислать csrf[]=x, поэтому сначала вызывается is_string. Только после этого hash_equals получает две строки; известный серверный эталон передаётся первым аргументом.

Принять токен своей формы и отклонить два неверных типа вводаСамостоятельный пример
<?php
declare(strict_types=1);

$session = ['csrf' => bin2hex(random_bytes(32))];

function csrfAccepted(mixed $sent, string $known): bool
{
    return is_string($sent) && hash_equals($known, $sent);
}

$attempts = [
    'Токен из нашей формы' => $session['csrf'],
    'Другая строка' => 'wrong-token',
    'Массив вместо строки' => ['wrong-token'],
];

foreach ($attempts as $label => $sent) {
    echo $label, ': ', csrfAccepted($sent, $session['csrf'])
        ? "действие разрешено\n"
        : "действие отклонено\n";
}
Ожидаемый результат
Токен из нашей формы: действие разрешено
Другая строка: действие отклонено
Массив вместо строки: действие отклонено

Это минимальная механика без HTTP. Следующий проект проверяет ту же схему реальными GET/POST, cookie и кодами 403/200. SameSite помогает браузеру ограничить межсайтовую отправку cookie, но в общем случае остаётся дополнительным слоем, а не заменой токена.

Поле формыЧто читает PHPКому доверять
name="username"$_POST['username']Это внешний ввод: проверить тип/длину; имя не выдаёт полномочий.
name="password"$_POST['password']Не trim, не возвращать в HTML и журнал; передать в password_verify без изменения.
name="csrf"$_POST['csrf']Сравнить с серверной $_SESSION['csrf']; hidden не является доверенным сам по себе.
Cookie сессииЕё разбирает модуль sessionID связывает запрос с серверным состоянием, но не доказывает право на каждый объект.

Полная связь с HTML-формой разобрана в главе о формах. Настройки cookie и блокировки — в полном справочнике сессий.

Рабочий кабинет: от формы входа до выхода

Получится маленький кабинет Анны: форма принимает имя и пароль, неверный ввод оставляет понятную ошибку, успешный показывает заказ №42. Кнопка выхода убирает текущий вход. Архив содержит все файлы; ничего из прежних глав копировать не требуется.

ШагЧто сделать и увидеть
ПодготовкаРаспаковать архив → php setup.php. Созданы private/users.json и private/sessions/. Учебный аккаунт anna, пароль Локальный кабинет 2026!.
Серверphp -S 127.0.0.1:8793 -t public из корня архива. Открыть http://127.0.0.1:8793/login.php. Папка private не является web-root.
ОшибкаВвести anna и wrong. Ответ401, текст «Неверное имя или пароль». Поле имени остаётся заполненным; пароль никогда не возвращается в HTML.
УспехВвести учебную фразу. POST получает303 Location:/account.php; GET кабинета показывает «Здравствуйте, Анна!» и «Заказ №42: готов к выдаче».
ВыходКнопка «Выйти» отправляет POST. Страница «Вы вышли»; повторный account.php ведёт к форме входа. GET logout.php не завершает вход.
Путь учётных данных и состояния
  1. login.php GET: сессия гостя и случайный CSRF → HTML hidden csrf
  2. POST: username/password/csrf → проверка типов и CSRF
  3. private/users.json: hash → password_verify
  4. regenerate_id(false) → только новый ID получает auth.id7
  5. account.php: сроки и auth → имя Анна и заказ42
  6. logout.php POST: CSRF → очистка массива, записи и cookie

Сначала разберите файлы по порядку ниже. В форме name="username" становится ключом $_POST['username']; password и csrf работают так же. Поле type=password скрывает символы на экране, но не шифрует сеть. Hidden тоже присылает клиент: сервер сравнивает его со своим токеном. Наличие required помогает интерфейсу; сервер всё равно отвергает массив вместо строки, слишком длинный ввод и неверный токен.

В контроллере if выбирает ветку; !== проверяет точное несовпадение; || означает «или», && — «и». ['auth'] — ключ вложенного массива. ['id' => 7] связывает имя с числом. Вызовы h, page, go, validToken, currentUser определены в bootstrap ниже; это наши функции, не дополнительные расширения PHP.

ПомощникВход, действие и результат
h(text)Получает строку → htmlspecialchars → возвращает безопасный HTML-текст. Экранируем имя при выводе, исходный пароль не меняем.
page(title, body)Получает заголовок и уже собранный безопасный HTML → закрывает сессию → печатает документ и exit. Тип never означает, что выполнение к вызывающему коду не вернётся.
go(path)Получает только заданный в коде локальный путь → закрывает сессию →303 Location → exit. Пользовательский URL сюда не попадает.
validToken(sent)mixed разрешает сначала принять любой PHP-тип → is_string → hash_equals(серверный эталон, присланное значение).
currentUser(now)Получает серверное время → читает auth → проверяет last_seen и created → возвращает массив пользователя либо null. Обновляет last_seen только при ещё допустимом входе.
Полный файл setup.php
<?php
declare(strict_types=1);
if (PHP_SAPI !== 'cli') { exit('Запустите из терминала.'); }
$path = __DIR__ . '/private';
if (!is_dir($path . '/sessions') && !mkdir($path . '/sessions', 0700, true)) { throw new RuntimeException('Не создан каталог сессий'); }
$users = ['anna' => ['id' => 7, 'name' => 'Анна', 'hash' => password_hash('Локальный кабинет 2026!', PASSWORD_BCRYPT)]];
$bytes = json_encode($users, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
if (file_put_contents($path . '/users.json', $bytes, LOCK_EX) !== strlen($bytes)) { throw new RuntimeException('Учётная запись не сохранена'); }
echo "Создан учебный аккаунт anna. Пароль хранится в виде хеша.\n";

setup.php выполняется в терминале. json_encode формирует запись аккаунта, file_put_contents сохраняет её; сравнение числа записанных байтов обнаруживает неполную запись. Лаборатория явно выбирает PASSWORD_BCRYPT, поэтому обработчик до хеширования отклоняет пароль длиннее 72 байтов и не обрезает его молча. В PHP 8.4 стандартный cost bcrypt вырос с 10 до 12; рабочее приложение измеряет свою задержку. Соль не выбирается вручную. В проекте один фиксированный учебный аккаунт; последовательная регистрация, смена и восстановление разобраны ниже в password-reference.

Полный файл private/bootstrap.php
<?php
declare(strict_types=1);
// This project is deliberately limited to the local PHP teaching server.
if (PHP_SAPI !== 'cli-server' || ($_SERVER['REMOTE_ADDR'] ?? '') !== '127.0.0.1') {
    http_response_code(503); exit('Этот учебный проект работает только на 127.0.0.1.');
}
$private = __DIR__;
if (!is_dir($private . '/sessions') || !is_file($private . '/users.json')) {
    http_response_code(503); exit('Сначала выполните php setup.php в папке проекта.');
}
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
session_name('ManualLogin');
session_save_path($private . '/sessions');
session_set_cookie_params([
    'lifetime' => 0, 'path' => '/', 'domain' => '',
    'secure' => false, // Only this loopback HTTP lab; HTTPS deployment requires true.
    'httponly' => true, 'samesite' => 'Lax',
]);
if (!session_start()) { http_response_code(503); exit('Вход временно недоступен.'); }
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
header("Content-Security-Policy: default-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'");
header('X-Content-Type-Options: nosniff');
// Only the local CLI helper can set the teaching clock; no request field does so.
$clockFile = $private . '/clock.txt';
$now = is_file($clockFile) ? (int) file_get_contents($clockFile) : time();
if (!isset($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); }
function h(string $text): string { return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function page(string $title, string $body): never {
    session_write_close();
    echo '<!doctype html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>', h($title), '</title></head><body><main><h1>', h($title), '</h1>', $body, '</main></body></html>';
    exit;
}
function go(string $path): never {
    session_write_close(); header('Location: ' . $path, true, 303); exit;
}
function validToken(mixed $sent): bool {
    return is_string($sent) && hash_equals($_SESSION['csrf'], $sent);
}
function currentUser(int $now): ?array {
    $auth = $_SESSION['auth'] ?? null;
    if (!is_array($auth)) { return null; }
    if ($now - $auth['last_seen'] >= 30 || $now - $auth['created'] >= 120) {
        unset($_SESSION['auth']); $_SESSION['expired'] = true; return null;
    }
    $_SESSION['auth']['last_seen'] = $now;
    return $auth;
}

bootstrap настраивает session до любого echo. Имя cookie ManualLogin одинаково для всех посетителей. Данные лежат вне public. На локальном HTTP Secure=false; для настоящего HTTPS нужно Secure=true, сам стенд намеренно отказывается обслуживать другие SAPI/адреса. HttpOnly запрещает чтение cookie JavaScript, SameSite=Lax ограничивает ряд межсайтовых передач; оба не заменяют CSRF и безопасный HTML-вывод.

Реально проверенный HTTP-шагЗначимая часть запроса/ответа
GET /login.php →200Set-Cookie: ManualLogin=[гостевой ID]; path=/; HttpOnly; SameSite=Lax. Случайная часть скрыта в отчёте.
POST /login.phpCookie: ManualLogin=[гостевой ID]; тело form-urlencoded содержит username=anna, UTF-8-байты password и csrf из формы.
Успешный POST →303Location: /account.php и Set-Cookie с новым ID. Проверка сравнила старый и новый ключи, не выводя их.
GET /account.php →200Новая cookie восстанавливает auth.id7; ответ содержит Cache-Control: no-store и HTML приветствия.
POST /logout.php →200Set-Cookie: ManualLogin=[удалено]; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; HttpOnly; SameSite=Lax.
Обработчик public/login.php: показать форму и принять входПример для указанного окружения
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
if (currentUser($now) !== null) { go('/account.php'); }
$message = !empty($_SESSION['expired']) ? 'Срок входа истёк. Войдите снова.' : '';
unset($_SESSION['expired']);
$username = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!validToken($_POST['csrf'] ?? null)) {
        http_response_code(403); page('Форма устарела', '<p>Откройте форму входа заново.</p><a href="/login.php">Войти</a>');
    }
    $name = $_POST['username'] ?? null;
    $password = $_POST['password'] ?? null;
    // This fixture deliberately uses bcrypt, so its documented 72-byte limit applies.
    if (!is_string($name) || !is_string($password) || strlen($name) > 40 || strlen($password) > 72 || str_contains($password, "\0")) {
        http_response_code(400); $message = 'Поля входа должны быть строками допустимой длины.';
    } else {
        $username = trim($name); // The password is deliberately not trimmed or escaped.
        $users = json_decode(file_get_contents(dirname(__DIR__) . '/private/users.json'), true, 512, JSON_THROW_ON_ERROR);
        $user = $users[$username] ?? null;
        // A stored dummy hash has the same cost; no hash is generated on a failed attempt.
        $hash = $user['hash'] ?? $users['anna']['hash'];
        $matches = password_verify($password, $hash);
        if (!$matches || $user === null) {
            http_response_code(401); $message = 'Неверное имя или пароль.';
        } else {
            // The old guest record receives no authentication data.
            $_SESSION['retired_at'] = $now;
            if (!session_regenerate_id(false)) { http_response_code(503); page('Вход недоступен', '<p>Повторите попытку позже.</p>'); }
            unset($_SESSION['retired_at']);
            $_SESSION['csrf'] = bin2hex(random_bytes(32));
            $_SESSION['auth'] = ['id' => $user['id'], 'name' => $user['name'], 'created' => $now, 'last_seen' => $now];
            go('/account.php');
        }
    }
}
$body = '<p>Учебный аккаунт: anna. Пароль: Локальный кабинет 2026!</p>';
if ($message !== '') { $body .= '<p role="alert">' . h($message) . '</p>'; }
$body .= '<form method="post" action="/login.php"><input type="hidden" name="csrf" value="' . h($_SESSION['csrf']) . '">'
    . '<p><label>Имя <input name="username" autocomplete="username" maxlength="40" required value="' . h($username) . '"></label></p>'
    . '<p><label>Пароль <input type="password" name="password" autocomplete="current-password" required></label></p>'
    . '<button type="submit">Войти</button></form>';
page('Вход в учебный кабинет', $body);
Ожидаемый результат
Форма: Вход в учебный кабинет
Неверный пароль: Неверное имя или пароль.
Правильный пароль: Здравствуйте, Анна!
Заказ №42: готов к выдаче.
Старый гостевой ID: личный кабинет недоступен.
Выход: Вы вышли
Повторный доступ: форма входа.
30 секунд бездействия: Срок входа истёк. Войдите снова.
120 секунд общего срока: Срок входа истёк. Войдите снова.
Поддельный CSRF: Выход не выполнен
Массив вместо пароля: Поля входа должны быть строками допустимой длины.
Cookie: ManualLogin; Path=/; HttpOnly; SameSite=Lax; ID сменён при входе.

В архиве example.php — копия показанного контроллера для чтения; запускайте проект командой с -t public из README, где рабочая копия называется public/login.php. Протокол проверен фактически на localhost. Случайные ID и токены не печатаются как готовые значения: HTTP-проверка сравнивает их внутри процесса.

Проверка пароля выполняется даже для неизвестного имени по заранее сохранённому dummy-хешу той же политики; это не обещание одинакового времени всего HTTP-запроса. После успеха регенерация идёт до назначения auth. Старый ID был гостем и не получает права; потеря новой cookie требует повторить вход. Повторная ротация уже авторизованных параллельных запросов требует другой политики timestamps/переходного периода, разобранной в полном session-reference.

Полный файл public/account.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
$user = currentUser($now);
if ($user === null) { go('/login.php'); }
page('Личный кабинет', '<p>Здравствуйте, ' . h($user['name']) . '!</p>'
    . '<p>Заказ №42: готов к выдаче.</p>'
    . '<form method="post" action="/logout.php"><input type="hidden" name="csrf" value="' . h($_SESSION['csrf']) . '"><button type="submit">Выйти</button></form>');

account.php не доверяет факту перехода с login.php. Каждый запрос заново вызывает currentUser. Имя берётся из закрытой серверной записи. Для приложения с разными заказами дополнительно проверяется владелец каждой записи по текущему ID; один корректный номер заказа не является разрешением его читать.

Полный файл public/logout.php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Allow: POST'); http_response_code(405); page('Нужна кнопка выхода', '<a href="/account.php">Личный кабинет</a>');
}
if (!validToken($_POST['csrf'] ?? null)) {
    http_response_code(403); page('Выход не выполнен', '<p>Откройте личный кабинет и нажмите кнопку выхода.</p>');
}
$policy = session_get_cookie_params();
$_SESSION = [];
if (!session_destroy()) { http_response_code(503); exit('Не удалось завершить сессию.'); }
setcookie(session_name(), '', [
    'expires' => 1, 'path' => $policy['path'], 'domain' => $policy['domain'],
    'secure' => $policy['secure'], 'httponly' => $policy['httponly'], 'samesite' => $policy['samesite'],
]);
echo '<!doctype html><html lang="ru"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Выход</title><h1>Вы вышли</h1><p>Личный кабинет теперь требует нового входа.</p><a href="/login.php">Войти снова</a></html>';

В выходе три разных действия: очистить $_SESSION в текущем запросе, удалить запись через session_destroy, истечь cookie с тем же path/domain. Ответ с удалением cookie не удаляет копии, ранее похищенные с других устройств, и не отзывает всю учётную запись. Для выхода везде нужна серверная модель сессий/версии и проверка отзыва на каждом защищённом запросе.

Истечение, поддельные поля и наблюдаемые отказы

У проекта два срока: 30секунд бездействия и 120секунд с момента входа. Это короткие учебные значения. Удаление старых файлов сборщиком мусора не определяет, разрешён ли доступ прямо сейчас.

ПроверкаДействия и точный результат
ПростойВ другом терминале php clock.php 1000, войти. Затем php clock.php 1030, обновить кабинет: «Срок входа истёк. Войдите снова».
Общий срокВойти при1000. Открыть кабинет при1020/1040/1060/1080/1100 — он доступен. При1120 новый запрос возвращает форму с тем же сообщением, хотя последний был20секунд назад.
Без CSRFPOST logout без hidden-токена:403 «Выход не выполнен»; защищённая страница по-прежнему доступна.
Поддельный типОтправить password[]=x с настоящим csrf:400 «Поля входа должны быть строками допустимой длины».
Хранилище не подготовленоЗапустить без setup:503 «Сначала выполните php setup.php в папке проекта».
Полный файл clock.php
<?php
declare(strict_types=1);
if (PHP_SAPI !== 'cli') { exit('Только терминал.'); }
$value = $argv[1] ?? '';
if (!ctype_digit($value)) { exit("Укажите учебные секунды: php clock.php 1000\n"); }
if (file_put_contents(__DIR__ . '/private/clock.txt', $value) === false) { throw new RuntimeException('Часы не установлены'); }
echo 'Учебное время: ', $value, "\n";

Часы читает только закрытый серверный файл. Ни поле формы, ни query-параметр не меняют время. Без clock.txt используется time(). Инструмент нужен для проверки сроков без ожиданий; это часть локальной лаборатории, не публичный endpoint.

Отдельный архив ниже намеренно запускается без setup.php. Bootstrap останавливает вход до создания сессии, поэтому отсутствие учётного хранилища не превращается в успешную авторизацию.

Не выдавать кабинет при отсутствии хранилища аккаунтовПример для указанного окружения
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/private/bootstrap.php';
if (currentUser($now) !== null) { go('/account.php'); }
$message = !empty($_SESSION['expired']) ? 'Срок входа истёк. Войдите снова.' : '';
unset($_SESSION['expired']);
$username = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!validToken($_POST['csrf'] ?? null)) {
        http_response_code(403); page('Форма устарела', '<p>Откройте форму входа заново.</p><a href="/login.php">Войти</a>');
    }
    $name = $_POST['username'] ?? null;
    $password = $_POST['password'] ?? null;
    // This fixture deliberately uses bcrypt, so its documented 72-byte limit applies.
    if (!is_string($name) || !is_string($password) || strlen($name) > 40 || strlen($password) > 72 || str_contains($password, "\0")) {
        http_response_code(400); $message = 'Поля входа должны быть строками допустимой длины.';
    } else {
        $username = trim($name); // The password is deliberately not trimmed or escaped.
        $users = json_decode(file_get_contents(dirname(__DIR__) . '/private/users.json'), true, 512, JSON_THROW_ON_ERROR);
        $user = $users[$username] ?? null;
        // A stored dummy hash has the same cost; no hash is generated on a failed attempt.
        $hash = $user['hash'] ?? $users['anna']['hash'];
        $matches = password_verify($password, $hash);
        if (!$matches || $user === null) {
            http_response_code(401); $message = 'Неверное имя или пароль.';
        } else {
            // The old guest record receives no authentication data.
            $_SESSION['retired_at'] = $now;
            if (!session_regenerate_id(false)) { http_response_code(503); page('Вход недоступен', '<p>Повторите попытку позже.</p>'); }
            unset($_SESSION['retired_at']);
            $_SESSION['csrf'] = bin2hex(random_bytes(32));
            $_SESSION['auth'] = ['id' => $user['id'], 'name' => $user['name'], 'created' => $now, 'last_seen' => $now];
            go('/account.php');
        }
    }
}
$body = '<p>Учебный аккаунт: anna. Пароль: Локальный кабинет 2026!</p>';
if ($message !== '') { $body .= '<p role="alert">' . h($message) . '</p>'; }
$body .= '<form method="post" action="/login.php"><input type="hidden" name="csrf" value="' . h($_SESSION['csrf']) . '">'
    . '<p><label>Имя <input name="username" autocomplete="username" maxlength="40" required value="' . h($username) . '"></label></p>'
    . '<p><label>Пароль <input type="password" name="password" autocomplete="current-password" required></label></p>'
    . '<button type="submit">Войти</button></form>';
page('Вход в учебный кабинет', $body);
Ожидаемый результат
HTTP503: Сначала выполните php setup.php в папке проекта.

Реальный localhost запрос проверен в отдельной папке без users.json и sessions/. Рабочий проект и его данные не меняются. После обычной подготовки первый архив разрешает вход учебному аккаунту.

Cookie содержит идентификатор, сервер хранит состояние

Наглядная модель

Cookie связывает новый запрос с серверной сессией

После входа сайт узнаёт покупателя при следующем запросе, хотя прежние локальные переменные PHP уже исчезли.

  1. Браузер отправляет cookie

    В cookie обычно находится непрозрачный ID сессии. Сама серверная корзина там не хранится.

  2. session_start() на сервере

    По ID загружается запись сессии. Обработчик хранения может удерживать её блокировку.

  3. PHP читает состояние

    Например, user_id=42 после проверенного входа. Право на конкретный заказ проверяется отдельно.

  4. Сохранить и освободить

    session_write_close() сохраняет изменения и позволяет отпустить блокировку до долгой работы.

При успешном входе меняют ID сессии. Cookie — идентификатор; запись сессии и локальные переменные запроса — разные вещи.

HTTP сам по себе не помнит посетителя. При cookie-сессии браузер передаёт идентификатор, а PHP загружает соответствующие данные из серверного хранилища. Обычно в сессии достаточно user_id, CSRF-токена и краткого flash-сообщения. Цены, права и состояние заказа лучше читать из актуального источника: пользователь мог потерять роль или товар подорожал после входа.

Идентификатор сессии обладает силой временного ключа доступа. HTTPS, Secure, HttpOnly и подходящий SameSite уменьшают способы его утечки. HttpOnly не мешает XSS выполнять действия от имени пользователя, поэтому защита вывода остаётся необходимой. Cookie-параметры задают до session_start. Не передавайте ID в URL и не выводите его в журнал.

Включённый session.use_strict_mode заставляет штатный обработчик отвергать неинициализированные идентификаторы. При успешном входе и изменении уровня привилегий идентификатор меняют, чтобы ранее известный ID не получил новые права. Немедленное удаление старой сессии при параллельных запросах может давать потерю состояния; для сложного приложения нужна продуманная миграция и краткий переходный период. Учебный последовательный пример показывает минимальную схему.

Проверить этот механизм в полном кабинете с настоящими HTTP-запросами.

Успешный пароль ещё не заканчивает вход

Проверка пароля и запись сессии должны сопровождаться политикой активности аккаунта. Заблокированный сотрудник не получает доступ только потому, что знает старый пароль. На чувствительных действиях проверяют текущие права, а не только роль, сохранённую полгода назад. Ограничение попыток учитывает аккаунт и источники запросов, но не должно позволять злоумышленнику навсегда заблокировать чужой вход.

Выход — изменение состояния, поэтому это POST с CSRF. Очистите данные сессии, удалите cookie с теми же path/domain и завершите серверную сессию. session_destroy не удаляет автоматически cookie браузера и не очищает текущий массив $_SESSION. Для выхода со всех устройств нужна отдельная серверная модель активных сессий или версия авторизации, сверяемая при запросе.

Проверить этот механизм в полном кабинете с настоящими HTTP-запросами.

Почему соседний AJAX ждёт и куда поставить session_write_close

Стандартное файловое хранилище блокирует запись сессии между session_start и её закрытием. Если обработчик десять секунд ждёт API, второй запрос того же браузера может ждать ту же блокировку. Отмена fetch в браузере не гарантирует остановку серверного скрипта. Поэтому прочитайте нужный user_id, сохраните изменения и вызовите session_write_close до медленной работы. Поведение других обработчиков зависит от их механизма блокировок.

После закрытия $_SESSION остаётся доступным в памяти, но присваивания в него уже не сохраняются автоматически. Если затем открыть сессию повторно, данные могли измениться другим запросом: нельзя бездумно перезаписать их старой копией. Длительный расчёт лучше хранить в задании базы/очереди, а сессию использовать лишь для определения пользователя. Для чтения есть read_and_close, но учитывайте особенности обновления времени хранения конкретным обработчиком.

Сессия не является универсальной блокировкой заказа. Два разных пользователя могут купить последний экземпляр; их сессии независимы. Такие гонки решают ограничениями и атомарными операциями базы. В то же время увеличение числа PHP-FPM процессов не устранит последовательное ожидание одной файловой сессии.

Освобождение блокировки
  1. session_start: прочитать пользователя
  2. Записать необходимые изменения
  3. session_write_close: снять блокировку
  4. Долгий запрос API / расчёт
  5. Ответ без повторного изменения старой копии

Сроки жизни и эксплуатация

Срок cookie, время простоя, максимальная длительность входа и удаление серверного файла — разные механизмы. Не полагайтесь исключительно на случайную уборку gc: приложение должно явно проверять собственные last_seen_at и authenticated_at. Перед обновлением last_seen_at сначала вычислите, не истёк ли допустимый интервал. Для банковских и административных операций нужна более строгая политика повторного подтверждения, чем для корзины.

При нескольких серверах общее хранилище сессий или осознанная привязка к узлу обязательны. Локальный файл на первом сервере не появится на втором. Потеря Redis/файлового хранилища должна приводить к контролируемой ошибке входа, а не к созданию псевдоавторизованного состояния. Проверяйте параллельные вкладки, повторный вход, выход, истечение времени и смену прав отдельными сценариями.

Корзина, привилегии и конкурентные вкладки

Корзина в сессии удобна для небольшого сайта, но хранить там окончательную цену заказа нельзя. При оформлении перечитайте цены, доступность и допустимые скидки. Если пользователь вошёл с другого устройства, синхронизация корзины уже требует серверной модели с версией и понятными правилами объединения. Сессия описывает текущий вход, а не автоматически все данные клиента.

При смене пароля или блокировке аккаунта заранее решите, что происходит с остальными активными сессиями. Можно хранить версию авторизации в аккаунте и сравнивать её с версией входа либо вести таблицу сессий с отзывом. Истечение browser-cookie не гарантирует немедленное уничтожение серверного файла, а наличие файла не означает действующие полномочия.

Отдельно проверяйте вход в двух вкладках, logout во время долгого запроса и повторный запуск после смены ID. Код, который безусловно записывает старый user_id после внешнего API, может восстановить уже завершённое состояние. Такие сценарии хорошо показывают, почему долгую бизнес-операцию и жизненный цикл сессии стоит разделять.

Практика: проверьте решение, а не только успешный путь

Отчёт читает user_id, вызывает API 8 секунд и в конце пишет flash. Как убрать блокировку, не потеряв flash?

Разбор решения

Сразу скопировать user_id и закрыть сессию. Результат отчёта хранить отдельно. Если flash необходим, снова открыть сессию в конце, прочитать актуальное состояние и добавить только своё сообщение; предусмотреть, что пользователь за это время вышел. Не записывать целиком старый массив.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

session_startЗагрузить корзину этого посетителя в $_SESSION или начать новую сессию.

PHP 8.2+ · Session

session_start(array $options = []): bool

Сохранить доставку для следующего открытия. Вызов без аргументов открывает сессию; $_SESSION['delivery'] выбирает ячейку «способ доставки». Присваивание = меняет её в памяти. Закрытие сохраняет её, следующий start читает сохранённое.

Путь конкретных данных
  1. delivery=Самовывоз записано в $_SESSION
  2. write_close сохраняет → новый start читает
  3. Страница получает: Доставка: Самовывоз

Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.

require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.

Загрузить корзину этого посетителя в $_SESSION или начать новую сессию.

Часть вызоваЧто означает
optionsМассив переопределений session.* без префикса session.: например use_strict_mode. Отдельный read_and_close читает и сразу закрывает.
Результат и отказыtrue при успешном старте; false и предупреждение при ошибке открытия/чтения. С PHP7.1 неуспешный старт не инициализирует $_SESSION. Повторный старт активной сессии не начинает вторую.
PHP8.5Числовой ключ options вызывает ValueError. Значения options принимают string/int/bool; иной тип даёт TypeError. Для read_and_close строка обязана представлять целое: положительное значение включает чтение/закрытие,0 и отрицательное выключают. Используйте настоящий bool.
HTTP-порядокДо вывода тела: PHP может отправить Set-Cookie и cache-заголовки. Настройка имени/хранилища/обработчика предшествует старту.
Сохранить доставку для следующего открытияСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/bootstrap.php';
if (!session_start()) { throw new RuntimeException('Сессия недоступна'); }
$_SESSION['delivery'] = 'Самовывоз';
session_write_close();
session_start();
$delivery = $_SESSION['delivery'];
session_write_close();
echo 'Доставка: ', $delivery, "\n";
Ожидаемый результат
Доставка: Самовывоз

В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.

read_and_close не запрещает менять массив в памяти, но эти изменения не сохраняются. У файлового обработчика такой проход не обновляет mtime: учитывайте политику удаления.

Прочитанная копия не становится записьюСамостоятельный пример
<?php
require __DIR__ . '/bootstrap.php';
session_start();
$_SESSION['cart_qty'] = 2;
session_write_close();
session_start(['read_and_close' => true]);
$_SESSION['cart_qty'] = 99;
session_start();
$saved = $_SESSION['cart_qty'];
session_write_close();
echo $saved, "
";
Ожидаемый результат
2

Это отдельный тест PHP8.5+. foreach перебирает два массива; catch перехватывает названные ошибки. Он не изображает отказ диска как ошибку параметров.

PHP8.5: неправильная форма optionsСамостоятельный пример
<?php
require __DIR__ . '/bootstrap.php';
$errors = [];
foreach ([[123 => true], ['read_and_close' => []]] as $options) {
    try { session_start($options); }
    catch (ValueError|TypeError $error) { $errors[] = $error::class; }
}
echo implode(',', $errors), "
";
Ожидаемый результат
ValueError,TypeError

Встроенные обработчики буферизации тоже имеют порядок: если используется ob_gzhandler, его регистрируют до старта; PHP рекомендует zlib.output_compression. Автоматическое переписывание URL связано с устаревающим trans_sid, а не с обычной cookie-сессией.

Дальше: session_status; session_write_close; session_set_cookie_params.

Читает идентификатор cookie и вызывает настроенный обработчик хранения. Возвращает true при успешном запуске. Options применяются к текущему запуску; session.auto_start и уже открытая сессия требуют согласованного поведения.

Рядом по задаче. session_status проверяет состояние; session_write_close сохраняет и освобождает блокировку.

session_statusПонять, можно ли сейчас сохранять изменения сессии.

PHP 8.2+ · Session

session_status(): int

Разрешать запись сообщения только в открытую сессию. Функция ничего не открывает. Константа PHP_SESSION_ACTIVE означает, что PHP сейчас работает с хранилищем. Это не признак входа пользователя.

Путь конкретных данных
  1. Сессия ACTIVE → сохраняем notice=Адрес сохранён
  2. write_close переводит в NONE → позднюю запись отклоняем
  3. Следующее открытие читает notice=Адрес сохранён

Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.

require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.

Понять, можно ли сейчас сохранять изменения сессии.

Часть вызоваЧто означает
АргументыНет.
РезультатPHP_SESSION_DISABLED, PHP_SESSION_NONE или PHP_SESSION_ACTIVE. Сравнивайте именованные константы, а не магические числа.
ГраницаACTIVE означает открытую сессию, а не успешный вход пользователя; проверка user_id и прав отдельная. Непустой session_id может остаться после закрытия.
Разрешать запись сообщения только в открытую сессиюСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/bootstrap.php';
session_start();
if (session_status() === PHP_SESSION_ACTIVE) { $_SESSION['notice'] = 'Адрес сохранён'; }
session_write_close();
if (session_status() !== PHP_SESSION_ACTIVE) { echo "Запись закрыта; поздние изменения не сохраняем.\n"; }
session_start(); $notice = $_SESSION['notice']; session_write_close();
echo 'Сообщение следующей страницы: ', $notice, "\n";
Ожидаемый результат
Запись закрыта; поздние изменения не сохраняем.
Сообщение следующей страницы: Адрес сохранён

В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.

void означает, что функция выполняет действие без возвращаемого значения. Защитная проверка обнаруживает запись после освобождения блокировки.

Функция flash требует открытой сессииСамостоятельный пример
<?php
require __DIR__ . '/bootstrap.php';
function addNotice(string $text): void {
    if (session_status() !== PHP_SESSION_ACTIVE) { throw new LogicException('Session is closed'); }
    $_SESSION['notice'] = $text;
}
session_start();
addNotice('Saved');
session_write_close();
try { addNotice('Too late'); }
catch (LogicException $error) { echo $error->getMessage(), "
"; }
Ожидаемый результат
Session is closed

Дальше: session_start; session_id.

Возвращает PHP_SESSION_DISABLED, PHP_SESSION_NONE либо PHP_SESSION_ACTIVE. Это состояние механизма сессий, а не подтверждение входа пользователя.

Рядом по задаче. session_start открывает сессию, session_write_close переводит активную сессию в закрытую.

session_write_closeСохранить текущие данные и освободить сессию для соседних запросов.

PHP 8.2+ · Session

session_write_close(): bool

Сохранить корзину перед долгой подготовкой страницы. У files открытая сессия удерживает блокировку одного ID. Сохраняем количество и освобождаем её до дальнейшей работы. Локальная переменная остаётся доступна после закрытия.

Путь конкретных данных
  1. В памяти cart_qty=3
  2. write_close записывает3 и освобождает files lock
  3. Повторное чтение: кисть —3шт.

Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.

require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.

Сохранить текущие данные и освободить сессию для соседних запросов.

Часть вызоваЧто означает
АргументыНет. Вызывать после последнего изменения $_SESSION, до долгого API-запроса/рендеринга.
Результатtrue при закрытии активной сессии; false, если активной сессии нет. Bool у write_close с PHP7.2 (раньше void); commit — alias с PHP4.4.
Граница гарантииВ PHP8.5.11 true не удостоверяет успешную долговечную запись: внутренний write может предупредить об отказе, а функция всё равно вернуть true. Не делайте сессию единственным журналом платежа/заказа.
После закрытия$_SESSION остаётся доступен в памяти. Изменения без нового старта не попадут в хранилище; блокировка files уже освобождена. Это не завершает весь HTTP-ответ и не отправляет его немедленно.
Сохранить корзину перед долгой подготовкой страницыСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart_qty'] = 3;
$qty = $_SESSION['cart_qty'];
session_write_close();
echo 'Страница заказа: кисть — ', $qty, " шт.\n";
session_start(); $saved = $_SESSION['cart_qty']; session_write_close();
echo 'Следующий запрос: кисть — ', $saved, " шт.\n";
Ожидаемый результат
Страница заказа: кисть — 3 шт.
Следующий запрос: кисть — 3 шт.

В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.

Второе закрытие false не записывает устаревшую локальную копию. Для реальной базы авторизация и изменения заказа выполняются отдельно.

Изменение после закрытия не сохранитсяСамостоятельный пример
<?php
require __DIR__ . '/bootstrap.php';
session_start();
$_SESSION['notice'] = 'Saved';
session_write_close();
$_SESSION['notice'] = 'Only in memory';
$again = session_write_close();
session_start();
$saved = $_SESSION['notice'];
session_write_close();
var_dump($again);
echo $saved, "\n";
Ожидаемый результат
bool(false)
Saved

Дальше: session_abort; session_reset; session_start.

Закрывает активную сессию и возвращает успешность. При обычном файловом обработчике освобождает блокировку. Массив $_SESSION остаётся в памяти.

Рядом по задаче. read_and_close у session_start подходит только чтению; session_abort закрывает без сохранения изменений.

session_regenerate_idСменить ключ текущего состояния, например перед назначением прав после входа.

PHP 8.2+ · Session

session_regenerate_id(bool $delete_old_session = false): bool

Передать права новому ID, оставив старую запись гостевой. false сохраняет прежнюю запись, что избегает немедленного удаления при соседнем запросе. Вход записывается только после смены ID. Здесь имитируется уже успешная проверка пароля; полный проект выполняет её в POST-обработчике.

Путь конкретных данных
  1. Старый ID: гость, cart=Кисть:2
  2. regenerate_id(false) → новый ID; затем user_id7
  3. Старый ключ остаётся гостевым, новый хранит пользователя7 и корзину

Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.

require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.

Сменить ключ текущего состояния, например перед назначением прав после входа.

Часть вызоваЧто означает
delete_old_sessionfalse сохраняет старую запись; true вызывает её удаление. Оба варианта меняют ID, сохраняя текущий массив.
Результатtrue/false; активная сессия и возможность отправить заголовки обязательны. При отказе не назначайте новые права.
Правильный моментПосле проверки пароля/MFA, но до записи нового user_id/уровня доступа. strict mode не заменяет ротацию уже существующего известного ID.
ГонкиНе ротируйте на каждом AJAX. false сам по себе оставляет старую авторизацию: нужны retired_at/сроки и проверка на каждом защищённом запросе. true способен потерять состояние параллельного запроса или новой cookie при обрыве сети.
Передать права новому ID, оставив старую запись гостевойСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart'] = 'Кисть:2'; $guest = session_id();
$_SESSION['retired_at'] = 1000;
if (!session_regenerate_id(false)) { throw new RuntimeException('Не удалось сменить ID'); }
unset($_SESSION['retired_at']); $_SESSION['user_id'] = 7;
$member = session_id(); session_write_close();
session_id($guest); session_start(); $old = $_SESSION['user_id'] ?? null; session_write_close();
session_id($member); session_start(); $cart = $_SESSION['cart']; $user = $_SESSION['user_id']; session_write_close();
echo $old === null ? "Старый ID: гость\n" : "Ошибка прав\n";
echo 'Новый ID: пользователь ', $user, '; корзина ', $cart, "\n";
Ожидаемый результат
Старый ID: гость
Новый ID: пользователь 7; корзина Кисть:2

В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.

Вместо проверки пароля здесь заранее задан факт успеха; 1000 — учебное время. Защищённый контроллер обязан проверять retired_at и сроки, а не считать любую загруженную сессию действующей.

Старому ID не достаются новые праваСамостоятельный пример
<?php
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart_qty'] = 2;
$old = session_id(); $_SESSION['retired_at'] = 1000;
if (!session_regenerate_id(false)) { throw new RuntimeException('Rotation failed'); }
unset($_SESSION['retired_at']); $_SESSION['user_id'] = 42; session_write_close();
session_id($old); session_start();
$oldHasLogin = isset($_SESSION['user_id']); $retired = $_SESSION['retired_at'];
session_write_close(); var_dump($oldHasLogin); echo $retired, "
";
Ожидаемый результат
bool(false)
1000

Глава Безопасность и сроки разделяет гостевую ротацию, повышение прав, плановую ротацию уже авторизованной сессии и отзыв всех устройств. Нельзя представлять две строки regenerate_id(true) как полноценный менеджер конкурентных сессий.

Дальше: session_create_id; session_destroy; session_start.

Создаёт новый ID, сохраняя текущие данные. true просит удалить старые данные. Возвращает bool; требуется активная сессия и возможность отправить cookie.

Рядом по задаче. session_start с use_strict_mode дополняет защиту. Полный сценарий входа.

session_destroyУдалить запись текущей сессии из её серверного хранилища.

PHP 8.2+ · Session

session_destroy(): bool

Удалить серверное состояние и перестать использовать старую память. destroy удаляет запись обработчика. Отдельно очищаем $_SESSION, чтобы текущий код не продолжил читать старый user_id. Cookie браузера удаляет HTTP-выход из полного проекта.

Путь конкретных данных
  1. Сохранена запись с user_id7
  2. destroy удаляет запись; $_SESSION=[] очищает память
  3. Старый ID при strict mode не восстанавливает вход

Распакуйте все файлы архива вместе. Команда php example.php выполняет этот пример; файлы из архива не нужно придумывать самостоятельно.

require __DIR__ . '/bootstrap.php' выполняет приложенный общий файл: __DIR__ — папка примера, точка соединяет путь. Он создаёт собственную session-data, выбирает files/php_serialize и отключает cookie только для CLI. Cookie между настоящими HTTP-запросами показана отдельно.

Удалить запись текущей сессии из её серверного хранилища.

Часть вызоваЧто означает
АргументыНет; нужна активная сессия.
Результатtrue при успехе, false при отказе; без активной сессии предупреждение.
Не делает автоматическиНе очищает $_SESSION в памяти и cookie браузера. Для выхода очищайте данные/снимайте авторизацию и отдельно истекайте cookie с теми же именем, path и domain.
Параллельные запросыМгновенное удаление активной записи даёт гонки. Для приложения с несколькими вкладками нужны отметка отзыва, серверные сроки и согласованная политика — подробно о ротации.
Удалить серверное состояние и перестать использовать старую памятьСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['user_id'] = 7;
$id = session_id(); session_write_close();
session_start(); session_destroy(); $_SESSION = [];
session_id($id); session_start();
$message = isset($_SESSION['user_id']) ? 'Доступ разрешён' : 'Нужен новый вход';
session_write_close(); echo $message, "\n";
Ожидаемый результат
Нужен новый вход

В архиве bootstrap.php создаёт только свою session-data, выбирает files/php_serialize и отключает cookie для CLI. Повторные start в одном запуске проверяют запись и чтение backend; это не имитация доставки cookie. В браузере тот же путь данных выполняют два HTTP-запроса в полном проекте входа. Полученные строки показывают конкретное сохранённое значение или действие; ID посетителя в вывод не попадает.

Файловый обработчик со strict mode отказывается усыновить идентификатор, записи которого уже нет; новая сессия пуста.

Strict mode не принимает удалённый IDСамостоятельный пример
<?php
require __DIR__ . '/bootstrap.php';
session_start(); $_SESSION['cart_qty'] = 2; session_write_close();
session_start(); $old = session_id(); session_destroy(); $_SESSION = [];
session_id($old); session_start();
$new = session_id() !== $old; $empty = $_SESSION === []; session_write_close();
var_dump($new, $empty);
Ожидаемый результат
bool(true)
bool(true)

Дальше: session_unset; session_regenerate_id; session_get_cookie_params.

Уничтожает данные настроенного хранилища для текущего ID и возвращает bool. Не очищает автоматически $_SESSION и cookie браузера.

Рядом по задаче. session_unset очищает переменные активной сессии; setcookie с истёкшим сроком удаляет cookie.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.