Boris.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. <?php
  2. /* vim: set shiftwidth=2 expandtab softtabstop=2: */
  3. namespace Boris;
  4. /**
  5. * Boris is a tiny REPL for PHP.
  6. */
  7. class Boris {
  8. const VERSION = "1.0.8";
  9. private $_prompt;
  10. private $_historyFile;
  11. private $_exports = array();
  12. private $_startHooks = array();
  13. private $_failureHooks = array();
  14. private $_inspector;
  15. /**
  16. * Create a new REPL, which consists of an evaluation worker and a readline client.
  17. *
  18. * @param string $prompt, optional
  19. * @param string $historyFile, optional
  20. */
  21. public function __construct($prompt = 'boris> ', $historyFile = null) {
  22. $this->setPrompt($prompt);
  23. $this->_historyFile = $historyFile
  24. ? $historyFile
  25. : sprintf('%s/.boris_history', getenv('HOME'))
  26. ;
  27. $this->_inspector = new ColoredInspector();
  28. }
  29. /**
  30. * Add a new hook to run in the context of the REPL when it starts.
  31. *
  32. * @param mixed $hook
  33. *
  34. * The hook is either a string of PHP code to eval(), or a Closure accepting
  35. * the EvalWorker object as its first argument and the array of defined
  36. * local variables in the second argument.
  37. *
  38. * If the hook is a callback and needs to set any local variables in the
  39. * REPL's scope, it should invoke $worker->setLocal($var_name, $value) to
  40. * do so.
  41. *
  42. * Hooks are guaranteed to run in the order they were added and the state
  43. * set by each hook is available to the next hook (either through global
  44. * resources, such as classes and interfaces, or through the 2nd parameter
  45. * of the callback, if any local variables were set.
  46. *
  47. * @example Contrived example where one hook sets the date and another
  48. * prints it in the REPL.
  49. *
  50. * $boris->onStart(function($worker, $vars){
  51. * $worker->setLocal('date', date('Y-m-d'));
  52. * });
  53. *
  54. * $boris->onStart('echo "The date is $date\n";');
  55. */
  56. public function onStart($hook) {
  57. $this->_startHooks[] = $hook;
  58. }
  59. /**
  60. * Add a new hook to run in the context of the REPL when a fatal error occurs.
  61. *
  62. * @param mixed $hook
  63. *
  64. * The hook is either a string of PHP code to eval(), or a Closure accepting
  65. * the EvalWorker object as its first argument and the array of defined
  66. * local variables in the second argument.
  67. *
  68. * If the hook is a callback and needs to set any local variables in the
  69. * REPL's scope, it should invoke $worker->setLocal($var_name, $value) to
  70. * do so.
  71. *
  72. * Hooks are guaranteed to run in the order they were added and the state
  73. * set by each hook is available to the next hook (either through global
  74. * resources, such as classes and interfaces, or through the 2nd parameter
  75. * of the callback, if any local variables were set.
  76. *
  77. * @example An example if your project requires some database connection cleanup:
  78. *
  79. * $boris->onFailure(function($worker, $vars){
  80. * DB::reset();
  81. * });
  82. */
  83. public function onFailure($hook){
  84. $this->_failureHooks[] = $hook;
  85. }
  86. /**
  87. * Set a local variable, or many local variables.
  88. *
  89. * @example Setting a single variable
  90. * $boris->setLocal('user', $bob);
  91. *
  92. * @example Setting many variables at once
  93. * $boris->setLocal(array('user' => $bob, 'appContext' => $appContext));
  94. *
  95. * This method can safely be invoked repeatedly.
  96. *
  97. * @param array|string $local
  98. * @param mixed $value, optional
  99. */
  100. public function setLocal($local, $value = null) {
  101. if (!is_array($local)) {
  102. $local = array($local => $value);
  103. }
  104. $this->_exports = array_merge($this->_exports, $local);
  105. }
  106. /**
  107. * Sets the Boris prompt text
  108. *
  109. * @param string $prompt
  110. */
  111. public function setPrompt($prompt) {
  112. $this->_prompt = $prompt;
  113. }
  114. /**
  115. * Set an Inspector object for Boris to output return values with.
  116. *
  117. * @param object $inspector any object the responds to inspect($v)
  118. */
  119. public function setInspector($inspector) {
  120. $this->_inspector = $inspector;
  121. }
  122. /**
  123. * Start the REPL (display the readline prompt).
  124. *
  125. * This method never returns.
  126. */
  127. public function start() {
  128. declare(ticks = 1);
  129. pcntl_signal(SIGINT, SIG_IGN, true);
  130. if (!$pipes = stream_socket_pair(
  131. STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP)) {
  132. throw new \RuntimeException('Failed to create socket pair');
  133. }
  134. $pid = pcntl_fork();
  135. if ($pid > 0) {
  136. if (function_exists('setproctitle')) {
  137. setproctitle('boris (master)');
  138. }
  139. fclose($pipes[0]);
  140. $client = new ReadlineClient($pipes[1]);
  141. $client->start($this->_prompt, $this->_historyFile);
  142. } elseif ($pid < 0) {
  143. throw new \RuntimeException('Failed to fork child process');
  144. } else {
  145. if (function_exists('setproctitle')) {
  146. setproctitle('boris (worker)');
  147. }
  148. fclose($pipes[1]);
  149. $worker = new EvalWorker($pipes[0]);
  150. $worker->setLocal($this->_exports);
  151. $worker->setStartHooks($this->_startHooks);
  152. $worker->setFailureHooks($this->_failureHooks);
  153. $worker->setInspector($this->_inspector);
  154. $worker->start();
  155. }
  156. }
  157. }