$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 $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}> [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); } }