1<?php
2namespace GuzzleHttp\Post;
3
4use GuzzleHttp\Stream\AppendStream;
5use GuzzleHttp\Stream\Stream;
6use GuzzleHttp\Stream\StreamDecoratorTrait;
7use GuzzleHttp\Stream\StreamInterface;
8
9/**
10 * Stream that when read returns bytes for a streaming multipart/form-data body
11 */
12class MultipartBody implements StreamInterface
13{
14    use StreamDecoratorTrait;
15
16    private $boundary;
17
18    /**
19     * @param array  $fields   Associative array of field names to values where
20     *                         each value is a string or array of strings.
21     * @param array  $files    Associative array of PostFileInterface objects
22     * @param string $boundary You can optionally provide a specific boundary
23     * @throws \InvalidArgumentException
24     */
25    public function __construct(
26        array $fields = [],
27        array $files = [],
28        $boundary = null
29    ) {
30        $this->boundary = $boundary ?: uniqid();
31        $this->stream = $this->createStream($fields, $files);
32    }
33
34    /**
35     * Get the boundary
36     *
37     * @return string
38     */
39    public function getBoundary()
40    {
41        return $this->boundary;
42    }
43
44    public function isWritable()
45    {
46        return false;
47    }
48
49    /**
50     * Get the string needed to transfer a POST field
51     */
52    private function getFieldString($name, $value)
53    {
54        return sprintf(
55            "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n",
56            $this->boundary,
57            $name,
58            $value
59        );
60    }
61
62    /**
63     * Get the headers needed before transferring the content of a POST file
64     */
65    private function getFileHeaders(PostFileInterface $file)
66    {
67        $headers = '';
68        foreach ($file->getHeaders() as $key => $value) {
69            $headers .= "{$key}: {$value}\r\n";
70        }
71
72        return "--{$this->boundary}\r\n" . trim($headers) . "\r\n\r\n";
73    }
74
75    /**
76     * Create the aggregate stream that will be used to upload the POST data
77     */
78    protected function createStream(array $fields, array $files)
79    {
80        $stream = new AppendStream();
81
82        foreach ($fields as $name => $fieldValues) {
83            foreach ((array) $fieldValues as $value) {
84                $stream->addStream(
85                    Stream::factory($this->getFieldString($name, $value))
86                );
87            }
88        }
89
90        foreach ($files as $file) {
91
92            if (!$file instanceof PostFileInterface) {
93                throw new \InvalidArgumentException('All POST fields must '
94                    . 'implement PostFieldInterface');
95            }
96
97            $stream->addStream(
98                Stream::factory($this->getFileHeaders($file))
99            );
100            $stream->addStream($file->getContent());
101            $stream->addStream(Stream::factory("\r\n"));
102        }
103
104        // Add the trailing boundary with CRLF
105        $stream->addStream(Stream::factory("--{$this->boundary}--\r\n"));
106
107        return $stream;
108    }
109}
110