PHPackages                             spoova/iotext - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. spoova/iotext

ActiveLibrary

spoova/iotext
=============

A Text Streamer Package

v1.0.0(1mo ago)03↓66.7%MITPHP

Since Jul 17Pushed 1mo agoCompare

[ Source](https://github.com/teymzz/IOText)[ Packagist](https://packagist.org/packages/spoova/iotext)[ RSS](/packages/spoova-iotext/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (1)Versions (2)Used By (0)

IOTextStream
============

[](#iotextstream)

**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`](https://github.com/spoova/mi) framework — `spoova\mi\core\classes\Bundle\IOText`.

---

Why
---

[](#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 `echo`s 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
--------

[](#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
------------

[](#installation)

`IOTextStream` ships as part of `spoova/mi`:

```
composer require spoova/mi
```

Quick start
-----------

[](#quick-start)

### Streaming directly from the request

[](#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

[](#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
-----------------------

[](#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:

MethodReturns`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
-------------

[](#api-reference)

### Output

[](#output)

MethodTracked?Description`view(string $message, int $timeout = 0)`NoWrites and flushes a plain text line`json(array $message, int $timeout = 0)`NoWrites and flushes a JSON-encoded line`stream_view(string $message, int $timeout = 0)`YesSame as `view()`, counted toward progress`stream_out(string $message, int $timeout = 0)`YesAlias of `stream_view()``stream_json(array $message, int $timeout = 0)`YesSame as `json()`, counted toward progress`issue_text(string $message)`NoWrites a line and immediately exits`issue_json(array $message)`NoWrites a JSON line and immediately exits### Context &amp; progress

[](#context--progress)

MethodDescription`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

[](#file-watching)

MethodDescription`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 &amp; sequential contexts

[](#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
--------------------------

[](#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
-------

[](#license)

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

###  Health Score

34

—

LowBetter than 74% of packages

Maintenance90

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

45d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/83e5087db5782cfe0290df66bd2871ab132d9ba620c1ac25b8e6036a71211302?d=identicon)[teymzz](/maintainers/teymzz)

---

Top Contributors

[![teymzz](https://avatars.githubusercontent.com/u/121361329?v=4)](https://github.com/teymzz "teymzz (1 commits)")

### Embed Badge

![Health badge](/badges/spoova-iotext/health.svg)

```
[![Health](https://phpackages.com/badges/spoova-iotext/health.svg)](https://phpackages.com/packages/spoova-iotext)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
