Completed
Push — master ( cf58c6...5e02c7 )
by David
03:11
created

MultipartStreamBuilder::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Http\Message\MultipartStream;
4
5
use Http\Discovery\StreamFactoryDiscovery;
6
use Http\Message\StreamFactory;
7
use Psr\Http\Message\StreamInterface;
8
9
/**
10
 * Build your own Multipart stream. A Multipart stream is a collection of streams separated with a $bounary. This
11
 * class helps you to create a Multipart stream with stream implementations from any PSR7 library.
12
 *
13
 * @author Michael Dowling and contributors to guzzlehttp/psr7
14
 * @author Tobias Nyholm <[email protected]>
15
 */
16
class MultipartStreamBuilder
17
{
18
    /**
19
     * @var StreamFactory
20
     */
21
    private $streamFactory;
22
23
    /**
24
     * @var MimetypeHelper
25
     */
26
    private $mimetypeHelper;
27
28
    /**
29
     * @var string
30
     */
31
    private $boundary;
32
33
    /**
34
     * @var array Element where each Element is an array with keys ['contents', 'headers', 'filename']
35
     */
36
    private $data = [];
37
38
    /**
39
     * @param StreamFactory|null $streamFactory
40
     */
41 8
    public function __construct(StreamFactory $streamFactory = null)
42
    {
43 8
        $this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
44 8
    }
45
46
    /**
47
     * Add a resource to the Multipart Stream. If the same $name is used twice the first resource will
48
     * be overwritten.
49
     *
50
     * @param string                          $name     the formpost name
51
     * @param string|resource|StreamInterface $resource
52
     * @param array                           $options  {
53
     *
54
     *     @var array $headers additional headers ['header-name' => 'header-value']
55
     *     @var string $filename
56
     * }
57
     *
58
     * @return MultipartStreamBuilder
59
     */
60 8
    public function addResource($name, $resource, array $options = [])
61
    {
62 8
        $stream = $this->streamFactory->createStream($resource);
63
64
        // validate options['headers'] exists
65 8
        if (!isset($options['headers'])) {
66 7
            $options['headers'] = [];
67 7
        }
68
69
        // Try to add filename if it is missing
70 8
        if (empty($options['filename'])) {
71 7
            $options['filename'] = null;
72 7
            $uri = $stream->getMetadata('uri');
73 7
            if (substr($uri, 0, 6) !== 'php://') {
74 1
                $options['filename'] = $uri;
75 1
            }
76 7
        }
77
78 8
        $this->prepareHeaders($name, $stream, $options['filename'], $options['headers']);
79 8
        $this->data[$name] = ['contents' => $stream, 'headers' => $options['headers'], 'filename' => $options['filename']];
80
81 8
        return $this;
82
    }
83
84
    /**
85
     * Build the stream.
86
     *
87
     * @return StreamInterface
88
     */
89 8
    public function build()
90
    {
91 8
        $streams = '';
92 8
        foreach ($this->data as $data) {
93
            // Add start and headers
94 7
            $streams .= "--{$this->getBoundary()}\r\n".
95 7
                $this->getHeaders($data['headers'])."\r\n";
96
97
            // Convert the stream to string
98 7
            $streams .= (string) $data['contents'];
99 7
            $streams .= "\r\n";
100 8
        }
101
102
        // Append end
103 8
        $streams .= "--{$this->getBoundary()}--\r\n";
104
105 8
        return $this->streamFactory->createStream($streams);
106
    }
107
108
    /**
109
     * Add extra headers if they are missing.
110
     *
111
     * @param string          $name
112
     * @param StreamInterface $stream
113
     * @param string          $filename
114
     * @param array           &$headers
115
     */
116 8
    private function prepareHeaders($name, StreamInterface $stream, $filename, array &$headers)
117
    {
118 8
        $hasFilename = $filename === '0' || $filename;
119
120
        // Set a default content-disposition header if one was not provided
121 8
        if (!$this->hasHeader($headers, 'content-disposition')) {
122 7
            $headers['Content-Disposition'] = sprintf('form-data; name="%s"', $name);
123 7
            if ($hasFilename) {
124 2
                $headers['Content-Disposition'] .= sprintf('; filename="%s"', $this->basename($filename));
125 2
            }
126 7
        }
127
128
        // Set a default content-length header if one was not provided
129 8
        if (!$this->hasHeader($headers, 'content-length')) {
130 7
            if ($length = $stream->getSize()) {
131 7
                $headers['Content-Length'] = (string) $length;
132 7
            }
133 7
        }
134
135
        // Set a default Content-Type if one was not provided
136 8
        if (!$this->hasHeader($headers, 'content-type') && $hasFilename) {
137 2
            if ($type = $this->getMimetypeHelper()->getMimetypeFromFilename($filename)) {
138 2
                $headers['Content-Type'] = $type;
139 2
            }
140 2
        }
141 8
    }
142
143
    /**
144
     * Get the headers formatted for the HTTP message.
145
     *
146
     * @param array $headers
147
     *
148
     * @return string
149
     */
150 7
    private function getHeaders(array $headers)
151
    {
152 7
        $str = '';
153 7
        foreach ($headers as $key => $value) {
154 7
            $str .= sprintf("%s: %s\r\n", $key, $value);
155 7
        }
156
157 7
        return $str;
158
    }
159
160
    /**
161
     * Check if header exist.
162
     *
163
     * @param array  $headers
164
     * @param string $key     case insensitive
165
     *
166
     * @return bool
167
     */
168 8
    private function hasHeader(array $headers, $key)
169
    {
170 8
        $lowercaseHeader = strtolower($key);
171 8
        foreach ($headers as $k => $v) {
172 8
            if (strtolower($k) === $lowercaseHeader) {
173 1
                return true;
174
            }
175 8
        }
176
177 7
        return false;
178
    }
179
180
    /**
181
     * Get the boundary that separates the streams.
182
     *
183
     * @return string
184
     */
185 8
    public function getBoundary()
186
    {
187 8
        if ($this->boundary === null) {
188 7
            $this->boundary = uniqid();
189 7
        }
190
191 8
        return $this->boundary;
192
    }
193
194
    /**
195
     * @param string $boundary
196
     *
197
     * @return MultipartStreamBuilder
198
     */
199 2
    public function setBoundary($boundary)
200
    {
201 2
        $this->boundary = $boundary;
202
203 2
        return $this;
204
    }
205
206
    /**
207
     * @return MimetypeHelper
208
     */
209 2
    private function getMimetypeHelper()
210
    {
211 2
        if ($this->mimetypeHelper === null) {
212 2
            $this->mimetypeHelper = new ApacheMimetypeHelper();
213 2
        }
214
215 2
        return $this->mimetypeHelper;
216
    }
217
218
    /**
219
     * If you have custom file extension you may overwrite the default MimetypeHelper with your own.
220
     *
221
     * @param MimetypeHelper $mimetypeHelper
222
     *
223
     * @return MultipartStreamBuilder
224
     */
225
    public function setMimetypeHelper(MimetypeHelper $mimetypeHelper)
226
    {
227
        $this->mimetypeHelper = $mimetypeHelper;
228
229
        return $this;
230
    }
231
232
    /**
233
     * Reset and clear all stored data. This allows you to use builder for a subsequent request.
234
     *
235
     * @return MultipartStreamBuilder
236
     */
237 1
    public function reset()
238
    {
239 1
        $this->data = [];
240 1
        $this->boundary = null;
241
242 1
        return $this;
243
    }
244
245
    /**
246
     * Gets the filename from a given path.
247
     *
248
     * PHP's basename() does not properly support streams or filenames beginning with a non-US-ASCII character.
249
     *
250
     * @author Drupal 8.2
251
     *
252
     * @param string $path
253
     *
254
     * @return string
255
     */
256 2
    private function basename($path)
257
    {
258 2
        $separators = '/';
259 2
        if (DIRECTORY_SEPARATOR != '/') {
260
            // For Windows OS add special separator.
261
            $separators .= DIRECTORY_SEPARATOR;
262
        }
263
264
        // Remove right-most slashes when $path points to directory.
265 2
        $path = rtrim($path, $separators);
266
267
        // Returns the trailing part of the $path starting after one of the directory separators.
268 2
        $filename = preg_match('@[^'.preg_quote($separators, '@').']+$@', $path, $matches) ? $matches[0] : '';
269
270 2
        return $filename;
271
    }
272
}
273