StdinHandler.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. namespace Consolidation\AnnotatedCommand\Input;
  3. use Symfony\Component\Console\Input\StreamableInputInterface;
  4. use Symfony\Component\Console\Input\InputInterface;
  5. /**
  6. * StdinHandler is a thin wrapper around php://stdin. It provides
  7. * methods for redirecting input from a file, possibly conditionally
  8. * under the control of an Input object.
  9. *
  10. * Example trivial usage (always reads from stdin):
  11. *
  12. * class Example implements StdinAwareInterface
  13. * {
  14. * /**
  15. * * @command cat
  16. * * @param string $file
  17. * * @default $file -
  18. * * /
  19. * public function cat()
  20. * {
  21. * print($this->stdin()->contents());
  22. * }
  23. * }
  24. *
  25. * Command that reads from stdin or file via an option:
  26. *
  27. * /**
  28. * * @command cat
  29. * * @param string $file
  30. * * @default $file -
  31. * * /
  32. * public function cat(InputInterface $input)
  33. * {
  34. * $data = $this->stdin()->select($input, 'file')->contents();
  35. * }
  36. *
  37. * Command that reads from stdin or file via an option:
  38. *
  39. * /**
  40. * * @command cat
  41. * * @option string $file
  42. * * @default $file -
  43. * * /
  44. * public function cat(InputInterface $input)
  45. * {
  46. * $data = $this->stdin()->select($input, 'file')->contents();
  47. * }
  48. *
  49. * It is also possible to inject the selected stream into the input object,
  50. * e.g. if you want the contents of the source file to be fed to any Question
  51. * helper et. al. that the $input object is used with.
  52. *
  53. * /**
  54. * * @command example
  55. * * @option string $file
  56. * * @default $file -
  57. * * /
  58. * public function example(InputInterface $input)
  59. * {
  60. * $this->stdin()->setStream($input, 'file');
  61. * }
  62. *
  63. *
  64. * Inject an alternate source for standard input in tests. Presumes that
  65. * the object under test gets a reference to the StdinHandler via dependency
  66. * injection from the container.
  67. *
  68. * $container->get('stdinHandler')->redirect($pathToTestStdinFileFixture);
  69. *
  70. * You may also inject your stdin file fixture stream into the $input object
  71. * as usual, and then use it with 'select()' or 'setStream()' as shown above.
  72. *
  73. * Finally, this class may also be used in absence of a dependency injection
  74. * container by using the static 'selectStream()' method:
  75. *
  76. * /**
  77. * * @command example
  78. * * @option string $file
  79. * * @default $file -
  80. * * /
  81. * public function example(InputInterface $input)
  82. * {
  83. * $data = StdinHandler::selectStream($input, 'file')->contents();
  84. * }
  85. *
  86. * To test a method that uses this technique, simply inject your stdin
  87. * fixture into the $input object in your test:
  88. *
  89. * $input->setStream(fopen($pathToFixture, 'r'));
  90. */
  91. class StdinHandler
  92. {
  93. protected $path;
  94. protected $stream;
  95. public static function selectStream(InputInterface $input, $optionOrArg)
  96. {
  97. $handler = new self();
  98. return $handler->setStream($input, $optionOrArg);
  99. }
  100. /**
  101. * hasPath returns 'true' if the stdin handler has a path to a file.
  102. *
  103. * @return bool
  104. */
  105. public function hasPath()
  106. {
  107. // Once the stream has been opened, we mask the existence of the path.
  108. return !$this->hasStream() && !empty($this->path);
  109. }
  110. /**
  111. * hasStream returns 'true' if the stdin handler has opened a stream.
  112. *
  113. * @return bool
  114. */
  115. public function hasStream()
  116. {
  117. return !empty($this->stream);
  118. }
  119. /**
  120. * path returns the path to any file that was set as a redirection
  121. * source, or `php://stdin` if none have been.
  122. *
  123. * @return string
  124. */
  125. public function path()
  126. {
  127. return $this->path ?: 'php://stdin';
  128. }
  129. /**
  130. * close closes the input stream if it was opened.
  131. */
  132. public function close()
  133. {
  134. if ($this->hasStream()) {
  135. fclose($this->stream);
  136. $this->stream = null;
  137. }
  138. return $this;
  139. }
  140. /**
  141. * redirect specifies a path to a file that should serve as the
  142. * source to read from. If the input path is '-' or empty,
  143. * then output will be taken from php://stdin (or whichever source
  144. * was provided via the 'redirect' method).
  145. *
  146. * @return $this
  147. */
  148. public function redirect($path)
  149. {
  150. if ($this->pathProvided($path)) {
  151. $this->path = $path;
  152. }
  153. return $this;
  154. }
  155. /**
  156. * select chooses the source of the input stream based on whether or
  157. * not the user provided the specified option or argument on the commandline.
  158. * Stdin is selected if there is no user selection.
  159. *
  160. * @param InputInterface $input
  161. * @param string $optionOrArg
  162. * @return $this
  163. */
  164. public function select(InputInterface $input, $optionOrArg)
  165. {
  166. $this->redirect($this->getOptionOrArg($input, $optionOrArg));
  167. if (!$this->hasPath() && ($input instanceof StreamableInputInterface)) {
  168. $this->stream = $input->getStream();
  169. }
  170. return $this;
  171. }
  172. /**
  173. * getStream opens and returns the stdin stream (or redirect file).
  174. */
  175. public function getStream()
  176. {
  177. if (!$this->hasStream()) {
  178. $this->stream = fopen($this->path(), 'r');
  179. }
  180. return $this->stream;
  181. }
  182. /**
  183. * setStream functions like 'select', and also sets up the $input
  184. * object to read from the selected input stream e.g. when used
  185. * with a question helper.
  186. */
  187. public function setStream(InputInterface $input, $optionOrArg)
  188. {
  189. $this->select($input, $optionOrArg);
  190. if ($input instanceof StreamableInputInterface) {
  191. $stream = $this->getStream();
  192. $input->setStream($stream);
  193. }
  194. return $this;
  195. }
  196. /**
  197. * contents reads the entire contents of the standard input stream.
  198. *
  199. * @return string
  200. */
  201. public function contents()
  202. {
  203. // Optimization: use file_get_contents if we have a path to a file
  204. // and the stream has not been opened yet.
  205. if (!$this->hasStream()) {
  206. return file_get_contents($this->path());
  207. }
  208. $stream = $this->getStream();
  209. stream_set_blocking($stream, false); // TODO: We did this in backend invoke. Necessary here?
  210. $contents = stream_get_contents($stream);
  211. $this->close();
  212. return $contents;
  213. }
  214. /**
  215. * Returns 'true' if a path was specfied, and that path was not '-'.
  216. */
  217. protected function pathProvided($path)
  218. {
  219. return !empty($path) && ($path != '-');
  220. }
  221. protected function getOptionOrArg(InputInterface $input, $optionOrArg)
  222. {
  223. if ($input->hasOption($optionOrArg)) {
  224. return $input->getOption($optionOrArg);
  225. }
  226. return $input->getArgument($optionOrArg);
  227. }
  228. }