Completed
Push — master ( eaccdd...67c865 )
by Lorenzo
02:00
created

Uploadable::uploadFile()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
3
namespace Padosoft\Uploadable;
4
5
use DB;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Http\UploadedFile;
8
use Illuminate\Support\Facades\Log;
9
use Illuminate\Support\Facades\URL;
10
use Padosoft\Io\DirHelper;
11
use Padosoft\Io\FileHelper;
12
use Padosoft\Laravel\Request\RequestHelper;
13
use Padosoft\Laravel\Request\UploadedFileHelper;
14
15
/**
16
 * Class Uploadable
17
 * Auto upload and save files on save/create/delete model.
18
 * @package Padosoft\Uploadable
19
 */
20
trait Uploadable
21
{
22
    /** @var UploadOptions */
23
    protected $uploadOptions;
24
25
    /**
26
     * Boot the trait.
27
     */
28
    public static function bootUploadable()
29
    {
30
        self::bindCreateEvents();
31
        self::bindSaveEvents();
32
        self::bindUpdateEvents();
33
        self::bindDeleteEvents();
34
    }
35
36
    /**
37
     * Bind create model events.
38
     */
39
    protected static function bindCreateEvents()
40
    {
41
        static::creating(function ($model) {
42
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
43
            $model->guardAgainstInvalidUploadOptions();
44
        });
45
    }
46
47
    /**
48
     * Bind save model events.
49
     */
50
    protected static function bindSaveEvents()
51
    {
52
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
53
        static::saving(function (Model $model) {
54
            $model->generateAllNewUploadFileNameAndSetAttribute();
55
        });
56
        */
57
        static::saved(function (Model $model) {
58
            $model->uploadFiles();
59
        });
60
    }
61
62
    /**
63
     * Bind update model events.
64
     */
65
    protected static function bindUpdateEvents()
66
    {
67
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
68
        static::updating(function (Model $model) {
69
            $model->generateAllNewUploadFileNameAndSetAttribute();
70
        });
71
        */
72
        static::updated(function (Model $model) {
0 ignored issues
show
Bug introduced by
The method updated() does not exist on Padosoft\Uploadable\Uploadable. Did you maybe mean updateDb()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
73
            $model->uploadFiles();
74
        });
75
    }
76
77
    /**
78
     * Bind delete model events.
79
     */
80
    protected static function bindDeleteEvents()
81
    {
82
        static::deleting(function (Model $model) {
83
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
84
            $model->guardAgainstInvalidUploadOptions();
85
        });
86
87
        static::deleted(function (Model $model) {
88
            $model->deleteUploadedFiles();
89
        });
90
    }
91
92
    /**
93
     * Retrive a specifice UploadOptions for this model, or return default UploadOptions
94
     * @return UploadOptions
95
     */
96
    public function getUploadOptionsOrDefault() : UploadOptions
97
    {
98
        if ($this->uploadOptions) {
99
            return $this->uploadOptions;
100
        }
101
102
        if (method_exists($this, 'getUploadOptions')) {
103
            $method = 'getUploadOptions';
104
            $this->uploadOptions = $this->{$method}();
105
        } else {
106
            $this->uploadOptions = UploadOptions::create()->getUploadOptionsDefault()
107
                ->setUploadBasePath(public_path('upload/' . $this->getTable()));
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
108
        }
109
110
        return $this->uploadOptions;
111
    }
112
113
    /**
114
     * This function will throw an exception when any of the options is missing or invalid.
115
     * @throws InvalidOption
116
     */
117
    public function guardAgainstInvalidUploadOptions()
118
    {
119
        if (!count($this->uploadOptions->uploads)) {
120
            throw InvalidOption::missingUploadFields();
121
        }
122
        if ($this->uploadOptions->uploadBasePath === null || $this->uploadOptions->uploadBasePath == '') {
123
            throw InvalidOption::missingUploadBasePath();
124
        }
125
    }
126
127
    /**
128
     * Handle file upload.
129
     */
130
    public function uploadFiles()
131
    {
132
        //invalid model
133
        if ($this->id < 1) {
0 ignored issues
show
Bug introduced by
The property id does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
134
            return;
135
        }
136
137
        //loop for every upload model attributes and do upload if has a file in request
138
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
139
            $this->uploadFile($uploadField);
140
        }
141
    }
142
143
    /**
144
     * Upload a file releted to a passed attribute name
145
     * @param string $uploadField
146
     */
147
    public function uploadFile(string $uploadField)
