You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
5.0 KiB
PHP

<?php
/* vim: set shiftwidth=2 expandtab softtabstop=2: */
namespace Boris;
/**
* Boris is a tiny REPL for PHP.
*/
class Boris {
const VERSION = "1.0.8";
private $_prompt;
private $_historyFile;
private $_exports = array();
private $_startHooks = array();
private $_failureHooks = array();
private $_inspector;
/**
* Create a new REPL, which consists of an evaluation worker and a readline client.
*
* @param string $prompt, optional
* @param string $historyFile, optional
*/
public function __construct($prompt = 'boris> ', $historyFile = null) {
$this->setPrompt($prompt);
$this->_historyFile = $historyFile
? $historyFile
: sprintf('%s/.boris_history', getenv('HOME'))
;
$this->_inspector = new ColoredInspector();
}
/**
* Add a new hook to run in the context of the REPL when it starts.
*
* @param mixed $hook
*
* The hook is either a string of PHP code to eval(), or a Closure accepting
* the EvalWorker object as its first argument and the array of defined
* local variables in the second argument.
*
* If the hook is a callback and needs to set any local variables in the
* REPL's scope, it should invoke $worker->setLocal($var_name, $value) to
* do so.
*
* Hooks are guaranteed to run in the order they were added and the state
* set by each hook is available to the next hook (either through global
* static, such as classes and interfaces, or through the 2nd parameter
* of the callback, if any local variables were set.
*
* @example Contrived example where one hook sets the date and another
* prints it in the REPL.
*
* $boris->onStart(function($worker, $vars){
* $worker->setLocal('date', date('Y-m-d'));
* });
*
* $boris->onStart('echo "The date is $date\n";');
*/
public function onStart($hook) {
$this->_startHooks[] = $hook;
}
/**
* Add a new hook to run in the context of the REPL when a fatal error occurs.
*
* @param mixed $hook
*
* The hook is either a string of PHP code to eval(), or a Closure accepting
* the EvalWorker object as its first argument and the array of defined
* local variables in the second argument.
*
* If the hook is a callback and needs to set any local variables in the
* REPL's scope, it should invoke $worker->setLocal($var_name, $value) to
* do so.
*
* Hooks are guaranteed to run in the order they were added and the state
* set by each hook is available to the next hook (either through global
* static, such as classes and interfaces, or through the 2nd parameter
* of the callback, if any local variables were set.
*
* @example An example if your project requires some database connection cleanup:
*
* $boris->onFailure(function($worker, $vars){
* DB::reset();
* });
*/
public function onFailure($hook){
$this->_failureHooks[] = $hook;
}
/**
* Set a local variable, or many local variables.
*
* @example Setting a single variable
* $boris->setLocal('user', $bob);
*
* @example Setting many variables at once
* $boris->setLocal(array('user' => $bob, 'appContext' => $appContext));
*
* This method can safely be invoked repeatedly.
*
* @param array|string $local
* @param mixed $value, optional
*/
public function setLocal($local, $value = null) {
if (!is_array($local)) {
$local = array($local => $value);
}
$this->_exports = array_merge($this->_exports, $local);
}
/**
* Sets the Boris prompt text
*
* @param string $prompt
*/
public function setPrompt($prompt) {
$this->_prompt = $prompt;
}
/**
* Set an Inspector object for Boris to output return values with.
*
* @param object $inspector any object the responds to inspect($v)
*/
public function setInspector($inspector) {
$this->_inspector = $inspector;
}
/**
* Start the REPL (display the readline prompt).
*
* This method never returns.
*/
public function start() {
declare(ticks = 1);
pcntl_signal(SIGINT, SIG_IGN, true);
if (!$pipes = stream_socket_pair(
STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP)) {
throw new \RuntimeException('Failed to create socket pair');
}
$pid = pcntl_fork();
if ($pid > 0) {
if (function_exists('setproctitle')) {
setproctitle('boris (master)');
}
fclose($pipes[0]);
$client = new ReadlineClient($pipes[1]);
$client->start($this->_prompt, $this->_historyFile);
} elseif ($pid < 0) {
throw new \RuntimeException('Failed to fork child process');
} else {
if (function_exists('setproctitle')) {
setproctitle('boris (worker)');
}
fclose($pipes[1]);
$worker = new EvalWorker($pipes[0]);
$worker->setLocal($this->_exports);
$worker->setStartHooks($this->_startHooks);
$worker->setFailureHooks($this->_failureHooks);
$worker->setInspector($this->_inspector);
$worker->start();
}
}
}