Completed
Push — master ( 78df82...1c3363 )
by David
03:54
created

MultipartStreamBuilder::addData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

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