148
    {
149
        //check if there is a valid file in request for current attribute
150
        if (!RequestHelper::isValidCurrentRequestUploadFile($uploadField,
151
            $this->getUploadOptionsOrDefault()->uploadsMimeType)
152
        ) {
153
            return;
154
        }
155
156
        //retrive the uploaded file
157
        $uploadedFile = RequestHelper::getCurrentRequestFileSafe($uploadField);
158
        if ($uploadedFile === null) {
159
            return;
160
        }
161
162
        //calcolate new file name and set it to model attribute
163
        $this->generateNewUploadFileNameAndSetAttribute($uploadField);
164
165
        //do the work
166
        $newName = $this->doUpload($uploadedFile, $uploadField);
167
168
        //save on db (not call model save because invoke event and entering in loop)
169
        $this->updateDb($uploadField, $newName);
170
    }
171
172
    /**
173
     * Get an UploadedFile, generate new name, and save it in destination path.
174
     * Return empty string if it fails, otherwise return the saved file name.
175
     * @param UploadedFile $uploadedFile
176
     * @param string $uploadAttribute
177
     * @return string
178
     */
179
    protected function doUpload(UploadedFile $uploadedFile, $uploadAttribute) : string
180
    {
181
        if (!$uploadedFile || !$uploadAttribute) {
182
            return '';
183
        }
184
185
        //get file name by attribute
186
        $newName = $this->{$uploadAttribute};
187
188
        //get upload path to store (method create dir if not exists and return '' if it failed)
189
        $pathToStore = $this->getUploadFileBasePath($uploadAttribute);
190
191
        //delete if file already exists
192
        FileHelper::unlinkSafe(DirHelper::njoin($pathToStore,$newName));
193
194
        //move file to destination folder
195
        try {
196
            $targetFile = $uploadedFile->move($pathToStore, $newName);
197
        } catch (\Symfony\Component\HttpFoundation\File\Exception\FileException $e) {
198
            $targetFile = null;
199
            Log::warning('Error in doUpload() when try to move ' . $newName . ' to folder: ' . $pathToStore . PHP_EOL . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
200
        }
201
202
        return $targetFile ? $newName : '';
203
    }
204
205
    /**
206
     * Check request for valid files and server for correct paths defined in model
207
     * @return bool
208
     */
209
    public function requestHasValidFilesAndCorrectPaths() : bool
210
    {
211
        //current request has not uploaded files
212
        if (!RequestHelper::currentRequestHasFiles()) {
213
            return false;
214
        }
215
216
        //ensure that all upload path are ok or create it.
217
        if (!$this->checkOrCreateAllUploadBasePaths()) {
218
            return false;
219
        }
220
221
        return true;
222
    }
223
224
    /**
225
     * Generate a new file name for uploaded file.
226
     * Return empty string if uploadedFile is null, otherwise return the new file name..
227
     * @param UploadedFile $uploadedFile
228
     * @return string
229
     * @internal param string $uploadField
230
     */
231
    public function generateNewUploadFileName(UploadedFile $uploadedFile) : string
232
    {
233
        if (!$uploadedFile) {
234
            return '';
235
        }
236
       if (!$this->id && $this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
237
            return '';
238
        }
239
240
        //check if file need a new name
241
        $newName = $this->calcolateNewUploadFileName($uploadedFile);
242
        if ($newName != '') {
243
            return $newName;
244
        }
245
246
        //no new file name, return original file name
247
        return $uploadedFile->getFilename();
248
    }
249
250
    /**
251
     * Check if file need a new name and return it, otherwise return empty string.
252
     * @param UploadedFile $uploadedFile
253
     * @return string
254
     */
255
    public function calcolateNewUploadFileName(UploadedFile $uploadedFile) : string
256
    {
257
        if (!$this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
258
            return '';
259
        }
260
261
        //retrive original file name and extension
262
        $filenameWithoutExtension = UploadedFileHelper::getFilenameWithoutExtension($uploadedFile);
263
        $ext = $uploadedFile->getClientOriginalExtension();
264
265
        $newName = $filenameWithoutExtension . $this->getUploadOptionsOrDefault()->uploadFileNameSuffixSeparator . $this->id . '.' . $ext;
266
        return sanitize_filename($newName);
267
    }
268
269
    /**
270
     * delete all Uploaded Files
271
     */
272
    public function deleteUploadedFiles()
273
    {
274
        //loop for every upload model attributes
275
        foreach ($this->getUploadOptionsOrDefault()->uploads as $uploadField) {
276
            $this->deleteUploadedFile($uploadField);
277
        }
278
    }
279
280
    /**
281
     * Delete upload file related to passed attribute name
282
     * @param string $uploadField
283
     */
284
    public function deleteUploadedFile(string $uploadField)
285
    {
286
        if (!$uploadField) {
287
            return;
288
        }
289
290
        if (!$this->{$uploadField}) {
291
            return;
292
        }
293
294
        //retrive correct upload storage path for current attribute
295
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
296
297
        //unlink file
298
        $path = DirHelper::njoin($uploadFieldPath, $this->{$uploadField});
299
        FileHelper::unlinkSafe($path);
300
301
        //reset model attribute and update db field
302
        $this->setBlanckAttributeAndDB($uploadField);
303
    }
304
305
    /**
306
     * Reset model attribute and update db field
307
     * @param string $uploadField
308
     */
309
    public function setBlanckAttributeAndDB(string $uploadField)
310
    {
311
        if (!$uploadField) {
312
            return;
313
        }
314
315
        //set to black attribute
316
        $this->{$uploadField} = '';
317
318
        //save on db (not call model save because invoke event and entering in loop)
319
        $this->updateDb($uploadField, '');
320
    }
321
322
    /**
323
     * Return true If All Upload atrributes Are Empty or
324
     * if the uploads array is not set.
325
     * @return bool
326
     */
327
    public function checkIfAllUploadFieldsAreEmpty() : bool
328
    {
329
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
330
            //for performance if one attribute has value exit false
331
            if ($uploadField && $this->{$uploadField}) {
332
                return false;
333
            }
334
        }
335
336
        return true;
337
    }
