UploadFileBag::getErrorMessage()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 1
nop 0
1
<?php
2
/*
3
 * This file is part of the Borobudur-Http package.
4
 *
5
 * (c) Hexacodelabs <http://hexacodelabs.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Borobudur\Http;
12
13
use Borobudur\Http\Exception\InvalidArgumentException;
14
use Borobudur\Http\Exception\RuntimeException;
15
use Borobudur\Http\File\AcceptedMimeType;
16
use Borobudur\Http\File\FileTrait;
17
use Borobudur\Http\File\UploadedFile;
18
use Closure;
19
20
/**
21
 * @author      Iqbal Maulana <[email protected]>
22
 * @created     8/2/15
23
 */
24
class UploadFileBag extends ParameterBag
25
{
26
    use FileTrait;
27
28
    /**
29
     * Global accepted file mime types.
30
     *
31
     * @var AcceptedMimeType
32
     */
33
    public static $acceptedMimeTypes;
34
35
    /**
36
     * Upload file keys mapping.
37
     *
38
     * @var array
39
     */
40
    private static $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
41
42
    /**
43
     * @var bool
44
     */
45
    private $tested;
46
47
    /**
48
     * Constructor.
49
     *
50
     * @param array $files
51
     * @param bool  $tested
52
     */
53
    public function __construct(array $files = array(), $tested = false)
54
    {
55
        if (null === self::$acceptedMimeTypes) {
56
            self::$acceptedMimeTypes = new AcceptedMimeType();
57
        }
58
59
        $this->tested = (bool) $tested;
60
        parent::__construct($files);
61
    }
62
63
    /**
64
     * Clear mime types and replace with sets of it.
65
     *
66
     * @param array $mimeTypes
67
     *
68
     * @return $this
69
     */
70
    public function replaceAcceptedMimeTypes(array $mimeTypes)
71
    {
72
        self::$acceptedMimeTypes->clear();
73
        $this->walkIn(function (UploadedFile &$file) {
74
            $file->getAcceptedMimeType()->clear();
75
        });
76
77
        return $this->addAcceptedMimeTypes($mimeTypes);
78
    }
79
80
    /**
81
     * Add sets of mime type.
82
     *
83
     * @param array $mimeTypes
84
     *
85
     * @return $this
86
     */
87
    public function addAcceptedMimeTypes(array $mimeTypes)
88
    {
89
        foreach ($mimeTypes as $mimeType) {
90
            $this->setAcceptedMimeType($mimeType);
91
        }
92
93
        return $this;
94
    }
95
96
    /**
97
     * Set mime type to all children.
98
     *
99
     * @param string $mimeType
100
     *
101
     * @return $this
102
     */
103
    public function setAcceptedMimeType($mimeType)
104
    {
105
        $mimeType = strtolower(trim($mimeType));
106
        self::$acceptedMimeTypes->set($mimeType);
107
108
        $this->walkIn(function (UploadedFile &$file) use ($mimeType) {
109
            $file->getAcceptedMimeType()->set($mimeType);
110
        });
111
112
        return $this;
113
    }
114
115
    /**
116
     * Get all accepted mime types.
117
     *
118
     * @return array
119
     */
120
    public function allAcceptedMimeTypes()
121
    {
122
        return self::$acceptedMimeTypes->all();
123
    }
124
125
    /**
126
     * Check if specified mime type is accepted.
127
     *
128
     * @param string $mimeType
129
     *
130
     * @return bool
131
     */
132
    public function isMimeTypeAccepted($mimeType)
133
    {
134
        $accepted = self::$acceptedMimeTypes->all();
135
        if (empty($accepted)) {
136
            return true;
137
        }
138
139
        return self::$acceptedMimeTypes->isAccepted($mimeType);
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function set($key, $value)
146
    {
147
        if (false === is_array($value) && false === $value instanceof UploadedFile) {
148
            throw new InvalidArgumentException(sprintf(
149
                'Uploaded file should be an array or instance of "%s", "%s" given.',
150
                'Borobudur\Http\File\UploadedFile',
151
                gettype($value)
152
            ));
153
        }
154
155
        return parent::set($key, $this->convertToFile($value));
156
    }
157
158
    /**
159
     * Check if file is valid to all composite children.
160
     *
161
     * @return bool
162
     */
163
    public function isValid()
164
    {
165
        try {
166
            $this->walkIn(function (UploadedFile $file) {
167
                if (false === $file->isValid()) {
168
                    throw new \Exception;
169
                }
170
            });
171
172
            return true;
173
        } catch (\Exception $e) {
174
            return false;
175
        }
176
    }
177
178
    /**
179
     * Execute closure function to all composite files children.
180
     *
181
     * Files will be deleted if exception.
182
     *
183
     * @param Closure $closure
184
     *
185
     * @return $this
186
     */
187
    public function executeInSafeMode(Closure $closure)
188
    {
189
        $successes = array();
190
191
        $this->walkIn(function (UploadedFile $file = null, $key, $parentKey = null) use (
192
            &$successes,
193
            &$closure,
194
            &$lastKey
195
        ) {
196
            try {
197
                if (null === $file = call_user_func($closure, $file, $key)) {
198
                    self::collectData($successes, $file, $key, $parentKey);
199
                }
200
            } catch (RuntimeException $e) {
201
                // Remove uploaded images if exception
202
                (new static($successes))->deleteAll();
203
                throw $e;
204
            }
205
        });
206
207
        return new static($successes);
208
    }
209
210
    /**
211
     * Move all composite files children to new location.
212
     *
213
     * Return new UploadFileBag with files that uploaded.
214
     *
215
     * @param string $directory New folder location.
216
     *
217
     * @return $this
218
     */
219
    public function move($directory)
220
    {
221
        return $this->createDirectoryAndCall($directory, function (UploadedFile $file) use ($directory) {
222
            return $file->move($directory);
223
        });
224
    }
225
226
    /**
227
     * Copy all composite files children to new location.
228
     *
229
     * Return new UploadedFileBag with files that copied.
230
     *
231
     * @param string $directory
232
     *
233
     * @return $this
234
     */
235
    public function copy($directory)
236
    {
237
        return $this->createDirectoryAndCall($directory, function (UploadedFile $file) use ($directory) {
238
            return $file->copy($directory);
239
        });
240
    }
241
242
    /**
243
     * Remove all composite files children.
244
     *
245
     * @return $this
246
     */
247
    public function deleteAll()
248
    {
249
        $this->walkIn(function (UploadedFile $file) {
250
            $file->remove();
251
        });
252
253
        return $this;
254
    }
255
256
    /**
257
     * Get all composite children error message.
258
     *
259
     * @return array
260
     */
261
    public function getErrorMessage()
262
    {
263
        $messages = array();
264
265
        $this->walkIn(function (UploadedFile $file, $key, $parentKey = null) use (&$messages) {
266
            $msg = $file->getErrorMessage();
267
268
            if (null !== $msg) {
269
                self::collectData($messages, $msg, $key, $parentKey);
270
            }
271
        });
272
273
        return $messages;
274
    }
275
276
    /**
277
     * Walk into all file.
278
     *
279
     * @param Closure $callback
280
     */
281
    public function walkIn(Closure $callback)
282
    {
283
        $this->walkInRecursively($callback, $this->parameters);
284
    }
285
286
    /**
287
     * Convert file information to UploadedFile or sets of it.
288
     *
289
     * @param UploadedFile|array $fileInfo
290
     *
291
     * @return UploadedFile|UploadedFile[]
292
     */
293
    protected function convertToFile($fileInfo)
294
    {
295
        if ($fileInfo instanceof UploadedFile) {
296
            return $fileInfo->setTest($this->tested);
297
        }
298
299
        $fileInfo = $this->normalizeFilesArray($fileInfo);
300
        if (is_array($fileInfo)) {
301
            $keys = array_keys($fileInfo);
302
            sort($keys);
303
304
            if ($keys == self::$fileKeys) {
305
                $fileInfo = $this->createFileFromArray($fileInfo);
306
            } else {
307
                $fileInfo = array_map(array($this, 'convertToFile'), $fileInfo);
308
            }
309
        }
310
311
        return $fileInfo;
312
    }
313
314
    /**
315
     * Create UploadedFile from array file info.
316
     *
317
     * @param array $fileInfo
318
     *
319
     * @return UploadedFile|null
320
     */
321
    protected function createFileFromArray(array $fileInfo)
322
    {
323
        if (UPLOAD_ERR_NO_FILE == $fileInfo['error']) {
324
            return null;
325
        }
326
327
        $fileInfo = UploadedFile::fromArray($fileInfo)->setTest($this->tested);
328
        $fileInfo->getAcceptedMimeType()->replace(self::$acceptedMimeTypes->all());
329
330
        return $fileInfo;
331
    }
332
333
    /**
334
     * Normalize nested $_FILES array.
335
     *
336
     * @param array $fileInfo
337
     *
338
     * @return array
339
     */
340
    protected function normalizeFilesArray(array $fileInfo)
341
    {
342
        if (self::isFileUpload($fileInfo) || !is_array($fileInfo['name'])) {
343
            return $fileInfo;
344
        }
345
346
        $normalized = array();
347
348
        foreach ($fileInfo['name'] as $key => $name) {
349
            $normalized[$key] = $this->normalizeFilesArray(array(
350
                'error'    => $fileInfo['error'][$key],
351
                'name'     => $name,
352
                'type'     => $fileInfo['type'][$key],
353
                'tmp_name' => $fileInfo['tmp_name'][$key],
354
                'size'     => $fileInfo['size'][$key],
355
            ));
356
        }
357
358
        return $normalized;
359
    }
360
361
    /**
362
     * Collecting data.
363
     *
364
     * @param array           $array
365
     * @param mixed           $data
366
     * @param string|int      $key
367
     * @param string|int|null $parentKey
368
     */
369
    private static function collectData(array &$array, $data, $key, $parentKey = null)
370
    {
371
        if (!is_int($key) && !isset($array[$key])) {
372
            $array[$key] = $data;
373
374
            return;
375
        }
376
377
        if (!isset($array[$parentKey])) {
378
            $array[$parentKey] = array($data);
379
380
            return;
381
        }
382
383
        $array[$parentKey][] = $data;
384
    }
385
386
    /**
387
     * Check if specified file is file upload.
388
     *
389
     * @param array $fileInfo
390
     *
391
     * @return bool
392
     */
393
    private static function isFileUpload(array $fileInfo)
394
    {
395
        $keys = array_keys($fileInfo);
396
        sort($keys);
397
398
        return $keys == self::$fileKeys;
399
    }
400
401
    /**
402
     * @param string  $directory
403
     * @param Closure $caller
404
     *
405
     * @return $this
406
     */
407
    private function createDirectoryAndCall($directory, Closure $caller)
408
    {
409
        if ($this->isValid()) {
410
            $this->createDirectory($directory);
411
412
            return $this->executeInSafeMode($caller);
413
        }
414
415
        throw new RuntimeException(implode(".\n", array_map(function ($message) {
416
            if (is_array($message)) {
417
                return implode(".\n", $message);
418
            }
419
420
            return $message;
421
        }, $this->getErrorMessage())));
422
    }
423
424
    /**
425
     * Walk in recursively.
426
     *
427
     * @param Closure    $callback
428
     * @param array      $data
429
     * @param mixed|null $parentKey
430
     */
431
    private function walkInRecursively(Closure $callback, array $data, $parentKey = null)
432
    {
433
        $me = $this;
434
435
        array_walk($data, function ($file, $key) use (&$callback, &$me, $parentKey) {
436
            if (is_array($file)) {
437
                $me->walkInRecursively($callback, $file, is_int($key) ? $parentKey : $key);
438
            }
439
440
            call_user_func($callback, $file, $key, $parentKey);
441
        });
442
    }
443
}
444