Completed
Push — master ( 4a59c1...67c0c5 )
by Tobias
02:44
created

MultipartStreamBuilder::createStream()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 10
cts 10
cp 1
rs 9.2728
c 0
b 0
f 0
cc 5
nc 5
nop 1
crap 5
1
<?php
2
3
namespace Http\Message\MultipartStream;
4
5
use Http\Discovery\Exception\NotFoundException;
6
use Http\Discovery\Psr17FactoryDiscovery;
7
use Http\Discovery\StreamFactoryDiscovery;
8
use Http\Message\StreamFactory as HttplugStreamFactory;
9
use Psr\Http\Message\StreamFactoryInterface;
10
use Psr\Http\Message\StreamInterface;
11
12
/**
13
 * Build your own Multipart stream. A Multipart stream is a collection of streams separated with a $bounary. This
14
 * class helps you to create a Multipart stream with stream implementations from any PSR7 library.
15
 *
16
 * @author Michael Dowling and contributors to guzzlehttp/psr7
17
 * @author Tobias Nyholm <[email protected]>
18
 */
19
class MultipartStreamBuilder
20
{
21
    /**
22
     * @var StreamFactory|StreamFactoryInterface
23
     */
24
    private $streamFactory;
25
26
    /**
27
     * @var MimetypeHelper
28
     */
29
    private $mimetypeHelper;
30
31
    /**
32
     * @var string
33
     */
34
    private $boundary;
35
36
    /**
37
     * @var array Element where each Element is an array with keys ['contents', 'headers', 'filename']
38
     */
39
    private $data = [];
40
41
    /**
42
     * @param StreamFactory|StreamFactoryInterface|null $streamFactory
43
     */
44 15
    public function __construct($streamFactory = null)
45
    {
46 15
        if ($streamFactory instanceof StreamFactoryInterface || $streamFactory instanceof HttplugStreamFactory) {
47 2
            $this->streamFactory = $streamFactory;
48
49 2
            return;
50
        }
51
52 13
        if (null !== $streamFactory) {
53 1
            throw new \LogicException(sprintf(
54 1
                'First arguemnt to the constructor of "%s" must be of type "%s", "%s" or null. Got %s',
55 1
                __CLASS__,
56 1
                StreamFactoryInterface::class,
57 1
                HttplugStreamFactory::class,
58 1
                \is_object($streamFactory) ? \get_class($streamFactory) : \gettype($streamFactory)
59
            ));
60
        }
61
62
        // Try to find a stream factory.
63
        try {
64 12
            $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory();
65
        } catch (NotFoundException $psr17Exception) {
66
            try {
67
                $this->streamFactory = StreamFactoryDiscovery::find();
0 ignored issues
show
Documentation Bug introduced by
It seems like \Http\Discovery\StreamFactoryDiscovery::find() of type object<Http\Message\StreamFactory> is incompatible with the declared type object<Http\Message\Mult...StreamFactoryInterface> of property $streamFactory.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
68
            } catch (NotFoundException $httplugException) {
69
                // we could not find any factory.
70
                throw $psr17Exception;
71
            }
72
        }
73 12
    }
74
75
    /**
76
     * Add a resource to the Multipart Stream.
77
     *
78
     * @param string                          $name     the formpost name
79
     * @param string|resource|StreamInterface $resource
80
     * @param array                           $options  {
81
     *
82
     *     @var array $headers additional headers ['header-name' => 'header-value']
83
     *     @var string $filename
84
     * }
85
     *
86
     * @return MultipartStreamBuilder
87
     */
88 14
    public function addResource($name, $resource, array $options = [])
89
    {
90 14
        $stream = $this->createStream($resource);
91
92
        // validate options['headers'] exists
93 13
        if (!isset($options['headers'])) {
94 12
            $options['headers'] = [];
95
        }
96
97
        // Try to add filename if it is missing
98 13
        if (empty($options['filename'])) {
99 12
            $options['filename'] = null;
100 12
            $uri = $stream->getMetadata('uri');
101 12
            if ('php://' !== substr($uri, 0, 6)) {
102 5
                $options['filename'] = $uri;
103
            }
104
        }
105
106 13
        $this->prepareHeaders($name, $stream, $options['filename'], $options['headers']);
107 13
        $this->data[] = ['contents' => $stream, 'headers' => $options['headers'], 'filename' => $options['filename']];
108
109 13
        return $this;
110
    }
111
112
    /**
113
     * Build the stream.
114
     *
115
     * @return StreamInterface
116
     */
117 13
    public function build()
118
    {
119 13
        $streams = '';
120 13
        foreach ($this->data as $data) {
121
            // Add start and headers
122 12
            $streams .= "--{$this->getBoundary()}\r\n".
123 12
                $this->getHeaders($data['headers'])."\r\n";
124
125
            // Convert the stream to string
126
            /* @var $contentStream StreamInterface */
127 12
            $contentStream = $data['contents'];
128 12
            if ($contentStream->isSeekable()) {
129 11
                $streams .= $contentStream->__toString();
130
            } else {
131 1
                $streams .= $contentStream->getContents();
132
            }
133
134 12
            $streams .= "\r\n";
135
        }
136
137
        // Append end
138 13
        $streams .= "--{$this->getBoundary()}--\r\n";
139
140 13
        return $this->createStream($streams);
141
    }
142
143
    /**
144
     * Add extra headers if they are missing.
145
     *
146
     * @param string          $name
147
     * @param StreamInterface $stream
148
     * @param string          $filename
149
     * @param array           &$headers
150
     */
151 13
    private function prepareHeaders($name, StreamInterface $stream, $filename, array &$headers)
152
    {
153 13
        $hasFilename = '0' === $filename || $filename;
154
155
        // Set a default content-disposition header if one was not provided
156 13
        if (!$this->hasHeader($headers, 'content-disposition')) {
157 12
            $headers['Content-Disposition'] = sprintf('form-data; name="%s"', $name);
158 12
            if ($hasFilename) {
159 6
                $headers['Content-Disposition'] .= sprintf('; filename="%s"', $this->basename($filename));
160
            }
161
        }
162
163
        // Set a default content-length header if one was not provided
164 13
        if (!$this->hasHeader($headers, 'content-length')) {
165 12
            if ($length = $stream->getSize()) {
166 11
                $headers['Content-Length'] = (string) $length;
167
            }
168
        }
169
170
        // Set a default Content-Type if one was not provided
171 13
        if (!$this->hasHeader($headers, 'content-type') && $hasFilename) {
172 6
            if ($type = $this->getMimetypeHelper()->getMimetypeFromFilename($filename)) {
173 6
                $headers['Content-Type'] = $type;
174
            }
175
        }
176 13
    }
177
178
    /**
179
     * Get the headers formatted for the HTTP message.
180
     *
181
     * @param array $headers
182
     *
183
     * @return string
184
     */
185 12
    private function getHeaders(array $headers)
186
    {
187 12
        $str = '';
188 12
        foreach ($headers as $key => $value) {
189 12
            $str .= sprintf("%s: %s\r\n", $key, $value);
190
        }
191
192 12
        return $str;
193
    }
194
195
    /**
196
     * Check if header exist.
197
     *
198
     * @param array  $headers
199
     * @param string $key     case insensitive
200
     *
201
     * @return bool
202
     */
203 13
    private function hasHeader(array $headers, $key)
204
    {
205 13
        $lowercaseHeader = strtolower($key);
206 13
        foreach ($headers as $k => $v) {
207 13
            if (strtolower($k) === $lowercaseHeader) {
208 1
                return true;
209
            }
210
        }
211
212 12
        return false;
213
    }
214
215
    /**
216
     * Get the boundary that separates the streams.
217
     *
218
     * @return string
219
     */
220 13
    public function getBoundary()
221
    {
222 13
        if (null === $this->boundary) {
223 12
            $this->boundary = uniqid('', true);
224
        }
225
226 13
        return $this->boundary;
227
    }
228
229
    /**
230
     * @param string $boundary
231
     *
232
     * @return MultipartStreamBuilder
233
     */
234 2
    public function setBoundary($boundary)
235
    {
236 2
        $this->boundary = $boundary;
237
238 2
        return $this;
239
    }
240
241
    /**
242
     * @return MimetypeHelper
243
     */
244 6
    private function getMimetypeHelper()
245
    {
246 6
        if (null === $this->mimetypeHelper) {
247 6
            $this->mimetypeHelper = new ApacheMimetypeHelper();
248
        }
249
250 6
        return $this->mimetypeHelper;
251
    }
252
253
    /**
254
     * If you have custom file extension you may overwrite the default MimetypeHelper with your own.
255
     *
256
     * @param MimetypeHelper $mimetypeHelper
257
     *
258
     * @return MultipartStreamBuilder
259
     */
260
    public function setMimetypeHelper(MimetypeHelper $mimetypeHelper)
261
    {
262
        $this->mimetypeHelper = $mimetypeHelper;
263
264
        return $this;
265
    }
266
267
    /**
268
     * Reset and clear all stored data. This allows you to use builder for a subsequent request.
269
     *
270
     * @return MultipartStreamBuilder
271
     */
272 1
    public function reset()
273
    {
274 1
        $this->data = [];
275 1
        $this->boundary = null;
276
277 1
        return $this;
278
    }
279
280
    /**
281
     * Gets the filename from a given path.
282
     *
283
     * PHP's basename() does not properly support streams or filenames beginning with a non-US-ASCII character.
284
     *
285
     * @author Drupal 8.2
286
     *
287
     * @param string $path
288
     *
289
     * @return string
290
     */
291 6
    private function basename($path)
292
    {
293 6
        $separators = '/';
294 6
        if (DIRECTORY_SEPARATOR != '/') {
295
            // For Windows OS add special separator.
296
            $separators .= DIRECTORY_SEPARATOR;
297
        }
298
299
        // Remove right-most slashes when $path points to directory.
300 6
        $path = rtrim($path, $separators);
301
302
        // Returns the trailing part of the $path starting after one of the directory separators.
303 6
        $filename = preg_match('@[^'.preg_quote($separators, '@').']+$@', $path, $matches) ? $matches[0] : '';
304
305 6
        return $filename;
306
    }
307
308
    /**
309
     * @param string|resource|StreamInterface $resource
310
     *
311
     * @return StreamInterface
312
     */
313 14
    private function createStream($resource)
314
    {
315 14
        if ($resource instanceof StreamInterface) {
316 1
            return $resource;
317
        }
318
319 14
        if ($this->streamFactory instanceof HttplugStreamFactory) {
320 1
            return $this->streamFactory->createStream($resource);
321
        }
322
323
        // Assert: We are using a PSR17 stream factory.
324 13
        if (\is_string($resource)) {
325 12
            return $this->streamFactory->createStream($resource);
326
        }
327
328 6
        if (\is_resource($resource)) {
329 5
            return $this->streamFactory->createStreamFromResource($resource);
330
        }
331
332 1
        throw new \InvalidArgumentException(sprintf('First argument to "%s::createStream()" must be a string, resource or StreamInterface.', __CLASS__));
333
    }
334
}
335