Skip to content

Commit d4eb58a

Browse files
committed
✨ +MultipartStream
1 parent 71b0827 commit d4eb58a

File tree

6 files changed

+677
-69
lines changed

6 files changed

+677
-69
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"psr/log": "^1.0"
3636
},
3737
"require-dev": {
38-
"phpunit/phpunit": "^7.4"
38+
"phpunit/phpunit": "^7.5",
39+
"guzzlehttp/psr7": "^1.5"
3940
},
4041
"autoload": {
4142
"files": [

src/Psr7/MultipartStream.php

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
/**
3+
* Class MultipartStream
4+
*
5+
* @link https://github.com/guzzle/psr7/blob/master/src/MultipartStream.php
6+
*
7+
* @filesource MultipartStream.php
8+
* @created 20.12.2018
9+
* @package chillerlan\HTTP\Psr7
10+
* @author smiley <smiley@chillerlan.net>
11+
* @copyright 2018 smiley
12+
* @license MIT
13+
*/
14+
15+
namespace chillerlan\HTTP\Psr7;
16+
17+
use chillerlan\HTTP\Psr17;
18+
use InvalidArgumentException, RuntimeException;
19+
20+
/**
21+
* @property \chillerlan\HTTP\Psr7\AppendStream $stream
22+
*/
23+
final class MultipartStream extends StreamAbstract{
24+
25+
/**
26+
* @var string
27+
*/
28+
protected $boundary;
29+
30+
/**
31+
* @var \Psr\Http\Message\StreamFactoryInterface
32+
*/
33+
protected $streamFactory;
34+
35+
/**
36+
* @var bool
37+
*/
38+
protected $built = false;
39+
40+
/**
41+
* MultipartStream constructor.
42+
*
43+
* @param array $elements [
44+
* name => string,
45+
* contents => StreamInterface/resource/string,
46+
* headers => array,
47+
* filename => string
48+
* ]
49+
* @param string|null $boundary
50+
*/
51+
public function __construct(array $elements = null, string $boundary = null){
52+
$this->boundary = $boundary ?? sha1(random_bytes(1024));
53+
$this->stream = Psr17\create_stream();
54+
55+
foreach($elements ?? [] as $element){
56+
$this->addElement($element);
57+
}
58+
59+
}
60+
61+
/**
62+
* @return string
63+
*/
64+
public function getBoundary():string{
65+
return $this->boundary;
66+
}
67+
68+
/**
69+
* @return \chillerlan\HTTP\Psr7\MultipartStream
70+
*/
71+
public function build():MultipartStream{
72+
73+
if(!$this->built){
74+
$this->stream->write("--{$this->getBoundary()}--\r\n");
75+
}
76+
77+
$this->stream->rewind();
78+
79+
$this->built = true;
80+
81+
return $this;
82+
}
83+
84+
/**
85+
* @param array $e
86+
*
87+
* @return \chillerlan\HTTP\Psr7\MultipartStream
88+
*/
89+
public function addElement(array $e):MultipartStream{
90+
91+
if($this->built){
92+
throw new RuntimeException('Stream already built');
93+
}
94+
95+
$e = array_merge(['filename' => null, 'headers' => []], $e);
96+
97+
foreach(['contents', 'name'] as $key){
98+
if(!isset($e[$key])){
99+
throw new InvalidArgumentException('A "'.$key.'" element is required');
100+
}
101+
}
102+
103+
$e['contents'] = Psr17\create_stream_from_input($e['contents']);
104+
105+
if(empty($e['filename'])){
106+
$uri = $e['contents']->getMetadata('uri');
107+
108+
if(substr($uri, 0, 6) !== 'php://'){
109+
$e['filename'] = $uri;
110+
}
111+
}
112+
113+
$hasFilename = $e['filename'] === '0' || $e['filename'];
114+
115+
// Set a default content-disposition header if none was provided
116+
if(!$this->hasHeader($e['headers'], 'content-disposition')){
117+
$e['headers']['Content-Disposition'] = 'form-data; name="'.$e['name'].'"'.($hasFilename ? '; filename="'.basename($e['filename']).'"' : '');
118+
}
119+
120+
// Set a default content-length header if none was provided
121+
if(!$this->hasHeader($e['headers'], 'content-length')){
122+
$length = $e['contents']->getSize();
123+
124+
if($length){
125+
$e['headers']['Content-Length'] = $length;
126+
}
127+
}
128+
129+
// Set a default Content-Type if none was supplied
130+
if(!$this->hasHeader($e['headers'], 'content-type') && $hasFilename){
131+
$type = MIMETYPES[pathinfo($e['filename'], PATHINFO_EXTENSION)] ?? null;
132+
133+
if($type){
134+
$e['headers']['Content-Type'] = $type;
135+
}
136+
}
137+
138+
139+
$this->stream->write('--'.$this->boundary."\r\n");
140+
141+
foreach($e['headers'] as $key => $value){
142+
$this->stream->write($key.': '.$value."\r\n");
143+
}
144+
145+
$this->stream->write("\r\n".$e['contents']->getContents()."\r\n");
146+
147+
148+
return $this;
149+
}
150+
151+
152+
private function hasHeader(array $headers, $key){
153+
$lowercaseHeader = strtolower($key);
154+
foreach ($headers as $k => $v) {
155+
if (strtolower($k) === $lowercaseHeader) {
156+
return true;
157+
}
158+
}
159+
160+
return false;
161+
}
162+
163+
/**
164+
* @inheritdoc
165+
*/
166+
public function __toString(){
167+
return $this->getContents();
168+
}
169+
170+
/**
171+
* @inheritdoc
172+
*/
173+
public function getSize():?int{
174+
return $this->stream->getSize() + strlen($this->boundary) + 6;
175+
}
176+
177+
/**
178+
* @inheritdoc
179+
*/
180+
public function isWritable():bool{
181+
return false;
182+
}
183+
184+
/**
185+
* @inheritdoc
186+
*/
187+
public function write($string):int{
188+
throw new RuntimeException('Cannot write to a MultipartStream, use the "addElement" method instead.');
189+
}
190+
191+
/**
192+
* @inheritdoc
193+
*/
194+
public function isReadable():bool{
195+
return true;
196+
}
197+
198+
/**
199+
* @inheritdoc
200+
*/
201+
public function getContents():string{
202+
return $this->build()->stream->getContents();
203+
}
204+
205+
}

src/Psr7/Stream.php

Lines changed: 12 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,12 @@
1212

1313
namespace chillerlan\HTTP\Psr7;
1414

15-
use Psr\Http\Message\StreamInterface;
1615
use Exception, InvalidArgumentException, RuntimeException;
1716

18-
final class Stream implements StreamInterface{
19-
20-
public const MODES_READ = [
21-
'a+' => true,
22-
'c+' => true,
23-
'c+b' => true,
24-
'c+t' => true,
25-
'r' => true,
26-
'r+' => true,
27-
'rb' => true,
28-
'rt' => true,
29-
'r+b' => true,
30-
'r+t' => true,
31-
'w+' => true,
32-
'w+b' => true,
33-
'w+t' => true,
34-
'x+' => true,
35-
'x+b' => true,
36-
'x+t' => true,
37-
];
38-
39-
public const MODES_WRITE = [
40-
'a' => true,
41-
'a+' => true,
42-
'c+' => true,
43-
'c+b' => true,
44-
'c+t' => true,
45-
'r+' => true,
46-
'rw' => true,
47-
'r+b' => true,
48-
'r+t' => true,
49-
'w' => true,
50-
'w+' => true,
51-
'wb' => true,
52-
'w+b' => true,
53-
'w+t' => true,
54-
'x+' => true,
55-
'x+b' => true,
56-
'x+t' => true,
57-
];
58-
59-
/**
60-
* @var resource
61-
*/
62-
private $stream;
17+
/**
18+
* @property resource $stream
19+
*/
20+
final class Stream extends StreamAbstract{
6321

6422
/**
6523
* @var bool
@@ -107,15 +65,6 @@ public function __construct($stream){
10765
$this->uri = $meta['uri'] ?? null;
10866
}
10967

110-
/**
111-
* Closes the stream when the destructed
112-
*
113-
* @return void
114-
*/
115-
public function __destruct(){
116-
$this->close();
117-
}
118-
11968
/**
12069
* @inheritdoc
12170
*/
@@ -126,11 +75,17 @@ public function __toString(){
12675
}
12776

12877
try{
129-
$this->seek(0);
78+
79+
if($this->isSeekable()){
80+
$this->seek(0);
81+
}
13082

13183
return (string)stream_get_contents($this->stream);
13284
}
13385
catch(Exception $e){
86+
// https://bugs.php.net/bug.php?id=53648
87+
trigger_error('StreamDecoratorTrait::__toString exception: '.$e->getMessage(), E_USER_ERROR);
88+
13489
return '';
13590
}
13691

@@ -314,18 +269,7 @@ public function getContents():string{
314269
}
315270

316271
/**
317-
* Get stream metadata as an associative array or retrieve a specific key.
318-
*
319-
* The keys returned are identical to the keys returned from PHP's
320-
* stream_get_meta_data() function.
321-
*
322-
* @link http://php.net/manual/en/function.stream-get-meta-data.php
323-
*
324-
* @param string $key Specific metadata to retrieve.
325-
*
326-
* @return array|mixed|null Returns an associative array if no key is
327-
* provided. Returns a specific key value if a key is provided and the
328-
* value is found, or null if the key is not found.
272+
* @inheritdoc
329273
*/
330274
public function getMetadata($key = null){
331275

0 commit comments

Comments
 (0)