Completed
Push — master ( f633e0...593386 )
by Tobias
02:18
created

src/UploadedFile.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Nyholm\Psr7;
6
7
use InvalidArgumentException;
8
use Nyholm\Psr7\Factory\StreamFactory;
9
use Psr\Http\Message\StreamInterface;
10
use Psr\Http\Message\UploadedFileInterface;
11
use RuntimeException;
12
13
/**
14
 * @author Michael Dowling and contributors to guzzlehttp/psr7
15
 * @author Tobias Nyholm <[email protected]>
16
 */
17
class UploadedFile implements UploadedFileInterface
18
{
19
    /** @var int[] */
20
    private static $errors = [
21
        UPLOAD_ERR_OK, UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE,
22
        UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION,
23
    ];
24
25
    /** @var string */
26
    private $clientFilename;
27
28
    /** @var string */
29
    private $clientMediaType;
30
31
    /** @var int */
32
    private $error;
33
34
    /** @var null|string */
35
    private $file;
36
37
    /** @var bool */
38
    private $moved = false;
39
40
    /** @var int */
41
    private $size;
42
43
    /** @var StreamInterface|null */
44
    private $stream;
45
46
    /**
47
     * @param StreamInterface|string|resource $streamOrFile
48
     * @param int                             $size
49
     * @param int                             $errorStatus
50
     * @param string|null                     $clientFilename
51
     * @param string|null                     $clientMediaType
52
     */
53 70
    public function __construct(
54
        $streamOrFile,
55
        $size,
56
        $errorStatus,
57
        $clientFilename = null,
58
        $clientMediaType = null
59
    ) {
60 70
        $this->setError($errorStatus);
61 61
        $this->setSize($size);
62 57
        $this->setClientFilename($clientFilename);
63 51
        $this->setClientMediaType($clientMediaType);
64
65 45
        if ($this->isOk()) {
66 23
            $this->setStreamOrFile($streamOrFile);
67
        }
68 38
    }
69
70
    /**
71
     * Depending on the value set file or stream variable.
72
     *
73
     * @param mixed $streamOrFile
74
     *
75
     * @throws InvalidArgumentException
76
     */
77 23
    private function setStreamOrFile($streamOrFile)
78
    {
79 23
        if (is_string($streamOrFile)) {
80 2
            $this->file = $streamOrFile;
81 21
        } elseif (is_resource($streamOrFile)) {
82 2
            $this->stream = Stream::createFromResource($streamOrFile);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Nyholm\Psr7\Stream::cre...Resource($streamOrFile) of type object<self> is incompatible with the declared type object<Psr\Http\Message\StreamInterface>|null of property $stream.

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...
83 19
        } elseif ($streamOrFile instanceof StreamInterface) {
84 12
            $this->stream = $streamOrFile;
85
        } else {
86 7
            throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile');
87
        }
88 16
    }
89
90
    /**
91
     * @param int $error
92
     *
93
     * @throws InvalidArgumentException
94
     */
95 70
    private function setError($error)
96
    {
97 70
        if (false === is_int($error)) {
98 7
            throw new InvalidArgumentException('Upload file error status must be an integer');
99
        }
100
101 63
        if (false === in_array($error, self::$errors)) {
102 2
            throw new InvalidArgumentException('Invalid error status for UploadedFile');
103
        }
104
105 61
        $this->error = $error;
106 61
    }
107
108
    /**
109
     * @param int $size
110
     *
111
     * @throws InvalidArgumentException
112
     */
113 61
    private function setSize($size)
114
    {
115 61
        if (false === is_int($size)) {
116 4
            throw new InvalidArgumentException('Upload file size must be an integer');
117
        }
118
119 57
        $this->size = $size;
120 57
    }
121
122
    /**
123
     * @param mixed $param
124
     *
125
     * @return bool
126
     */
127 57
    private function isStringOrNull($param)
128
    {
129 57
        return in_array(gettype($param), ['string', 'NULL']);
130
    }
131
132
    /**
133
     * @param mixed $param
134
     *
135
     * @return bool
136
     */
137 12
    private function isStringNotEmpty($param)
138
    {
139 12
        return is_string($param) && false === empty($param);
140
    }
141
142
    /**
143
     * @param string|null $clientFilename
144
     *
145
     * @throws InvalidArgumentException
146
     */
147 57
    private function setClientFilename($clientFilename)
148
    {
149 57
        if (false === $this->isStringOrNull($clientFilename)) {
150 6
            throw new InvalidArgumentException('Upload file client filename must be a string or null');
151
        }
152
153 51
        $this->clientFilename = $clientFilename;
154 51
    }
155
156
    /**
157
     * @param string|null $clientMediaType
158
     *
159
     * @throws InvalidArgumentException
160
     */
161 51
    private function setClientMediaType($clientMediaType)
162
    {
163 51
        if (false === $this->isStringOrNull($clientMediaType)) {
164 6
            throw new InvalidArgumentException('Upload file client media type must be a string or null');
165
        }
166
167 45
        $this->clientMediaType = $clientMediaType;
168 45
    }
169
170
    /**
171
     * @return bool Return true if there is no upload error.
172
     */
173 45
    private function isOk()
174
    {
175 45
        return UPLOAD_ERR_OK === $this->error;
176
    }
177
178
    /**
179
     * @throws RuntimeException if is moved or not ok
180
     */
181 30
    private function validateActive()
182
    {
183 30
        if (false === $this->isOk()) {
184 14
            throw new RuntimeException('Cannot retrieve stream due to upload error');
185
        }
186
187 16
        if ($this->moved) {
188 2
            throw new RuntimeException('Cannot retrieve stream after it has already been moved');
189
        }
190 16
    }
191
192 14
    public function getStream()
193
    {
194 14
        $this->validateActive();
195
196 7
        if ($this->stream instanceof StreamInterface) {
197 6
            return $this->stream;
198
        }
199
200 1
        $resource = fopen($this->file, 'r');
201
202 1
        return Stream::createFromResource($resource);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Nyholm\Psr7\Stre...romResource($resource); (self) is incompatible with the return type declared by the interface Psr\Http\Message\UploadedFileInterface::getStream of type Psr\Http\Message\StreamInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
203
    }
204
205 19
    public function moveTo($targetPath)
206
    {
207 19
        $this->validateActive();
208
209 12
        if (false === $this->isStringNotEmpty($targetPath)) {
210 8
            throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
211
        }
212
213 4
        if (null !== $this->file) {
214 1
            $this->moved = 'cli' == php_sapi_name()
215 1
                ? rename($this->file, $targetPath)
216 1
                : move_uploaded_file($this->file, $targetPath);
217
        } else {
218 3
            $stream = $this->getStream();
219 3
            if ($stream->isSeekable()) {
220 3
                $stream->rewind();
221
            }
222 3
            (new StreamFactory())->copyToStream(
223 3
                $stream,
224 3
                Stream::createFromResource(fopen($targetPath, 'w'))
225
            );
226
227 3
            $this->moved = true;
228
        }
229
230 4
        if (false === $this->moved) {
231
            throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath));
232
        }
233 4
    }
234
235 3
    public function getSize()
236
    {
237 3
        return $this->size;
238
    }
239
240 10
    public function getError()
241
    {
242 10
        return $this->error;
243
    }
244
245 3
    public function getClientFilename()
246
    {
247 3
        return $this->clientFilename;
248
    }
249
250 3
    public function getClientMediaType()
251
    {
252 3
        return $this->clientMediaType;
253
    }
254
}
255