PHP does not currently provide a cross-platform API for basic terminal operations such as querying the terminal size, switching the terminal into raw mode, reading individual keys, or reading secret input without echo.
Userland libraries generally implement these features using platform-specific workarounds. On POSIX systems this often involves invoking tools such as stty, while Windows requires the Win32 Console API.
This RFC proposes a small native terminal API in the Io\Terminal namespace.
<?php use Io\Terminal\Key; use Io\Terminal\Terminal; use Time\Duration; $terminal = Terminal::create(); $size = $terminal->getSize(); if ($size !== false) { printf("%dx%d\n", $size->cols, $size->rows); } $mode = $terminal->enableRawMode(); try { $key = $terminal->readKey(Duration::fromSeconds(5)); if ($key === Key::Escape) { echo "Escape pressed\n"; } } finally { if ($mode !== false) { $terminal->restoreMode($mode); } }
The RFC adds an object-oriented terminal API under Io\Terminal.
The proposed API is:
<?php namespace Io\Terminal; class TerminalException extends \Io\IoException {} enum Key { case Up; case Down; case Right; case Left; case Enter; case Backspace; case Escape; case Tab; case Home; case End; case Delete; case PageUp; case PageDown; case Resize; case F1; case F2; case F3; case F4; case F5; case F6; case F7; case F8; case F9; case F10; case F11; case F12; } final readonly class TerminalSize { public readonly int $cols; public readonly int $rows; private function __construct() {} } final class ModeToken { private function __construct() {} } final class Terminal { private function __construct() {} public static function create(): Terminal {} /** * @param resource $input * @param resource|null $output */ public static function fromStreams($input, $output = null): Terminal {} public function getSize(): TerminalSize|false {} public function enableRawMode(): ModeToken|false {} public function restoreMode(?ModeToken $mode = null): bool {} public function readKey( ?\Time\Duration $timeout = null, ?\Time\Duration $sequenceTimeout = null, ): Key|string|false {} public function readSecret(): string {} }
Terminal and ModeToken are not serializable.
Terminal::create() creates a terminal session using the process standard input and standard output.
Terminal::fromStreams() creates a terminal session using an explicit input stream and an optional output stream. If the output stream is omitted, the input stream is also used as the output stream.
The constructor is private. Terminal instances are created using these named constructors.
Terminal::getSize() queries the size of the terminal associated with the output side of the session.
On success it returns a TerminalSize object containing the number of columns and rows. It returns false if a native terminal size cannot be obtained.
No fallback to the COLUMNS or LINES environment variables is performed by this API.
Terminal::enableRawMode() enables raw input mode for the session input and returns a ModeToken. It returns false if raw mode cannot be enabled.
Terminal::restoreMode() restores a saved terminal mode. A token can be passed explicitly, or the method can be called without an argument to restore the active mode owned by the Terminal instance.
ModeToken is an opaque object. An active token restores its saved terminal mode when it is destroyed. A Terminal instance also restores an active mode it owns when the object is destroyed. Active modes are restored during normal request shutdown as well.
These restoration mechanisms are intended to avoid leaving a terminal in raw or no-echo mode after ordinary PHP control flow exits. They cannot provide a guarantee for process termination that bypasses PHP shutdown entirely.
Terminal::readKey() reads one logical key from the session input.
Recognized special keys are returned as Io\Terminal\Key enum cases. Printable input is returned as a string. Input that does not map to a known enum case is also returned as a string rather than discarded.
Key::Resize is returned when a terminal resize event is observed.
The optional $timeout controls how long the method waits for input. A null timeout waits indefinitely. A zero duration performs a non-blocking poll.
On POSIX, $sequenceTimeout controls the short wait used while reading multi-byte input and terminal escape sequences. If it is omitted, the implementation uses a 25 ms sequence timeout. Windows receives structured console input events and does not use this value.
Both timeout parameters use Time\Duration. Negative durations result in a ValueError.
Terminal::readSecret() reads input without echoing the entered characters.
Enter completes the input. Backspace removes the previous character. Escape, Ctrl+C, and Ctrl+D cancel the read.
If the read cannot be completed, or if the terminal mode cannot be restored, the method throws Io\Terminal\TerminalException.
TerminalException extends Io\IoException.
Terminal handling is inherently platform-specific. POSIX systems expose terminal functionality through APIs such as termios and ioctl, while Windows uses the Win32 Console API.
Without native support, PHP libraries need external processes, FFI, or other platform-specific workarounds for these operations.
The API proposed here is based on experience from the ext-terminal extension and from integrating that extension with Symfony Console/TUI. The core proposal is intentionally smaller than the complete ext-terminal API.
No existing PHP API is changed.
The RFC introduces the following names:
Io\Terminal\TerminalIo\Terminal\TerminalSizeIo\Terminal\ModeTokenIo\Terminal\TerminalExceptionIo\Terminal\KeyUserland code declaring one of these exact names would conflict with the new internal symbol.
PHP 8.7.
CLI libraries may use the native API when it is available and retain their existing fallbacks for older PHP versions.
The ext-terminal extension can continue to serve as a backport and as a place to experiment with functionality that is outside this RFC.
The implementation is part of ext/standard.
No existing extension API is removed or changed.
The main use case is CLI and other environments attached to a terminal.
The API itself is available independently of SAPI. Operations that require a terminal return false or throw TerminalException as described above when the associated stream is not a usable terminal.
None currently.
The following functionality exists in the reference extension or has been explored separately, but is not proposed by this RFC:
These can be considered separately if there is sufficient need and the API semantics are clear.
The primary vote will be:
Implement the Io\Terminal API as described in this RFC?
The available choices will be Yes, No, and Abstain.
Acceptance requires a 2/3 majority of Yes votes over No votes.
Implementation:
https://github.com/php/php-src/pull/23941
Reference extension:
https://github.com/prateekbhujel/php-terminal
The implementation includes PHPT coverage for terminal construction, terminal sizing, raw mode and restoration, key input, escape sequences, timeout validation, and secret input.
Not implemented.
Previous discussion:
Related work:
Earlier iterations considered a larger API. The following are not part of this RFC:
getWidth() and getHeight() methods in addition to getSize()