Completed
Push — master ( 71b082...d4eb58 )
by smiley
02:29
created

MultipartStream::getBoundary()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 <[email protected]>
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 [
0 ignored issues
show
Documentation introduced by
Should the type for parameter $elements not be null|array? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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
}
206