Lines 53.84% 14 / 26
Methods 28.57% 2 / 7
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 toSSE 80.00% 12 / 15 0.00% 0 / 1 6.29
 output 0.00% 0 / 1 0.00% 0 / 1 2
 error 0.00% 0 / 1 0.00% 0 / 1 2
 progress 0.00% 0 / 6 0.00% 0 / 1 2
 complete 0.00% 0 / 1 0.00% 0 / 1 2
 data 100.00% 1 / 1 100.00% 1 / 1 1
26readonly final class Event
27{
28    /**
29     * @param string $type Event name, emitted as the "event:" field. An
30     *                        empty string omits the field entirely, so the
31     *                        browser dispatches the event to its default
32     *                        "message" handler (addEventListener('message')).
33     * @param array $data Event payload. JSON-encoded and emitted as one
34     *                        or more "data:" lines (multi-line JSON is split
35     *                        per line, as the SSE spec requires).
36     * @param string|null $id Optional SSE "id:" field — a cursor the
37     *                        browser sends back on reconnection (Last-Event-ID),
38     *                        letting the server resume from where it left off.
39     * @param int|null $retry Optional SSE "retry:" field — reconnection
40     *                        delay in milliseconds the browser should use
41     *                        if the connection drops.
42     */
43    public function __construct(
44        public string  $type,
45        public array   $data,
46        public ?string $id = null,
47        public ?int    $retry = null
48    ) {}
49
50    /**
51     * Serialize this event to the SSE wire format.
52     *
53     * Field order is fixed (id, retry, event, data) and each field is
54     * terminated with a newline. The event ends with a blank line, which is
55     * what tells the browser "this event is complete". The payload is
56     * JSON-encoded; if it contains newlines, each line is emitted as its
57     * own "data:" field (the spec concatenates them back with \n on the
58     * client side).
59     *
60     * @return string The complete SSE event, including the trailing blank line.
61     * @throws \RuntimeException If the payload cannot be JSON-encoded
62     *                           (e.g. invalid UTF-8 or depth overflow).
63     */
64    public function toSSE(): string
65    {
66        $output = '';
67
68        if ($this->id !== null) {
69            $output .= "id: {$this->id}\n";
70        }
71
72        if ($this->retry !== null) {
73            $output .= "retry: {$this->retry}\n";
74        }
75
76        if ($this->type !== '') {
77            $output .= "event: {$this->type}\n";
78        }
79
80        // Handle multi-line data
81        $jsonData = json_encode($this->data);
82        if ($jsonData === false) {
83            throw new \RuntimeException('Unable to JSON-encode event data: ' . json_last_error_msg());
84        }
85        $lines = explode("\n", $jsonData);
86        foreach ($lines as $line) {
87            $output .= "data: {$line}\n";
88        }
89
90        $output .= "\n";
91
92        return $output;
93    }
94
95    /**
96     * Create an "output" event — a single line of free-form output.
97     *
98     * Useful for streaming command/process output to a terminal-style UI.
99     *
100     * @param string $line The output line to send
101     * @param string|null $id Optional SSE id (see constructor).
102     * @return self
103     */
104    public static function output(string $line, ?string $id = null): self
105    {
106        return new self('output', ['line' => $line], $id);
107    }
108    /**
109     * Create an "error" event — a failure message.
110     *
111     * @param string $message The error description
112     * @param string|null $id Optional SSE id (see constructor).
113     * @return self
114     */
115    public static function error(string $message, ?string $id = null): self
116    {
117        return new self('error', ['message' => $message], $id);
118    }
119    /**
120     * Create a "progress" event — a progress report for a long-running job.
121     *
122     * Includes the current/total counts and a pre-computed percentage, so
123     * clients can render a progress bar without doing the math themselves.
124     *
125     * @param int $current Units completed so far
126     * @param int $total Total units to complete
127     * @param string|null $message Optional human-readable status text
128     * @param string|null $id Optional SSE id (see constructor).
129     * @return self
130     */
131    public static function progress(int $current, int $total, ?string $message = null, ?string $id = null): self
132    {
133        return new self('progress', [
134            'current' => $current,
135            'total' => $total,
136            'percentage' => round(($current / $total) * 100, 2),
137            'message' => $message
138        ], $id);
139    }
140
141    /**
142     * Create a "complete" event — signals that a job finished successfully.
143     *
144     * @param array $data Optional final payload (e.g. results, totals).
145     * @param string|null $id Optional SSE id (see constructor).
146     * @return self
147     */
148    public static function complete(array $data = [], ?string $id = null): self
149    {
150        return new self('complete', $data, $id);
151    }
152    /**
153     * Create a custom-named event with an arbitrary payload.
154     *
155     * The general-purpose factory: pick any event name and the browser can
156     * listen for it with addEventListener('<type>', ...). For the spec's
157     * default "message" handler, pass '' as the type.
158     *
159     * @param string $type Event name; '' omits the "event:" field (see
160     *                        constructor).
161     * @param array $data Arbitrary payload, JSON-encoded on the wire
162     * @param string|null $id Optional SSE id (see constructor).
163     * @return self
164     */
165    public static function data(string $type, array $data, ?string $id = null): self
166    {
167        return new self($type, $data, $id);
168    }
169}