spoova/iotext

A Text Streamer Package

Maintainers

Package info

github.com/teymzz/IOText

pkg:composer/spoova/iotext

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-17 17:48 UTC

This package is auto-updated.

Last update: 2026-07-17 18:49:56 UTC


README

Live, single-request progress streaming for PHP.

Send one AJAX request, get real-time status updates back โ€” no polling, no background jobs, no extra endpoints. IOTextStream lets a long-running PHP process push lines like Processing data..., Extracting data..., Compressing file... straight to the client as they happen, over the same connection that started the request.

Part of the spoova/mi framework โ€” spoova\mi\core\classes\Bundle\IOText.

Why

Most "live progress" tutorials reach for polling: hit an endpoint every second, ask "are we done yet?". It works, but it's chatty, laggy, and needs somewhere to persist state between requests.

IOTextStream takes a simpler path: PHP output buffering plus chunked responses. The server echos and flush()es as work happens; the client reads the response body as a stream instead of waiting for it to finish. One request, real-time output, no extra moving parts.

Features

  • ๐Ÿ”ด True single-request streaming โ€” no polling interval, no extra round trips
  • ๐Ÿงญ Two modes โ€” stream directly from the process doing the work, or watch a file being written to by another process
  • ๐Ÿ“Š Built-in progress tracking โ€” steps() + tracked messages give you a live percentage for free
  • ๐Ÿงต Stacked, isolated contexts โ€” nested or sequential streaming blocks each get their own counters, with a fully static API
  • ๐Ÿชถ Zero dependencies beyond the framework's own Ghost* proxy classes

Installation

IOTextStream ships as part of spoova/mi:

composer require spoova/mi

Quick start

Streaming directly from the request

Use this when the work โ€” extracting, compressing, whatever โ€” happens in the same PHP process that's handling the request.

use spoova\mi\core\classes\Bundle\IOText\IOTextStream;

IOTextStream::init(steps: 4);

IOTextStream::stream_view("Processing data...");
processData();

IOTextStream::stream_view("Extracting data...");
extractData();

IOTextStream::stream_view("Compressing file...");
compressFile();

IOTextStream::stream_view("Extraction complete...");

IOTextStream::close();

Each stream_view() call writes a line, flushes it to the client immediately, and counts toward IOTextStream::progress().

Reading it on the client

fetch() with a ReadableStream reader is what makes this work โ€” plain XMLHttpRequest can't read a response cleanly while it's still arriving.

async function watchProgress(url) {
  const res = await fetch(url);
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();

    for (const line of lines) {
      if (line.trim()) updateStatus(line);
    }
  }
}

function updateStatus(line) {
  document.getElementById('status').textContent = line;
}

watchProgress('/your-endpoint');

done becomes true once PHP's script ends and the connection closes โ€” not based on anything in the message content itself.

Watching a file instead

If the actual work happens in a separate process (a queued job, a subprocess, a CLI script), there's no shared PHP process to echo from directly. IOTextStream::stream_file() bridges that gap by watching a file for changes and relaying them into the response.

use spoova\mi\core\classes\Bundle\IOText\IOTextStream;
use spoova\mi\core\classes\Bundle\IOText\IOTextFile;

IOTextStream::init();

IOTextStream::stream_file('/path/to/progress.log', function (IOTextFile $file) {
    if ($file->isModified()) {
        IOTextStream::stream_view($file->textFromLine($file->lineCount()));
    }

    if (str_contains($file->text(), 'DONE')) {
        $file->stop();
    }
}, interval: 0.5);

IOTextStream::close();

The callback receives a read-only IOTextFile snapshot with everything you need to react to changes:

Method Returns
file() Path being watched
text() Full file contents so far
lines() File contents split into lines
lineCount() Number of lines
textFromLine(int $n) 1-based line lookup
isModified() false on the first read, true after
modifiedAt() Last modification timestamp
tick() Number of times a change has been detected
exists() Whether the file currently exists
stop() Ends the watch loop

IOTextFile is abstract and only ever produced internally through a GhostProxy while stream_file() is running โ€” it isn't meant to be instantiated directly.

API reference

Output

Method Tracked? Description
view(string $message, int $timeout = 0) No Writes and flushes a plain text line
json(array $message, int $timeout = 0) No Writes and flushes a JSON-encoded line
stream_view(string $message, int $timeout = 0) Yes Same as view(), counted toward progress
stream_out(string $message, int $timeout = 0) Yes Alias of stream_view()
stream_json(array $message, int $timeout = 0) Yes Same as json(), counted toward progress
issue_text(string $message) No Writes a line and immediately exits
issue_json(array $message) No Writes a JSON line and immediately exits

Context & progress

Method Description
init(int $steps = 0) Starts a new streaming context โ€” sets headers, opens the output buffer, pushes fresh counters
close() Ends the current context and restores the previous one, if any
steps(int $steps) Sets the total step count used to calculate progress
interval(int $milliseconds) Sets a default delay applied after each message
progress(bool $int = false) Percentage complete, based on tracked messages vs. total steps
views() Total messages written in the current context
streams() Total tracked messages written in the current context
pause(int $seconds) Blocking delay, in seconds
wait(int $milliseconds) Blocking delay, in milliseconds

File watching

Method Description
stream_file(string $path, Closure $callback, float $interval = 1.0) Watches $path for changes, invoking $callback(IOTextFile $file) on the first read and every change after, until $file->stop() is called

Nested & sequential contexts

Every method above operates on a stack of independent contexts under the hood, so you can safely bracket multiple phases without one's counters bleeding into another's โ€” while every call stays a plain static method:

IOTextStream::init(steps: 2);
IOTextStream::stream_view("Phase one, step one...");
IOTextStream::stream_view("Phase one, step two...");
IOTextStream::close();

IOTextStream::init(steps: 3); // completely fresh counters
IOTextStream::stream_view("Phase two, step one...");
// ...
IOTextStream::close();

Server configuration notes

Chunked flushing only reaches the browser if nothing between PHP and the client buffers it up first:

  • Nginx โ€” IOTextStream::init() already sends X-Accel-Buffering: no to disable proxy buffering for the response.
  • Gzip / zlib.output_compression โ€” turn this off for streaming endpoints; compression buffers the whole payload before sending it.
  • Cloudflare or similar edge proxies โ€” may need additional configuration to pass chunks through as they arrive rather than buffering the full response.

Test streaming behavior early in your environment โ€” a working flush() locally doesn't guarantee the chunks survive every layer of infrastructure in front of it.

License

Part of the spoova/mi framework. See the main project repository for license details.