= LARGE_LITERAL_THRESHOLD bytes) are read in 8 KB * chunks into php://temp resources instead of being appended to $raw, * so the body content never reaches the lexer as a plain string. * * @return array{string, list} [$raw, $preloadedLiterals] */ private function readNextRaw(ResponseStream $stream): array { $raw = $stream->readLine(); $preloaded = []; while (preg_match('/\{(?\d+)}\r\n$/', $raw, $matches)) { $literalSize = (int) $matches['bytes']; if ($literalSize >= self::LARGE_LITERAL_THRESHOLD) { // Stream into a temp file to avoid holding a huge string in // memory. php://temp uses RAM up to 2 MB then spills to disk. $tmp = fopen('php://temp', 'r+'); $remaining = $literalSize; while ($remaining > 0) { $chunk = $stream->read(min(8192, $remaining)); fwrite($tmp, $chunk); $remaining -= strlen($chunk); } rewind($tmp); $preloaded[] = $tmp; // Keep the {N}\r\n header in $raw so the parser can read the // literal size, but do NOT append the N bytes — the parser // will pull them from the preloaded resource instead. } else { $raw .= $stream->read($literalSize); } $raw .= $stream->readLine(); } return [$raw, $preloaded]; } public function handle(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Response { $responseBuilder = new ResponseBuilder($statusTag); do { [$raw, $preloaded] = $this->readNextRaw($stream); $line = $this->parser->parse($raw, $preloaded); if ($line instanceof CommandContinuation) { $continuationHandler->continue(); continue; } $responseBuilder->addLine($line); } while (!$responseBuilder->hasStatus()); return $responseBuilder->build(); } /** * Streams parsed response lines one at a time as a Generator, yielding each * untagged Line immediately as it arrives from the socket. The terminal * Status line is NOT yielded; instead it is set as the generator return * value so callers can retrieve it via $gen->getReturn() after exhaustion. * * @throws CommandFailed if the tagged status is NO or BAD * * @return Generator */ public function stream(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Generator { $status = null; do { [$raw, $preloaded] = $this->readNextRaw($stream); $line = $this->parser->parse($raw, $preloaded); if ($line instanceof CommandContinuation) { $continuationHandler->continue(); continue; } if ($line instanceof Status && $line->tag === $statusTag) { $status = $line; break; } yield $line; } while (true); if ($status->type !== StatusType::OK) { throw CommandFailed::withStatus($status); } return $status; } }