Response.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\HttpServer;
  12. use BadMethodCallException;
  13. use Hyperf\Codec\Json;
  14. use Hyperf\Codec\Xml;
  15. use Hyperf\Context\ApplicationContext;
  16. use Hyperf\Context\Context;
  17. use Hyperf\Context\RequestContext;
  18. use Hyperf\Context\ResponseContext;
  19. use Hyperf\Contract\Arrayable;
  20. use Hyperf\Contract\Jsonable;
  21. use Hyperf\Contract\Xmlable;
  22. use Hyperf\HttpMessage\Cookie\Cookie;
  23. use Hyperf\HttpMessage\Server\Chunk\Chunkable;
  24. use Hyperf\HttpMessage\Stream\SwooleFileStream;
  25. use Hyperf\HttpMessage\Stream\SwooleStream;
  26. use Hyperf\HttpServer\Contract\ResponseInterface;
  27. use Hyperf\HttpServer\Exception\Http\EncodingException;
  28. use Hyperf\HttpServer\Exception\Http\FileException;
  29. use Hyperf\Macroable\Macroable;
  30. use Hyperf\Stringable\Str;
  31. use Hyperf\Support\ClearStatCache;
  32. use Hyperf\Support\MimeTypeExtensionGuesser;
  33. use InvalidArgumentException;
  34. use Psr\Http\Message\MessageInterface;
  35. use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
  36. use Psr\Http\Message\StreamInterface;
  37. use SplFileInfo;
  38. use Stringable;
  39. use Swow\Psr7\Message\ResponsePlusInterface;
  40. use Throwable;
  41. use function get_class;
  42. use function Hyperf\Support\value;
  43. class Response implements PsrResponseInterface, ResponseInterface
  44. {
  45. use Macroable;
  46. public function __construct(protected ?ResponsePlusInterface $response = null)
  47. {
  48. }
  49. public function __call($method, $parameters)
  50. {
  51. $response = $this->getResponse();
  52. if (! method_exists($response, $method)) {
  53. throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_class($this), $method));
  54. }
  55. return $response->{$method}(...$parameters);
  56. }
  57. public static function __callStatic($method, $parameters)
  58. {
  59. $response = Context::get(PsrResponseInterface::class);
  60. if (! method_exists($response, $method)) {
  61. throw new BadMethodCallException(sprintf('Call to undefined static method %s::%s()', self::class, $method));
  62. }
  63. return $response::{$method}(...$parameters);
  64. }
  65. /**
  66. * Format data to JSON and return data with Content-Type:application/json header.
  67. *
  68. * @param array|Arrayable|Jsonable $data
  69. */
  70. public function json($data): PsrResponseInterface
  71. {
  72. $data = $this->toJson($data);
  73. return $this->getResponse()
  74. ->addHeader('content-type', 'application/json; charset=utf-8')
  75. ->setBody(new SwooleStream($data));
  76. }
  77. /**
  78. * Format data to XML and return data with Content-Type:application/xml header.
  79. *
  80. * @param array|Arrayable|Xmlable $data
  81. */
  82. public function xml($data, string $root = 'root', string $charset = 'utf-8'): PsrResponseInterface
  83. {
  84. $data = $this->toXml($data, null, $root);
  85. return $this->getResponse()
  86. ->addHeader('content-type', 'application/xml; charset=' . $charset)
  87. ->setBody(new SwooleStream($data));
  88. }
  89. /**
  90. * return data with content-type:text/html header.
  91. */
  92. public function html(string $html, string $charset = 'utf-8'): PsrResponseInterface
  93. {
  94. return $this->getResponse()
  95. ->withAddedHeader('content-type', 'text/html; charset=' . $charset)
  96. ->withBody(new SwooleStream($html));
  97. }
  98. /**
  99. * Format data to a string and return data with content-type:text/plain header.
  100. *
  101. * @param mixed|Stringable $data will transfer to a string value
  102. */
  103. public function raw($data, string $charset = 'utf-8'): PsrResponseInterface
  104. {
  105. return $this->getResponse()
  106. ->addHeader('content-type', 'text/plain; charset=' . $charset)
  107. ->setBody(new SwooleStream((string) $data));
  108. }
  109. /**
  110. * Redirect to an url with a status.
  111. */
  112. public function redirect(
  113. string $toUrl,
  114. int $status = 302,
  115. string $schema = 'http'
  116. ): PsrResponseInterface {
  117. $toUrl = value(function () use ($toUrl, $schema) {
  118. if (Str::startsWith($toUrl, ['http://', 'https://'])) {
  119. return $toUrl;
  120. }
  121. $host = RequestContext::get()->getUri()->getAuthority();
  122. // Build the url by $schema and host.
  123. return $schema . '://' . $host . (Str::startsWith($toUrl, '/') ? $toUrl : '/' . $toUrl);
  124. });
  125. return $this->getResponse()->withStatus($status)->withAddedHeader('Location', $toUrl);
  126. }
  127. /**
  128. * Create a file download response.
  129. *
  130. * @param string $file the file path which want to send to client
  131. * @param string $name the alias name of the file that client receive
  132. */
  133. public function download(string $file, string $name = ''): PsrResponseInterface
  134. {
  135. $file = new SplFileInfo($file);
  136. if (! $file->isReadable()) {
  137. throw new FileException('File must be readable.');
  138. }
  139. $filename = $name ?: $file->getBasename();
  140. $etag = $this->createEtag($file);
  141. $contentType = value(function () use ($file) {
  142. $mineType = null;
  143. if (ApplicationContext::hasContainer()) {
  144. $guesser = ApplicationContext::getContainer()->get(MimeTypeExtensionGuesser::class);
  145. $mineType = $guesser->guessMimeType($file->getExtension());
  146. }
  147. return $mineType ?? 'application/octet-stream';
  148. });
  149. // Determine if ETag the client expects matches calculated ETag
  150. $request = RequestContext::get();
  151. $ifMatch = $request->getHeaderLine('if-match');
  152. $ifNoneMatch = $request->getHeaderLine('if-none-match');
  153. $clientEtags = explode(',', $ifMatch ?: $ifNoneMatch);
  154. /* @phpstan-ignore-next-line */
  155. array_walk($clientEtags, 'trim');
  156. if (in_array($etag, $clientEtags, true)) {
  157. return $this->getResponse()->setStatus(304)->addHeader('content-type', $contentType);
  158. }
  159. return $this->getResponse()->setHeader('content-description', 'File Transfer')
  160. ->setHeader('content-type', $contentType)
  161. ->setHeader('content-disposition', "attachment; filename={$filename}; filename*=UTF-8''" . rawurlencode($filename))
  162. ->setHeader('content-transfer-encoding', 'binary')
  163. ->setHeader('pragma', 'public')
  164. ->setHeader('etag', $etag)
  165. ->setBody(new SwooleFileStream($file));
  166. }
  167. public function withCookie(Cookie $cookie): ResponseInterface
  168. {
  169. return $this->call(__FUNCTION__, func_get_args());
  170. }
  171. /**
  172. * Retrieves the HTTP protocol version as a string.
  173. * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
  174. *
  175. * @return string HTTP protocol version
  176. */
  177. public function getProtocolVersion(): string
  178. {
  179. return $this->getResponse()->getProtocolVersion();
  180. }
  181. /**
  182. * Return an instance with the specified HTTP protocol version.
  183. * The version string MUST contain only the HTTP version number (e.g.,
  184. * "1.1", "1.0").
  185. * This method MUST be implemented in such a way as to retain the
  186. * immutability of the message, and MUST return an instance that has the
  187. * new protocol version.
  188. *
  189. * @param string $version HTTP protocol version
  190. * @return PsrResponseInterface
  191. */
  192. public function withProtocolVersion($version): MessageInterface
  193. {
  194. return $this->call(__FUNCTION__, func_get_args());
  195. }
  196. /**
  197. * Retrieves all message header values.
  198. * The keys represent the header name as it will be sent over the wire, and
  199. * each value is an array of strings associated with the header.
  200. * // Represent the headers as a string
  201. * foreach ($message->getHeaders() as $name => $values) {
  202. * echo $name . ": " . implode(", ", $values);
  203. * }
  204. * // Emit headers iteratively:
  205. * foreach ($message->getHeaders() as $name => $values) {
  206. * foreach ($values as $value) {
  207. * header(sprintf('%s: %s', $name, $value), false);
  208. * }
  209. * }
  210. * While header names are not case-sensitive, getHeaders() will preserve the
  211. * exact case in which headers were originally specified.
  212. *
  213. * @return string[][] Returns an associative array of the message's headers. Each
  214. * key MUST be a header name, and each value MUST be an array of strings
  215. * for that header.
  216. */
  217. public function getHeaders(): array
  218. {
  219. return $this->getResponse()->getHeaders();
  220. }
  221. /**
  222. * Checks if a header exists by the given case-insensitive name.
  223. *
  224. * @param string $name case-insensitive header field name
  225. * @return bool Returns true if any header names match the given header
  226. * name using a case-insensitive string comparison. Returns false if
  227. * no matching header name is found in the message.
  228. */
  229. public function hasHeader($name): bool
  230. {
  231. return $this->getResponse()->hasHeader($name);
  232. }
  233. /**
  234. * Retrieves a message header value by the given case-insensitive name.
  235. * This method returns an array of all the header values of the given
  236. * case-insensitive header name.
  237. * If the header does not appear in the message, this method MUST return an
  238. * empty array.
  239. *
  240. * @param string $name case-insensitive header field name
  241. * @return string[] An array of string values as provided for the given
  242. * header. If the header does not appear in the message, this method MUST
  243. * return an empty array.
  244. */
  245. public function getHeader($name): array
  246. {
  247. return $this->getResponse()->getHeader($name);
  248. }
  249. /**
  250. * Retrieves a comma-separated string of the values for a single header.
  251. * This method returns all the header values of the given
  252. * case-insensitive header name as a string concatenated together using
  253. * a comma.
  254. * NOTE: Not all header values may be appropriately represented using
  255. * comma concatenation. For such headers, use getHeader() instead
  256. * and supply your own delimiter when concatenating.
  257. * If the header does not appear in the message, this method MUST return
  258. * an empty string.
  259. *
  260. * @param string $name case-insensitive header field name
  261. * @return string A string of values as provided for the given header
  262. * concatenated together using a comma. If the header does not appear in
  263. * the message, this method MUST return an empty string.
  264. */
  265. public function getHeaderLine($name): string
  266. {
  267. return $this->getResponse()->getHeaderLine($name);
  268. }
  269. /**
  270. * Return an instance with the provided value replacing the specified header.
  271. * While header names are case-insensitive, the casing of the header will
  272. * be preserved by this function, and returned from getHeaders().
  273. * This method MUST be implemented in such a way as to retain the
  274. * immutability of the message, and MUST return an instance that has the
  275. * new and/or updated header and value.
  276. *
  277. * @param string $name case-insensitive header field name
  278. * @param string|string[] $value header value(s)
  279. * @return PsrResponseInterface
  280. * @throws InvalidArgumentException for invalid header names or values
  281. */
  282. public function withHeader($name, $value): MessageInterface
  283. {
  284. return $this->call(__FUNCTION__, func_get_args());
  285. }
  286. /**
  287. * Return an instance with the specified header appended with the given value.
  288. * Existing values for the specified header will be maintained. The new
  289. * value(s) will be appended to the existing list. If the header did not
  290. * exist previously, it will be added.
  291. * This method MUST be implemented in such a way as to retain the
  292. * immutability of the message, and MUST return an instance that has the
  293. * new header and/or value.
  294. *
  295. * @param string $name case-insensitive header field name to add
  296. * @param string|string[] $value header value(s)
  297. * @return PsrResponseInterface
  298. * @throws InvalidArgumentException for invalid header names or values
  299. */
  300. public function withAddedHeader($name, $value): MessageInterface
  301. {
  302. return $this->call(__FUNCTION__, func_get_args());
  303. }
  304. /**
  305. * Return an instance without the specified header.
  306. * Header resolution MUST be done without case-sensitivity.
  307. * This method MUST be implemented in such a way as to retain the
  308. * immutability of the message, and MUST return an instance that removes
  309. * the named header.
  310. *
  311. * @param string $name case-insensitive header field name to remove
  312. * @return PsrResponseInterface
  313. */
  314. public function withoutHeader($name): MessageInterface
  315. {
  316. return $this->call(__FUNCTION__, func_get_args());
  317. }
  318. /**
  319. * Gets the body of the message.
  320. *
  321. * @return StreamInterface returns the body as a stream
  322. */
  323. public function getBody(): StreamInterface
  324. {
  325. return $this->getResponse()->getBody();
  326. }
  327. /**
  328. * Return an instance with the specified message body.
  329. * The body MUST be a StreamInterface object.
  330. * This method MUST be implemented in such a way as to retain the
  331. * immutability of the message, and MUST return a new instance that has the
  332. * new body stream.
  333. *
  334. * @param StreamInterface $body body
  335. * @return PsrResponseInterface
  336. * @throws InvalidArgumentException when the body is not valid
  337. */
  338. public function withBody(StreamInterface $body): MessageInterface
  339. {
  340. return $this->call(__FUNCTION__, func_get_args());
  341. }
  342. /**
  343. * Gets the response status code.
  344. * The status code is a 3-digit integer result code of the server's attempt
  345. * to understand and satisfy the request.
  346. *
  347. * @return int status code
  348. */
  349. public function getStatusCode(): int
  350. {
  351. return $this->getResponse()->getStatusCode();
  352. }
  353. /**
  354. * Return an instance with the specified status code and, optionally, reason phrase.
  355. * If no reason phrase is specified, implementations MAY choose to default
  356. * to the RFC 7231 or IANA recommended reason phrase for the response's
  357. * status code.
  358. * This method MUST be implemented in such a way as to retain the
  359. * immutability of the message, and MUST return an instance that has the
  360. * updated status and reason phrase.
  361. *
  362. * @see http://tools.ietf.org/html/rfc7231#section-6
  363. * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
  364. * @param int $code the 3-digit integer result code to set
  365. * @param string $reasonPhrase the reason phrase to use with the
  366. * provided status code; if none is provided, implementations MAY
  367. * use the defaults as suggested in the HTTP specification
  368. * @throws InvalidArgumentException for invalid status code arguments
  369. */
  370. public function withStatus($code, $reasonPhrase = ''): PsrResponseInterface
  371. {
  372. return $this->call(__FUNCTION__, func_get_args());
  373. }
  374. /**
  375. * Gets the response reason phrase associated with the status code.
  376. * Because a reason phrase is not a required element in a response
  377. * status line, the reason phrase value MAY be null. Implementations MAY
  378. * choose to return the default RFC 7231 recommended reason phrase (or those
  379. * listed in the IANA HTTP Status Code Registry) for the response's
  380. * status code.
  381. *
  382. * @see http://tools.ietf.org/html/rfc7231#section-6
  383. * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
  384. * @return string reason phrase; must return an empty string if none present
  385. */
  386. public function getReasonPhrase(): string
  387. {
  388. return $this->getResponse()->getReasonPhrase();
  389. }
  390. public function write(string $data): bool
  391. {
  392. $response = $this->getResponse();
  393. if ($response instanceof Chunkable) {
  394. return $response->write($data);
  395. }
  396. return false;
  397. }
  398. protected function call($name, $arguments)
  399. {
  400. $response = $this->getResponse();
  401. if (! method_exists($response, $name)) {
  402. throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_class($this), $name));
  403. }
  404. return new static($response->{$name}(...$arguments));
  405. }
  406. /**
  407. * Get ETag header according to the checksum of the file.
  408. */
  409. protected function createEtag(SplFileInfo $file, bool $weak = false): string
  410. {
  411. $etag = '';
  412. if ($weak) {
  413. ClearStatCache::clear($file->getPathname());
  414. $lastModified = $file->getMTime();
  415. $filesize = $file->getSize();
  416. if (! $lastModified || ! $filesize) {
  417. return $etag;
  418. }
  419. $etag = sprintf('W/"%x-%x"', $lastModified, $filesize);
  420. } else {
  421. $etag = md5_file($file->getPathname());
  422. }
  423. return $etag;
  424. }
  425. /**
  426. * @param array|Arrayable|Jsonable $data
  427. * @throws EncodingException when the data encoding error
  428. */
  429. protected function toJson($data): string
  430. {
  431. try {
  432. $result = Json::encode($data);
  433. } catch (Throwable $exception) {
  434. throw new EncodingException($exception->getMessage(), (int) $exception->getCode(), $exception);
  435. }
  436. return $result;
  437. }
  438. /**
  439. * @param array|Arrayable|Xmlable $data
  440. * @param null|mixed $parentNode
  441. * @param mixed $root
  442. * @throws EncodingException when the data encoding error
  443. */
  444. protected function toXml($data, $parentNode = null, $root = 'root'): string
  445. {
  446. return Xml::toXml($data, $parentNode, $root);
  447. }
  448. /**
  449. * Get the response object from context.
  450. *
  451. * @return ResponsePlusInterface it's an object that implemented PsrResponseInterface, or maybe it's a proxy class
  452. */
  453. protected function getResponse(): ResponsePlusInterface
  454. {
  455. if ($this->response instanceof ResponsePlusInterface) {
  456. return $this->response;
  457. }
  458. return ResponseContext::get();
  459. }
  460. }