<?phpnamespace App\Chat;/** * The wire format of the chat, unchanged since 2002: one frame per line ("\n"), its fields separated by tabs, * the first field is the numeric code. The server always writes "code TAB fields... LF" (a code without fields * still has its tab: "667\t\n"), as the original PHP server did. */final class Frame{ public const TAB = "\t"; public const EOL = "\n"; /** @param list<string> $fields */ public static function encode(string $code, array $fields = []): string { return $code . self::TAB . implode(self::TAB, array_map(self::clean(...), $fields)) . self::EOL; } /** * A list joined the way the original server built its payloads: each item followed by a tab. * * @param list<string|int> $items */ public static function list(array $items): string { return $items === [] ? '' : implode(self::TAB, array_map(fn ($v) => self::clean((string)$v), $items)) . self::TAB; } /** * Splits complete lines off a buffer; what is left (an unfinished line) stays in $buffer. * * @return list<array{0: string, 1: list<string>}> [code, fields] */ public static function decode(string &$buffer): array { $frames = []; while (($end = strpos($buffer, self::EOL)) !== false) { $line = rtrim(substr($buffer, 0, $end), "\r"); $buffer = (string)substr($buffer, $end + 1); if ($line === '') { continue; } $parts = explode(self::TAB, $line); $code = array_shift($parts); $frames[] = [$code, $parts]; } return $frames; } /** A field never carries a line end (it would cut the frame). Tabs inside list payloads are kept on purpose. */ private static function clean(string $value): string { return str_replace(["\r", "\n"], ' ', $value); }}