', $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(); } } }