Completed
Push — master ( d8dc03...af03e2 )
by Tobias
03:25
created

MultipartStreamBuilder::addResource()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

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