/lib/Phluid/Http/Response.php

https://bitbucket.org/beaucollins/phluid-php · PHP · 283 lines · 196 code · 46 blank · 41 comment · 22 complexity · 5cef6c623052f389445ae45898800447 MD5 · raw file

  1. <?php
  2. namespace Phluid\Http;
  3. use Evenement\EventEmitter;
  4. use React\Socket\ConnectionInterface;
  5. use React\Stream\WritableStreamInterface;
  6. use React\Stream\ReadableStreamInterface;
  7. use Phluid\Utils;
  8. use Phluid\View;
  9. class Response extends EventEmitter implements WritableStreamInterface {
  10. private $conn;
  11. private $closed = false;
  12. private $writable = true;
  13. private $headWritten;
  14. private $chunkedEncoding = true;
  15. private $options;
  16. private $request;
  17. public $status = 200;
  18. private $headers = array();
  19. function __construct( ConnectionInterface $conn, Request $request ){
  20. $this->request = $request;
  21. $this->conn = $conn;
  22. $this->conn->on('end', function () {
  23. $this->close();
  24. });
  25. $this->conn->on('error', function ($error) {
  26. $this->emit('error', array($error, $this));
  27. $this->close();
  28. });
  29. $this->conn->on('drain', function () {
  30. $this->emit('drain');
  31. });
  32. $this->options = array(
  33. 'default_layout' => null,
  34. 'view_path' => null
  35. );
  36. }
  37. public function __toString(){
  38. return (string) $this->status;
  39. }
  40. public function getOptions(){
  41. return $this->options;
  42. }
  43. public function setOptions( array $options ){
  44. $this->options = array_merge( $this->options, $options );
  45. }
  46. /**
  47. * Reteurn an associative array of all HTTP headers
  48. *
  49. * @return (array) header name and value pairs
  50. * @author Beau Collins
  51. */
  52. public function getHeaders(){
  53. $headers = $this->headers;
  54. return $headers;
  55. }
  56. public function setHeaders( $headers ){
  57. foreach ( $headers as $name => $value ) {
  58. $this->setHeader( $name, $value );
  59. }
  60. }
  61. public function getStatus(){
  62. return $this->status;
  63. }
  64. /**
  65. * Set an HTTP response header
  66. *
  67. * @param string $key header name
  68. * @param string $value header value
  69. * @return void
  70. * @author Beau Collins
  71. */
  72. public function setHeader( $key, $value ){
  73. $this->headers[trim( strtoupper( $key ) )] = $value;
  74. }
  75. public function getHeader( $key ){
  76. $key = strtoupper( $key );
  77. if ( array_key_exists( $key, $this->headers ) ) {
  78. return $this->headers[$key];
  79. }
  80. }
  81. public function redirectTo( $path, $status = 302 ){
  82. $this->setHeader( 'location', $path );
  83. $this->sendHeaders( $status );
  84. $this->end();
  85. }
  86. public function render( $template, $locals = array(), $options = array() ){
  87. $layout = Utils::array_val( $options, 'layout', $this->options['default_layout'] );
  88. $status = Utils::array_val( $options, 'status', 200 );
  89. $content_type = Utils::array_val($options, 'content-type', 'text/html' );
  90. $locals['request'] = $this->request;
  91. $view = new View( $template, $layout, $this->options['view_path'] );
  92. $this->renderString( $view->render( $locals ), $content_type, $status );
  93. }
  94. public function renderString( $string, $content_type="text/plain", $status = 200 ){
  95. $this->status_code = $status;
  96. $this->setHeader( 'Content-Type', $content_type );
  97. $this->setHeader( 'Content-Length', strlen( (string) $string ) );
  98. // write the headers and the body
  99. $this->sendHeaders( $status );
  100. $this->end( (string) $string );
  101. }
  102. /**
  103. * Alias of renderString
  104. *
  105. * @param string $string text to respond with
  106. * @param string $content_type content type for HTTP header
  107. * @param int $status HTTP status code to use
  108. * @return void
  109. * @author Beau Collins
  110. */
  111. public function renderText( $string, $content_type="text/plain", $status = 200 ){
  112. $this->renderString( $string, $content_type, $status );
  113. }
  114. /**
  115. * Renders the given object as a string encoded with json_encode and given
  116. * application/json as the content-type
  117. *
  118. * @param string $object
  119. * @param int $status HTTP status to send
  120. * @return void
  121. * @author Beau Collins
  122. */
  123. public function renderJSON( $object, $status = 200 ){
  124. $this->renderString( json_encode($object), "application/json" );
  125. }
  126. public function sendFile( $path, $options_or_status = array(), $status = 200 ){
  127. // TODO: handle if a file doesn't exist or isn't readable
  128. if ( is_int( $options_or_status )) {
  129. $status = $options_or_status;
  130. $options = array();
  131. } else {
  132. $options = $options_or_status;
  133. }
  134. if ( array_key_exists( 'attachment', $options ) ) {
  135. $disposition = $options['attachment'];
  136. if( $disposition === true ){
  137. $disposition = "attachment;";
  138. } else {
  139. $disposition = "attachment; filename=\"$disposition\"";
  140. }
  141. $this->setHeader( 'Content-Disposition', $disposition );
  142. }
  143. $this->setHeader( 'Content-Length', filesize( $path ) );
  144. $this->sendHeaders( $status );
  145. if( $handle = fopen( $path, 'r' ) ){
  146. $readFile = function() use ( $handle ){
  147. while( $string = fread( $handle, $this->conn->bufferSize ) ){
  148. if ( feof( $handle ) ) {
  149. fclose( $handle );
  150. $this->end( $string );
  151. return;
  152. } else {
  153. if( !$this->write( $string ) ) return;
  154. }
  155. }
  156. };
  157. $this->on( 'drain', $readFile );
  158. $readFile();
  159. }
  160. }
  161. public function sendHeaders( $status_or_headers = 200, $headers = array() ){
  162. if ( !is_null( $status_or_headers ) and is_int( $status_or_headers ) ) {
  163. $this->status = $status_or_headers;
  164. } else if ( !is_null( $status_or_headers ) && is_array( $status_or_headers ) ) {
  165. $headers = $status_or_headers;
  166. }
  167. $this->setHeaders( $headers );
  168. $this->writeHead( $this->status, $this->headers );
  169. }
  170. public function writeHead( $status = 200, $headers = array() ){
  171. if ( $this->headWritten ) {
  172. throw new \Exception("Response head has already been written");
  173. }
  174. $this->emit( 'headers' );
  175. $this->conn->write( $this->statusHeader( $status ) . "\r\n" );
  176. $this->eachHeader( function( $name, $value ){
  177. $this->conn->write( "$name: $value" . "\r\n" );
  178. });
  179. $this->conn->write( "\r\n" );
  180. if ( $this->getHeader( 'Content-Length' ) ) {
  181. $this->chunkedEncoding = false;
  182. }
  183. $this->headWritten = true;
  184. }
  185. public function statusHeader( $status = 200 ){
  186. return "HTTP/1.1 " . $status;
  187. }
  188. /**
  189. * Iterate throuch each header name/value with a callback
  190. *
  191. * @param string $callback that accepts to arguments
  192. * @return void
  193. * @author Beau Collins
  194. */
  195. public function eachHeader( $callback ){
  196. foreach( $this->headers as $name => $value ){
  197. $callback( $name, $value );
  198. }
  199. }
  200. public function isWritable() {
  201. return $this->writable;
  202. }
  203. public function write( $data ) {
  204. if ( !$this->headWritten ) {
  205. throw new \Exception( 'Response head has not yet been written.' );
  206. }
  207. if ( $this->chunkedEncoding ) {
  208. $len = strlen( $data );
  209. $chunk = dechex( $len ) . "\r\n" . $data . "\r\n";
  210. $flushed = $this->conn->write( $chunk );
  211. } else {
  212. $flushed = $this->conn->write( $data );
  213. }
  214. return $flushed;
  215. }
  216. public function end( $data = null ) {
  217. if ( null !== $data ) {
  218. $this->write( $data );
  219. }
  220. if ( $this->chunkedEncoding ) {
  221. $this->conn->write( "0\r\n\r\n" );
  222. }
  223. $this->emit( 'end' );
  224. $this->removeAllListeners();
  225. $this->conn->end();
  226. }
  227. public function close() {
  228. if ( $this->closed ) {
  229. return;
  230. }
  231. $this->closed = true;
  232. $this->writable = false;
  233. $this->emit( 'close' );
  234. $this->removeAllListeners();
  235. $this->conn->close();
  236. }
  237. }