Skip to content

Use temp stream instead of string to buffer content #52

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

Merged
merged 9 commits into from
Dec 23, 2020
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 88 additions & 16 deletions src/MultipartStreamBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ class MultipartStreamBuilder
*/
private $data = [];

/**
* @var int Bytes of unallocated memory to use for stream buffer.
* To have MultipartStreamBuilder manage it automatically, set to -1.
* Default: -1
*/
private $bufferMaxMemory = -1;

/**
* @param HttplugStreamFactory|StreamFactoryInterface|null $streamFactory
*/
Expand Down Expand Up @@ -73,12 +80,10 @@ public function __construct($streamFactory = null)
}

/**
* Add a resource to the Multipart Stream
* Add a resource to the Multipart Stream.
*
* @param string|resource|\Psr\Http\Message\StreamInterface $resource
* The filepath, resource or StreamInterface of the data.
* @param array $headers
* Additional headers array: ['header-name' => 'header-value'].
* @param string|resource|\Psr\Http\Message\StreamInterface $resource the filepath, resource or StreamInterface of the data
* @param array $headers additional headers array: ['header-name' => 'header-value']
*
* @return MultipartStreamBuilder
*/
Expand Down Expand Up @@ -133,28 +138,52 @@ public function addResource($name, $resource, array $options = [])
*/
public function build()
{
$streams = '';
// Assign maximimum 1/4 php's available memory
// to attempt buffering the stream content.
// If the stream content exceed this, will fallback
// to use temporary file.
$maxmemory = ($this->bufferMaxMemory < 0)
? \floor(static::getAvailableMemory() / 4)
: $this->bufferMaxMemory;

// Open a temporary read-write stream as buffer.
// If the size is less than predefined limit, things will stay in memory.
// If the size is more than that, things will be stored in temp file.
$buffer = fopen('php://temp/maxmemory:'.$maxmemory, 'r+');
foreach ($this->data as $data) {
// Add start and headers
$streams .= "--{$this->getBoundary()}\r\n".
$this->getHeaders($data['headers'])."\r\n";
fwrite($buffer, "--{$this->getBoundary()}\r\n".
$this->getHeaders($data['headers'])."\r\n");

// Convert the stream to string
/* @var $contentStream StreamInterface */
/**
* @var \Psr\Http\Message\StreamInterface
*/
$contentStream = $data['contents'];

// Read stream into buffer
if ($contentStream->isSeekable()) {
$streams .= $contentStream->__toString();
$contentStream->rewind(); // rewind to beginning.
}
if ($contentStream->isReadable()) {
while (!$contentStream->eof()) {
// read 8KB chunk into buffer until reached EOF.
fwrite($buffer, $contentStream->read(8192));
}
} else {
$streams .= $contentStream->getContents();
// Try to getContents for non-readable stream.
// Less controllable chunk size (thus memory usage).
fwrite($buffer, $contentStream->getContents());
}

$streams .= "\r\n";
fwrite($buffer, "\r\n");
}

// Append end
$streams .= "--{$this->getBoundary()}--\r\n";
fwrite($buffer, "--{$this->getBoundary()}--\r\n");

return $this->createStream($streams);
// Rewind to starting position for reading.
fseek($buffer, 0);

return $this->createStream($buffer);
}

/**
Expand Down Expand Up @@ -342,4 +371,47 @@ private function createStream($resource)

throw new \InvalidArgumentException(sprintf('First argument to "%s::createStream()" must be a string, resource or StreamInterface.', __CLASS__));
}

/**
* Setup the stream buffer size limit. PHP will allocate buffer
* in memory if the size of the stream is smaller than this size.
* Otherwise, PHP will store the stream data in a temporary file.
*
* @param int $size size of stream data buffered (in bytes)
* until using temporary file to buffer
*/
public function setBufferMaxMemory(int $size): MultipartStreamBuilder
{
$this->bufferMaxMemory = $size;

return $this;
}

/**
* Estimate the available memory in the system by php.ini memory_limit
* and memory_get_usage(). If memory_limit is "-1", the default estimation
* would be 100MB.
*
* @throws \Exception if the ini format does not match expectation
*/
protected static function getAvailableMemory(): int
{
$memory_limit = ini_get('memory_limit');
if ('-1' === $memory_limit) {
// If there is no memory limit, return 100MB by default.
return 100 * 1024 * 1024;
}
if (!preg_match('/^(\d+)(G|M|K|)$/', $memory_limit, $matches)) {
throw new \Exception("Unknown memory_limit format: {$memory_limit}");
}
if ('G' === $matches[2]) {
$memory_limit = $matches[1] * 1024 * 1024 * 1024; // nnnG -> nnn GB
} elseif ('M' === $matches[2]) {
$memory_limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB
} elseif ('K' === $matches[2]) {
$memory_limit = $matches[1] * 1024; // nnnK -> nnn KB
}

return (int) $memory_limit - \memory_get_usage();
}
}