Streaming ReactPHP in ReactJS
Streams have a great deal of synonymy with popular use-cases like live video consumption and live audio streaming. They are essentially data artifacts that allow those who choose to use them (YouTube and Deezer per the example cited) to instantaneously incrementally process large data blocks (video and audio files) as divvied up chunks. Streams often exist as a consistent flow of what are morsels relative to the large blocks for which a single download often confers — (transitively) to an end-user of an application — an arduous idleness (a notable turnaround time between the initiation of a download and its completion).
Streaming is right up the alley of most asynchronous runtimes for which streams are a foundation feature. ReactPHP, much like its JavaScript analog — NodeJS — is no outlier in this regard. Streams are, in ReactPHP, powered by a low-level API that efficiently shuttles data chunks between memory buffers and the Operating System kernel. The manifestation of stream usage is riveresque data flow (typically) unblighted by the blockages endemic to synchronous code. Generally, streams take on one of either Readable, Writable, or Duplex forms for which the said behavior holds.
Readable streams, like input from a console application (STDIN), are essentially one-way data types usable in shunting data from a source to some target program. Writable streams, unlike readable streams, are more apt for conveying data from a program to a destination which could be a file, socket, or output device (STDOUT). Duplex streams are meta-streams whose characteristics combine the potencies of readable and writable streams and are well suited to two-way communications: think TCP and files opened for simultaneous read-and-write operations.
Piping streams — that is, connecting the flow of one stream to that of another — often of a different type, is pivotal to transferring data from source to destination in end-to-end communications. Channeling streams asynchronously through pipes is the praxis of modern applications for which there exists a demand for real-time data. Henceforth, the focus of this text shall be building a simple streaming application in PHP with React and consuming it, sans-EventSource, with the browser-provided fetch API in a ReactJS client application.
The Streaming Server
The goal of the streaming experiment detailed in this article is to convey music lyrics stored in a file, line-by-line via Duplex Through stream. To the aforestated end, a simple ReactPHP server with a single streaming endpoint will suffice. The directory structure for the project is as follows.
server
|
└─── src
| |
| └─── api.php
| └─── constants.php
| └─── filesystem.php
└─── composer.json
└─── composer.lock
└─── lyrics.txt
└─── server.php
Key to the operationalization of the entire project is the composer.json file which not only holds the requisite file configuration for function autoloading but also, definitions of the project’s dependencies — react/http, chemem/asyncify, and nikic/fast-route. The first of the trio of dependencies — react/http avails the artifacts required for building the HTTP server à la NodeJS, the second — chemem/asyncify (that I am shamelessly plugging) executes synchronous code asynchronously, and the third — fast-route is a popular, battle-tested routing library created by one of the heralds of modern PHP. The composer.json file appears as shown below.
{
"require": {
"chemem/asyncify": "~0",
"nikic/fast-route": "~1",
"react/http": "~1"
},
"autoload": {
"files": [
"src/api.php",
"src/constants.php",
"src/filesystem.php"
]
}
}
Type the command composer install in a console of your choosing to install the libraries listed in the snippet above. Upon successful installation, proceed to create the file line reading functions freadl and freadlAsync in the filesystem.php file. The former is a synchronous function that utilizes userland filesystem functions to return the contents of a file on a specified line. The latter is simply an asynchronous version of the aforedescribed line reading function that runs its blocking routines in an asynchronously executable child process. The complete filesystem.php file appears as shown in the snippet below.
namespace App\Filesystem;use React\EventLoop\Loop;
use React\Promise\PromiseInterface;
use function Chemem\Asyncify\call;function freadl(string $file, int $line): string
{
$fp = \fopen($file, 'r');
$count = 0;
while ($count < $line) {
$contents = \fgets($fp, 1024);
\fseek($fp, \ftell($fp));
$count++;
}
fclose($fp); return !$contents ? '' : $contents;
}function freadlAsync(string $file, int $line): PromiseInterface
{
$async = call(Loop::get()); return $async(
__NAMESPACE__ . '\\freadl',
[$file, $line],
);
}
The code above accounts for a decent chunk of the algorithmic vigor of the application — the rest of which is defined in what is a straightforward HTTP server definition and a streaming API endpoint. The latter artifact, described in a function named streamLyrics (akin to a controller), iteratively funnels line output from the lyrics file into a Through stream which simultaneously writes the data it receives to the client requesting it. Parameterized via the ReactPHP event loop’s scheduling mechanism is the interval for conveying data from file to server and ultimately, requesting client — a two-second window between successive simultaneous read-write operations (far from ideal for casual melodic recitation or competitive karaoke but still sufficient for demonstration). streamLyrics simply prints each line in the project’s lyrics file and terminates the stream upon complete top-to-bottom traversal of the said file.
namespace App\Api;use App\Constants;use App\Filesystem;
use React\EventLoop\Loop;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Stream\ThroughStream;
use function React\Promise\resolve;function streamLyrics(): PromiseInterface
{
$line = 1;
$stream = new ThroughStream();
$timer = Loop::addPeriodicTimer(
2,
function () use (&$line, $stream, &$timer) {
Filesystem\freadlAsync(
__DIR__ . '/../lyrics.txt',
$line++,
)->then(
function (string $data) use ($stream, $timer) {
if (empty($data)) {
$stream->end();
Loop::cancelTimer($timer);
} $stream->write($data);
},
);
},
); $stream->on(
'close',
function () use ($timer) {
Loop::cancelTimer($timer);
},
);
return resolve(
new Response(
200,
\array_merge(Constants\CORS_HEADERS, Constants\TEXT_TYPE),
$stream,
),
);
}
To account for browser environments, it is apt to include headers in the API specification that enable preflight requests. The said data, mostly immutable, constitutes the constants.php file which also includes ReactPHP-parsable JSON and text MIME type specifications.
namespace App\Constants;const CORS_HEADERS = [
'access-control-allow-origin' => '*',
'access-control-allow-methods' => 'GET, OPTIONS',
];const JSON_TYPE = [
'content-type' => 'application/json',
];const TEXT_TYPE = [
'content-type' => 'text/plain',
];
Onto the connective glue of the server application — the actual server implementation in the project’s server.php file. The snippet to follow contains a definition of a single endpoint named /api/lyrics inclusive of its direct mapping to the aforedescribed streamLyrics function as well as response triggers for erroneous HTTP requests — invalid request methods and non-existent endpoints.
use App\Api;
use App\Constants;
use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Socket\SocketServer;
use Psr\Http\Message\ServerRequestInterface;
use function FastRoute\simpleDispatcher;
use function React\Promise\resolve;$http = new HttpServer(
function (ServerRequestInterface $request): PromiseInterface {
$dispatcher = simpleDispatcher(
function (RouteCollector $route) {
$route->get('/api/lyrics', fn () => Api\streamLyrics());
return $route;
},
);
$response = $dispatcher->dispatch(
$request->getMethod(),
$request->getUri()->getPath(),
);
$info = $response[0];
if ($info === Dispatcher::METHOD_NOT_ALLOWED) {
return resolve(
new Response(
405,
array_merge(Constants\JSON_TYPE, Constants\CORS_HEADERS),
json_encode(['message' => 'Invalid request method']),
),
);
} if ($info === Dispatcher::NOT_FOUND) {
return resolve(
new Response(
404,
array_merge(Constants\JSON_TYPE, Constants\CORS_HEADERS),
json_encode(['message' => 'Route not found']),
),
);
} return $response[1]();
},
);
$socket = new SocketServer('127.0.0.1:5000');
$http->listen($socket);echo 'Listening on port 5000' . PHP_EOL;
Stored in the lyrics.txt file are the lyrics to Lana Del Rey’s West Coast that should convey line-by-line — per the underlying server streaming mechanism — in a client application. To make the server streaming endpoint visible to clients capable of parsing the stream, simply type php server.php in a console.
The Streaming Client
ReactJS is a popular library for creating user interfaces in JavaScript. It alleviates many of the burdens of writing vanilla JavaScript to selectively update the DOM and plays nicely with APIs — browser-based and otherwise. The client discussed throughout this section is one set up to stream the lyrics data availed by the server at the focal point of the previous section with the fetch API and React hooks.
Though there are several alternatives for setting up ReactJS-powered applications, the scope of this demonstration is limited to Next.JS from which a client application can be bootstrapped by typing the following in a console.
$ npx create-next-app client --use-npm | --use-yarn
Considering the server only exposes one endpoint, a single React component should suffice. The said component, the sole constituent of the primary index.js file located in the pages directory, contains a useEffect hook invocation and two instances of the useState hook to manage the stream.
const [stop, stopStream] = useState(false)
const [lyrics, addLyrics] = useState([])
In the snippet above, the first of a duo of state components, stop, is a flag whose purpose is globally signaling the end of a stream — shortly after its termination (after the transfer of the last line). The second state component, lyrics, is a lyrics store (an array) to which each new lyric in the stream is appended. stopStream and addLyrics are, per the rubric of useState, proxies for updating the state elements — stop and lyrics to which they are respectively bound.
Onto the project’s useEffect hook the justification for using is streaming the lyrics once the page loads. Subsumed in useEffect is the mirror streamLyrics function in which the fetch request is scoped. Much like the function that powers the streaming endpoint from the previous section, the client function leverages a stream — albeit a Readable one, to iteratively decode server data.
useEffect(() => {
const streamLyrics = async () => {
const response = await fetch('http://localhost:5000/api/lyrics')
const reader = response.body
.pipeThrough(new textDecoderStream())
.getReader() while (true) {
const { value, done } = await reader.read()
if (done) {
stopStream(true)
break
}
addLyrics((prev) => [...new Set(prev.concat([value]))])
} stopStream(true)
}
// rest of useEffect
}, [lyrics, stop])
Quirky or not, the default behavior of the Response body object of the fetch API is to convey stream data as an array of 8-bit unsigned integers. Converting the default stream to more end-user intelligible text is achievable — via pipe through a TextDecoderStream. What follows (per the snippet above) is iterating through the stream body, updating the component-scoped lyrics container as each lyric arrives, and terminating the stream upon completion of the asynchronous file content transfer.
To round up all things useEffect-related is the subsequent invocation of streamLyrics. Because the goal is to preempt infinite calls to the streaming endpoint, calling the said function should only occur prior to the ascertainment of a positive stop state.
if (!stop) {
streamLyrics()
}
As creating an aesthetically elaborate interface is not the goal of the experiment described in this text, a default white background with a heading and cascading list should prove apt. To effectively complete the client is the following snippet of JSX in which critical conditional rendering logic is interspersed.
return (
<>
<div>
<h2>Lana Del Rey - West Coast Lyrics</h2>
<ul style={{ listStyleType: 'none' }}>
{lyrics.length > 0 &&
lyrics.map((lyric, idx) => (
<li
key={idx.toString()}
style={{ lineHeight: '2.5rem' }}
>
{lyric}
</li>
))}
</ul>
</div>
</>
)
The stream is at this point primed for consumption in a browser. Type npm run dev in a console to start the client and navigate to localhost:3000/ to revel in the elegance of an end-to-end lyrics stream.
All code for the server and client components featured in the tutorial is available in a Gist that you, the reader, have carte blanche to modify as you see fit.
Streaming is one of several applications of ReactPHP which turned 10 years old recently. I strongly advise that you consider exploring the toolkit’s ever-expanding ecosystem to infuse asynchrony into your PHP-powered web servers, REPLs, message brokers, and daemons.
Happy coding!