Управление идентификатором сессии
Request::sessionId() читает идентификатор из cookie Session::$name (по умолчанию PHPSID), проверяет формат либо создаёт новый из random_bytes. Допустимы 16–256 символов A–Z, a–z, 0–9, запятая и дефис. Создание/смена cookie требует присоединённого TCP-соединения; для автономного Request это не рабочий auth flow.
sessionId($id) задаёт валидный идентификатор и ставит Set-Cookie в очередь заголовков соединения. sessionRegenerateId($deleteOld = false) переносит данные в новый объект Session; при true старые данные очищаются. Для смены уровня привилегий планируйте ротацию и сохранение явно, учитывая параллельные запросы со старым cookie. Ротация ID сама не проверяет пароль и не отзывает другие пользовательские сессии.
При выходе flush() + save() очищают текущее хранилище; отдельно отправьте cookie с истёкшим Max-Age и теми же path/domain. Для глобального logout нужна серверная модель отзыва, а не только удаление cookie в браузере.
Проверяемый ниже пример не выполняет вход и не создаёт пользовательские данные: он фиксирует разбор Request, HEAD/chunked и семантику Session на изолированном handler. Запуск: положите файл рядом с Composer vendor Server 7 и выполните php http-contracts.php. Ожидается http-contracts-example-ok.
<?php
declare(strict_types=1);
use localzet\Server\Protocols\Http\Request;use localzet\Server\Protocols\Http\Response;use localzet\Server\Protocols\Http\Session;use localzet\Server\Protocols\Http\Session\SessionHandlerInterface;
require __DIR__ . '/vendor/autoload.php';
function verify(bool $condition, string $message): void{ if (!$condition) { throw new RuntimeException($message); }}
$request = new Request("POST /items?id=query HTTP/1.1\r\nHost: example.test\r\nContent-Type: application/json\r\n\r\n" . '{"id":"body","active":false}');verify($request->input('id') === 'query', 'Query must take precedence');verify($request->post('id') === 'body', 'Body remains separately accessible');verify($request->all() === ['id' => 'query', 'active' => false], 'Merged input mismatch');verify($request->fullUrl() === '//example.test/items?id=query', 'URL is scheme-relative');$invalid = new Request("POST / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{");verify($invalid->json() === null && $invalid->post() === [], 'Malformed JSON is not automatically rejected');
$response = new Response(200, [], 'hello');verify($response->withHeader('X-Example', 'yes') === $response, 'Response is mutable');$response->withoutBody();verify($response->getHeader('Content-Length') === '5', 'HEAD representation length');verify(str_ends_with((string)$response, "\r\n\r\n"), 'HEAD must omit body');$stream = (new Response(200, ['Content-Length' => '5'], 'hello'))->withChunkedTransfer();verify($stream->getHeader('Content-Length') === null && $stream->rawBody() === '', 'Chunked setup must clear body and length');
$handler = new class implements SessionHandlerInterface { public array $data = []; public bool $failWrites = false; public function open(string $savePath, string $name): bool { return true; } public function close(): bool { return true; } public function read(string $sessionId): string|false { return $this->data[$sessionId] ?? false; } public function write(string $sessionId, string $sessionData): bool { if ($this->failWrites) { return false; } $this->data[$sessionId] = $sessionData; return true; } public function destroy(string $sessionId): bool { unset($this->data[$sessionId]); return true; } public function gc(int $maxLifetime): bool { return true; } public function updateTimestamp(string $sessionId, string $data = ''): bool { return isset($this->data[$sessionId]); }};Session::setHandler($handler);Session::$gcProbability = [0, 1];$id = str_repeat('a', 32);$session = new Session($id);$session->put(['nullable' => null, 'user' => 42]);verify(!$session->has('nullable') && $session->exists('nullable'), 'Null presence semantics');$session->save();$loaded = new Session($id);verify($loaded->get('user') === 42, 'Session round trip');$loaded->flush();$loaded->save();verify(!isset($handler->data[$id]), 'Empty session must destroy storage');$handler->failWrites = true;$session->set('user', 43);$session->save();verify(!isset($handler->data[$id]), 'False write is currently ignored by save');unset($session, $loaded);echo "http-contracts-example-ok\n";