Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move the code from pr#455 #525

Merged
merged 2 commits into from Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# v1.0.14 - TBD

## Added

[#525](https://github.com/hyperf-cloud/hyperf/pull/525) Added `download()` method of `Hyperf\HttpServer\Contract\ResponseInterface`.

## Changed

- [#482](https://github.com/hyperf-cloud/hyperf/pull/482) Re-generate the `fillable` argument of Model when use `refresh-fillable` option, at the same time, the command will keep the `fillable` argument as default behaviours.
Expand Down
37 changes: 32 additions & 5 deletions doc/zh/response.md
Expand Up @@ -78,11 +78,11 @@ class IndexController

`redirect` 方法:

| 参数 | 类型 | 默认值 | 备注 |
|:-------------------:|:------:|:---------------:|:------------------:|
| toUrl | string | | 如果参数不存在 `http://` 或 `https://` 则根据当前服务的 Host 自动拼接对应的 URL,且根据 `$schema` 参数拼接协议 |
| status | int | 302 | 响应状态码 |
| schema | string | http | 当 `$toUrl` 不存在 `http://` 或 `https://` 时生效,仅可传递 `http` 或 `https` |
| 参数 | 类型 | 默认值 | 备注 |
|:------:|:------:|:------:|:--------------------------------------------------------------------------------------------------------------:|
| toUrl | string | 无 | 如果参数不存在 `http://` 或 `https://` 则根据当前服务的 Host 自动拼接对应的 URL,且根据 `$schema` 参数拼接协议 |
| status | int | 302 | 响应状态码 |
| schema | string | http | 当 `$toUrl` 不存在 `http://` 或 `https://` 时生效,仅可传递 `http` 或 `https` |

```php
<?php
Expand Down Expand Up @@ -126,3 +126,30 @@ class IndexController
## 分块传输编码 Chunk

## 返回文件下载

`Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `download(string $file, string $name = '')` 返回一个已设置下载文件状态的 `Psr7ResponseInterface` 对象。
如果请求中带有 `if-match` 或 `if-none-match` 的请求头,Hyperf 也会跟根据协议标准与 `ETag` 进行比较,如果一致则会返回一个 `304` 状态码的响应。

`download` 方法:

| 参数 | 类型 | 默认值 | 备注 |
|:----:|:------:|:------:|:-------------------------------------------------------------------:|
| file | string | 无 | 要返回下载文件的绝对路径,同通过 BASE_PATH 常量来定位到项目的根目录 |
| name | string | 无 | 客户端下载文件的文件名,为空则会使用下载文件的原名 |


```php
<?php
namespace App\Controller;

use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;

class IndexController
{
public function index(ResponseInterface $response): Psr7ResponseInterface
{
return $response->download(BASE_PATH . '/public/file.csv', 'filename.csv');
}
}
```
8 changes: 6 additions & 2 deletions src/http-message/src/Server/Response.php
Expand Up @@ -13,6 +13,7 @@
namespace Hyperf\HttpMessage\Server;

use Hyperf\HttpMessage\Cookie\Cookie;
use Hyperf\HttpMessage\Stream\FileInterface;
use Hyperf\HttpMessage\Stream\SwooleStream;

class Response extends \Hyperf\HttpMessage\Base\Response
Expand Down Expand Up @@ -45,8 +46,11 @@ public function send()
}

$this->buildSwooleResponse($this->swooleResponse, $this);

$this->swooleResponse->end($this->getBody()->getContents());
$content = $this->getBody();
if ($content instanceof FileInterface) {
return $this->swooleResponse->sendfile($content->getFilename());
}
$this->swooleResponse->end($content->getContents());
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/http-message/src/Stream/FileInterface.php
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/

namespace Hyperf\HttpMessage\Stream;

interface FileInterface
{
public function getFilename(): string;
}
240 changes: 240 additions & 0 deletions src/http-message/src/Stream/SwooleFileStream.php
@@ -0,0 +1,240 @@
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/

namespace Hyperf\HttpMessage\Stream;

use Hyperf\HttpServer\Exception\Http\FileException;
use Psr\Http\Message\StreamInterface;

class SwooleFileStream implements StreamInterface, FileInterface
{
/**
* @var string
*/
protected $size;

/**
* @var \SplFileInfo
*/
protected $file;

/**
* SwooleFileStream constructor.
*
* @param string|\SplFileInfo $file
*/
public function __construct($file)
{
if (! $file instanceof \SplFileInfo) {
$file = new \SplFileInfo($file);
}
if (! $file->isReadable()) {
throw new FileException('File must be readable.');
}
$this->file = $file;
$this->size = $file->getSize();
}

/**
* Reads all data from the stream into a string, from the beginning to end.
* This method MUST attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
* Warning: This could attempt to load a large amount of data into memory.
* This method MUST NOT raise an exception in order to conform with PHP's
* string casting operations.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString()
{
try {
return $this->getContents();
} catch (\Throwable $e) {
return '';
}
}

/**
* Closes the stream and any underlying resources.
*/
public function close()
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Separates any underlying resources from the stream.
* After the stream has been detached, the stream is in an unusable state.
*
* @return null|resource Underlying PHP stream, if any
*/
public function detach()
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Get the size of the stream if known.
*
* @return null|int returns the size in bytes if known, or null if unknown
*/
public function getSize()
{
if (! $this->size) {
$this->size = filesize($this->getContents());
}
return $this->size;
}

/**
* Returns the current position of the file read/write pointer.
*
* @throws \RuntimeException on error
* @return int Position of the file pointer
*/
public function tell()
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof()
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable()
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Seek to a position in the stream.
*
* @see http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @throws \RuntimeException on failure
*/
public function seek($offset, $whence = SEEK_SET)
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Seek to the beginning of the stream.
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @throws \RuntimeException on failure
* @see http://www.php.net/manual/en/function.fseek.php
* @see seek()
*/
public function rewind()
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable()
{
return false;
}

/**
* Write data to the stream.
*
* @param string $string the string that is to be written
* @throws \RuntimeException on failure
* @return int returns the number of bytes written to the stream
*/
public function write($string)
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable()
{
return true;
}

/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @throws \RuntimeException if an error occurs
* @return string returns the data read from the stream, or an empty string
* if no bytes are available
*/
public function read($length)
{
throw new \BadMethodCallException('Not implemented');
}

/**
* Returns the remaining contents in a string.
*
* @throws \RuntimeException if unable to read or an error occurs while
* reading
* @return string
*/
public function getContents()
{
return $this->getFilename();
}

/**
* Get stream metadata as an associative array or retrieve a specific key.
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @see http://php.net/manual/en/function.stream-get-meta-data.php
* @param string $key specific metadata to retrieve
* @return null|array|mixed Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata($key = null)
{
throw new \BadMethodCallException('Not implemented');
}

public function getFilename(): string
{
return $this->file->getPathname();
}
}
54 changes: 54 additions & 0 deletions src/http-message/tests/SwooleStreamTest.php
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/

namespace HyperfTest\HttpMessage;

use Hyperf\HttpMessage\Server\Response;
use Hyperf\HttpMessage\Stream\SwooleFileStream;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Mockery;
use PHPUnit\Framework\TestCase;
use Swoole\Http\Response as SwooleResponse;

/**
* @internal
* @coversNothing
*/
class SwooleStreamTest extends TestCase
{
public function testSwooleFileStream()
{
$swooleResponse = Mockery::mock(SwooleResponse::class);
$file = __FILE__;
$swooleResponse->shouldReceive('sendfile')->with($file)->once()->andReturn(null);
$swooleResponse->shouldReceive('status')->with(Mockery::any())->once()->andReturn(200);

$response = new Response($swooleResponse);
$response = $response->withBody(new SwooleFileStream($file));

$this->assertSame(null, $response->send());
}

public function testSwooleStream()
{
$swooleResponse = Mockery::mock(SwooleResponse::class);
$content = '{"id":1}';
$swooleResponse->shouldReceive('end')->with($content)->once()->andReturn(null);
$swooleResponse->shouldReceive('status')->with(Mockery::any())->once()->andReturn(200);
$swooleResponse->shouldReceive('header')->with('TOKEN', 'xxx')->once()->andReturn(null);

$response = new Response($swooleResponse);
$response = $response->withBody(new SwooleStream($content))->withHeader('TOKEN', 'xxx');

$this->assertSame(null, $response->send());
}
}