338
339
    /**
340
     * Check all attributes upload path, and try to create dir if not already exists.
341
     * Return false if it fails to create all founded dirs.
342
     * @return bool
343
     */
344
    public function checkOrCreateAllUploadBasePaths() : bool
345
    {
346
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
347
            if (!$this->checkOrCreateUploadBasePath($uploadField)) {
348
                return false;
349
            }
350
        }
351
352
        return true;
353
    }
354
355
    /**
356
     * Check uploads property and return a uploads class field
357
     * or empty array if somethings wrong.
358
     * @return array
359
     */
360
    public function getUploadsAttributesSafe() : array
361
    {
362
        if (!is_array($this->getUploadOptionsOrDefault()->uploads)) {
363
            return [];
364
        }
365
366
        return $this->getUploadOptionsOrDefault()->uploads;
367
    }
368
369
    /**
370
     * Check attribute upload path, and try to create dir if not already exists.
371
     * Return false if it fails to create the dir.
372
     * @param string $uploadField
373
     * @return bool
374
     */
375
    public function checkOrCreateUploadBasePath(string $uploadField) : bool
376
    {
377
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
378
379
        return DirHelper::checkDirExistOrCreate($uploadFieldPath,
380
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask);
381
    }
382
383
    /**
384
     * Return the upload path for the passed attribute and try to create it if not exists.
385
     * Returns empty string if dir if not exists and fails to create it.
386
     * @param string $uploadField
387
     * @return string
388
     */
389
    public function getUploadFileBasePath(string $uploadField) : string
390
    {
391
        //default model upload path
392
        $uploadFieldPath = DirHelper::canonicalize($this->getUploadOptionsOrDefault()->uploadBasePath);
393
394
        //overwrite if there is specific path for the field
395
        $specificPath = $this->getUploadFileBasePathSpecific($uploadField);
396
        if ($specificPath != '') {
397
            $uploadFieldPath = $specificPath;
398
        }
399
400
        //check if exists or try to create dir
401
        if (!DirHelper::checkDirExistOrCreate($uploadFieldPath,
402
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask)
403
        ) {
404
            return '';
405
        }
406
407
        return $uploadFieldPath;
408
    }
409
410
    /**
411
     * Return the specific upload path (by uploadPaths prop) for the passed attribute if exists,
412
     * otherwise return empty string.
413
     * If uploadPaths for this field is relative, a public_path was appended.
414
     * @param string $uploadField
415
     * @return string
416
     */
417
    public function getUploadFileBasePathSpecific(string $uploadField) : string
418
    {
419
        //check if there is a specified upload path
420
        if (empty($this->getUploadOptionsOrDefault()->uploadPaths) || count($this->getUploadOptionsOrDefault()->uploadPaths) < 1
421
            || !array_key_exists($uploadField, $this->getUploadOptionsOrDefault()->uploadPaths)
422
        ) {
423
            return '';
424
        }
425
        $path = $this->getUploadOptionsOrDefault()->uploadPaths[$uploadField];
426
        return DirHelper::isAbsolute($path) ? DirHelper::canonicalize($path) : DirHelper::canonicalize(public_path($path));
427
    }
