Session identifier management
Request::sessionId() reads the cookie named by Session::$name (PHPSID by default), validates its format or generates a new value using random_bytes. Allowed identifiers contain 16–256 A–Z, a–z, 0–9, comma or hyphen characters. Creating/changing the cookie requires an attached TCP connection; a detached Request is not a working authentication flow.
sessionId($id) sets a valid identifier and queues Set-Cookie on the connection. sessionRegenerateId($deleteOld = false) copies data to a new Session object; true clears old data. Plan explicit rotation and persistence when privileges change, accounting for concurrent requests using the old cookie. Rotation does not validate passwords or revoke other sessions.
On logout, flush() + save() clear current storage; separately expire the cookie using a negative Max-Age and matching path/domain. Global logout requires server-side revocation, not merely deleting a browser cookie.
The executable example below performs no login and creates no user data: it checks Request parsing, HEAD/chunked behavior and Session semantics with an isolated handler. Place it next to Server 7 Composer vendor and run php http-contracts.php. Expected output: 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";