clue/stdio-react
ReactPHP-based STDIO stream wrapper for non-blocking access to STDIN/STDOUT/STDERR. Enables event-driven CLI apps with readable and writable streams, integrating terminal input/output into the ReactPHP loop for async command-line tools.
Installation:
composer require clue/stdio-react
Add to composer.json if using Laravel's autoloader:
"autoload": {
"psr-4": {
"App\\": "app/",
"Clue\\React\\": "vendor/clue/reactphp-stdio/src/"
}
}
First Use Case: Create a simple interactive CLI command in Laravel:
// app/Console/Commands/InteractiveCommand.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Clue\React\Stdio\Stdio;
class InteractiveCommand extends Command
{
protected $signature = 'interactive:demo';
protected $description = 'Demonstrate async CLI interaction';
public function handle()
{
$loop = \React\EventLoop\Factory::create();
$stdio = new Stdio($loop);
$stdio->setPrompt('> ');
$stdio->on('data', function ($line) use ($stdio, $loop) {
$line = rtrim($line, "\r\n");
$this->info("You entered: $line");
if ($line === 'exit') {
$stdio->end();
$loop->stop();
}
});
$loop->run();
}
}
Where to Look First:
$stdio->on('data', function ($line) {
// Process input line-by-line
$this->handleInput(rtrim($line, "\r\n"));
});
$stdio->write("Processing... ");
$stdio->on('data', function ($line) use ($stdio) {
$stdio->write("Received: " . rtrim($line, "\r\n") . PHP_EOL);
});
$stdio->setPrompt('Password: ');
$stdio->setEcho('*'); // Mask input with asterisks
$stdio->on('data', function ($line) use ($stdio) {
$line = rtrim($line);
$history = $stdio->listHistory();
if (!empty($line) && end($history) !== $line) {
$stdio->addHistory($line);
}
});
$stdio->setAutocomplete(function ($input) {
return $this->suggestCommands($input);
});
// app/Providers/StdioServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Clue\React\Stdio\Stdio;
use React\EventLoop\Factory;
class StdioServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Stdio::class, function ($app) {
$loop = Factory::create();
return new Stdio($loop);
});
}
}
// app/Console/Kernel.php
protected $commands = [
\App\Console\Commands\InteractiveCommand::class,
// Other commands...
];
public function handle()
{
$this->call('interactive:demo');
}
// app/Providers/EventServiceProvider.php
public function boot()
{
Artisan::starting(function () {
$loop = \React\EventLoop\Factory::create();
$stdio = new Stdio($loop);
// Attach to Artisan's output
});
}
$stdio->on('key', function ($key) {
if ($key === 'ctrl+c') {
$this->info("Interrupt received");
}
});
$stdio->setPrompt('> ');
$stdio->on('data', function ($line) use ($stdio) {
if (substr($line, -2) === '\\n') {
$stdio->write("... ");
return;
}
// Process complete input
});
if ($stdio->isTTY()) {
$stdio->setPrompt('TTY Mode: ');
} else {
$stdio->setPrompt('Non-TTY Mode: ');
}
$stdio->write("Processing [");
$stdio->on('progress', function ($percent) use ($stdio) {
$stdio->write(str_repeat('#', $percent));
});
$stdio->write("] Done!" . PHP_EOL);
Blocking Calls:
ReactPHP HTTP clients).file_get_contents() in data handler.React\Dns\Resolver or React\Http\Client.Cursor Position Quirks:
getCursorPosition() returns character count, not screen columns.getCursorCell() for visual alignment.History Management:
addHistory().addHistory('').Echo Mode Edge Cases:
setEcho(false) hides all input, including cursor movement.setEcho('*') for passwords to show feedback.TTY Assumptions:
isTTY() may return false in pipes or non-interactive shells.php artisan command | head to catch issues.Log Raw Input:
$stdio->on('data', function ($line) {
$this->debug("Raw input: " . json_encode($line));
});
Visualize Cursor:
$stdio->on('key', function ($key) {
$this->info("Key: $key, Cursor: {$stdio->getCursorPosition()}");
});
Check TTY State:
$this->info("Is TTY: " . ($stdio->isTTY() ? 'Yes' : 'No'));
Handle Interrupts Gracefully:
$stdio->on('key', function ($key) {
if ($key === 'ctrl+c') {
$this->error("Aborted!");
$stdio->end();
$loop->stop();
}
});
Environment Variables:
HISTSIZE for history limits:
$limit = (int)getenv('HISTSIZE') ?: 500;
$stdio->limitHistory($limit);
Prompt Formatting:
\r for carriage returns (overwrites prompt):
$stdio->write("\rProcessing... ");
Autocomplete Delays:
$stdio->setAutocomplete(function ($input) {
$loop->futureTick(function () use ($input) {
return $this->suggestCommands($input);
});
});
Custom Prompt Rendering:
$stdio->setPromptRenderer(function ($prompt) {
return "\033[32m" . $prompt . "\033[0m"; // Green prompt
});
Input Validation:
$stdio->on('data', function ($line) use ($stdio) {
if (!$this->validateInput(rtrim($line, "\r\n"))) {
$stdio->write("Invalid input. Try again.\n");
$stdio->setInput(''); // Clear buffer
}
});
Plugin System:
// Extend Stdio with traits or decorators
trait StdioLogger {
public function logInput($line) {
$
How can I help you explore Laravel packages today?