428
429
    /**
430
     * Return the full (path+filename) upload abs path for the passed attribute.
431
     * Returns empty string if dir if not exists.
432
     * @param string $uploadField
433
     * @return string
434
     */
435
    public function getUploadFileFullPath(string $uploadField) : string
436
    {
437
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
438
        $uploadFieldPath = DirHelper::addFinalSlash($uploadFieldPath) . $this->{$uploadField};
439
440
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
441
            return '';
442
        }
443
444
        return $uploadFieldPath;
445
    }
446
447
    /**
448
     * Return the full url (base url + filename) for the passed attribute.
449
     * Returns empty string if dir if not exists.
450
     * Ex.:  http://localhost/laravel/public/upload/news/pippo.jpg
451
     * @param string $uploadField
452
     * @return string
453
     */
454 View Code Duplication
    public function getUploadFileUrl(string $uploadField) : string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
455
    {
456
        $Url = $this->getUploadFileFullPath($uploadField);
457
458
        $uploadFieldPath = DirHelper::canonicalize($this->removePublicPath($Url));
459
460
        return $uploadFieldPath == '' ? '' : URL::to($uploadFieldPath);
461
    }
462
463
    /**
464
     * get a path and remove public_path.
465
     * @param string $path
466
     * @return string
467
     */
468
    public function removePublicPath(string $path) : string
469
    {
470
        if ($path == '') {
471
            return '';
472
        }
473
        $path = str_replace(DirHelper::canonicalize(public_path()), '', DirHelper::canonicalize($path));
474
        if ($this->isSlashOrEmptyDir($path)) {
475
            return '';
476
        }
477
478
        return $path;
479
    }
480
481
    /**
482
     * Return the base url (without filename) for the passed attribute.
483
     * Returns empty string if dir if not exists.
484
     * Ex.:  http://localhost/laravel/public/upload/
485
     * @param string $uploadField
486
     * @return string
487
     */
488 View Code Duplication
    public function getUploadFileBaseUrl(string $uploadField) : string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
489
    {
490
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
491
492
        $uploadFieldPath = DirHelper::canonicalize(DirHelper::addFinalSlash($this->removePublicPath($uploadFieldPath)));
493
494
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
495
            return '';
496
        }
497
498
        return URL::to($uploadFieldPath);
499
    }
500
501
    /**
502
     * Calcolate the new name for ALL uploaded files and set relative upload attributes
503
     */
504
    public function generateAllNewUploadFileNameAndSetAttribute()
505
    {
506
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
507
            $this->generateNewUploadFileNameAndSetAttribute($uploadField);
508
        }
509
    }
510
511
    /**
512
     * Calcolate the new name for uploaded file relative to passed attribute name and set the upload attribute
513
     * @param string $uploadField
514
     */
515
    public function generateNewUploadFileNameAndSetAttribute(string $uploadField)
516
    {
517
        if (!trim($uploadField)) {
518
            return;
519
        }
520
521
        //generate new file name
522
        $uploadedFile = RequestHelper::getCurrentRequestFileSafe($uploadField);
523
        if ($uploadedFile === null) {
524
            return;
525
        }
526
        $newName = $this->generateNewUploadFileName($uploadedFile);
0 ignored issues
show
Bug introduced by
It seems like $uploadedFile defined by \Padosoft\Laravel\Reques...tFileSafe($uploadField) on line 522 can also be of type array; however, Padosoft\Uploadable\Uplo...rateNewUploadFileName() does only seem to accept object<Illuminate\Http\UploadedFile>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
527
        if ($newName == '') {
528
            return;
529
        }
530
531
        //set attribute
532
        $this->{$uploadField} = $newName;
533
    }
534
535
    /**
536
     * @param string $uploadField
537
     * @param string $newName
538
     */
539
    public function updateDb(string $uploadField, string $newName)
540
    {
541
        if ($this->id < 1) {
542
            return;
543
        }
544
        DB::table($this->getTable())
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
545
            ->where('id', $this->id)
546
            ->update([$uploadField => $newName]);
547
    }
548
549
    /**
550
     * @param string $path
551
     * @return bool
552
     */
553
    public function isSlashOrEmptyDir(string $path):bool
554
    {
555
        return $path === null || $path == '' || $path == '\/' || $path == DIRECTORY_SEPARATOR;
556
    }
557
}
558