generateNewUploadFileNameAndSetAttribute()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.584
c 0
b 0
f 0
cc 4
nc 4
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\Storage;
10
use Illuminate\Support\Facades\URL;
11
use Padosoft\Uploadable\Events\UploadDeleteExecuted;
12
use Padosoft\Uploadable\Events\UploadExecuted;
13
14
/**
15
 * Class Uploadable
16
 * Auto upload and save files on save/create/delete model.
17
 * @package Padosoft\Uploadable
18
 */
19
trait Uploadable
20
{
21
    /** @var UploadOptions */
22
    protected $uploadOptions;
23
24
    /**
25
     * Determine if the model or given attribute(s) have been modified.
26
     * Implemented in \Illuminate\Database\Eloquent\Model
27
     *
28
     * @param array|string|null $attributes = null
29
     *
30
     * @return bool
31
     */
32
    abstract public function isDirty($attributes = null);
33
34
    /**
35
     * Get the model's original attribute values.
36
     * Implemented in \Illuminate\Database\Eloquent\Model
37
     *
38
     * @param string|null $key = null
39
     * @param mixed $default = null
40
     *
41
     * @return mixed|array
42
     */
43
    abstract public function getOriginal($key = null, $default = null);
44
45
    /**
46
     * Boot the trait.
47
     */
48
    public static function bootUploadable()
49
    {
50
        self::bindCreateEvents();
51
        self::bindSaveEvents();
52
        self::bindUpdateEvents();
53
        self::bindDeleteEvents();
54
    }
55
56
    /**
57
     * Bind create model events.
58
     */
59
    protected static function bindCreateEvents()
60
    {
61
        static::creating(function ($model) {
62
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
63
            $model->guardAgainstInvalidUploadOptions();
64
        });
65
    }
66
67
    /**
68
     * Bind save model events.
69
     */
70
    protected static function bindSaveEvents()
71
    {
72
        static::saved(function (Model $model) {
73
            $model->uploadFiles();
74
        });
75
    }
76
77
    /**
78
     * Bind update model events.
79
     */
80
    protected static function bindUpdateEvents()
81
    {
82
        static::updating(function (Model $model) {
83
            //check if there is old files to delete
84
            $model->checkIfNeedToDeleteOldFiles();
85
        });
86
    }
87
88
    /**
89
     * Bind delete model events.
90
     */
91
    protected static function bindDeleteEvents()
92
    {
93
        static::deleting(function (Model $model) {
94
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
95
            $model->guardAgainstInvalidUploadOptions();
96
            //check if there is old files to delete
97
            $model->checkIfNeedToDeleteOldFiles();
98
        });
99
100
        static::deleted(function (Model $model) {
101
            $model->deleteUploadedFiles();
102
        });
103
    }
104
105
    /**
106
     * Retrive a specifice UploadOptions for this model, or return default UploadOptions
107
     * @return UploadOptions
108
     */
109
    public function getUploadOptionsOrDefault() : UploadOptions
110
    {
111
        if ($this->uploadOptions) {
112
            return $this->uploadOptions;
113
        }
114
115
        if (method_exists($this, 'getUploadOptions')) {
116
            $method = 'getUploadOptions';
117
            $this->uploadOptions = $this->{$method}();
118
        } else {
119
            $this->uploadOptions = UploadOptions::create()->getUploadOptionsDefault()
120
                ->setUploadBasePath('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...
121
        }
122
123
        return $this->uploadOptions;
124
    }
125
126
    /**
127
     * This function will throw an exception when any of the options is missing or invalid.
128
     * @throws InvalidOption
129
     */
130
    public function guardAgainstInvalidUploadOptions()
131
    {
132
        if (!is_array($this->uploadOptions->uploads) || count($this->uploadOptions->uploads)<1) {
133
            throw InvalidOption::missingUploadFields();
134
        }
135
        if ($this->uploadOptions->uploadBasePath === null || $this->uploadOptions->uploadBasePath == '') {
136
            throw InvalidOption::missingUploadBasePath();
137
        }
138
    }
139
140
    /**
141
     * Handle file upload.
142
     */
143
    public function uploadFiles()
144
    {
145
        //invalid model
146
        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...
147
            return;
148
        }
149
150
        //loop for every upload model attributes and do upload if has a file in request
151
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
152
153
            $this->uploadFile($uploadField);
154
        }
155
    }
156
157
    /**
158
     * Upload a file releted to a passed attribute name
159
     * @param string $uploadField
160
     */
161
    public function uploadFile(string $uploadField)
162
    {
163
        //check if there is a valid file in request for current attribute
164
        if (!isValidCurrentRequestUploadFile($uploadField,
165
            $this->getUploadOptionsOrDefault()->uploadsMimeType)
166
        ) {
167
            return;
168
        }
169
170
        //retrive the uploaded file
171
        $uploadedFile = getCurrentRequestFileSafe($uploadField);
172
        if ($uploadedFile === null) {
173
            return;
174
        }
175
176
        //calcolate new file name and set it to model attribute
177
        $this->generateNewUploadFileNameAndSetAttribute($uploadField);
178
179
        //do the work
180
        $newName = $this->doUpload($uploadedFile, $uploadField);
0 ignored issues
show
Bug introduced by
It seems like $uploadedFile defined by getCurrentRequestFileSafe($uploadField) on line 171 can also be of type array; however, Padosoft\Uploadable\Uploadable::doUpload() 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...
181
182
        //save on db (not call model save because invoke event and entering in loop)
183
        $this->updateDb($uploadField, $newName);
184
185
        event(new UploadExecuted($uploadField, $this));
186
    }
187
188
    /**
189
     * Get an UploadedFile, generate new name, and save it in destination path.
190
     * Return empty string if it fails, otherwise return the saved file name.
191
     * @param UploadedFile $uploadedFile
192
     * @param string $uploadAttribute
193
     * @return string
194
     */
195
    public function doUpload(UploadedFile $uploadedFile, $uploadAttribute) : string
196
    {
197
        if (!$uploadAttribute) {
198
            return '';
199
        }
200
201
        //get file name by attribute
202
        $newName = $this->{$uploadAttribute};
203
204
        //get upload path to store (method create dir if not exists and return '' if it failed)
205
        $pathToStore = $this->getUploadFileBasePath($uploadAttribute);
206
207
        //delete if file already exists
208
        $newFile = njoin($pathToStore, $newName);
209
        if($this->getUploadOptionsOrDefault()->storage->exists($newFile)){
210
            $this->getUploadOptionsOrDefault()->storage->delete($newFile);
211
        }
212
213
        //move file to destination folder
214
        try {
215
            //$targetFile = $uploadedFile->move($pathToStore, $newName);
216
            $targetFile = $this->getUploadOptionsOrDefault()->storage->putFileAs(
217
                                                        $pathToStore, $uploadedFile, $newName
218
                                                    );
219
220
        } catch (\Exception $e) {
221
            $targetFile = null;
222
            Log::warning('Error in doUpload() when try to move ' . $newName . ' to folder: ' . $pathToStore . PHP_EOL . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
223
        }
224
225
        return $targetFile ? $newName : '';
226
    }
227
228
    /**
229
     * Check request for valid files and server for correct paths defined in model
230
     * @return bool
231
     */
232
    public function requestHasValidFilesAndCorrectPaths() : bool
233
    {
234
        //current request has not uploaded files
235
        if (!currentRequestHasFiles()) {
236
            return false;
237
        }
238
239
        //ensure that all upload path are ok or create it.
240
        if (!$this->checkOrCreateAllUploadBasePaths()) {
241
            return false;
242
        }
243
244
        return true;
245
    }
246
247
    /**
248
     * Generate a new file name for uploaded file.
249
     * Return empty string if uploadedFile is null, otherwise return the new file name..
250
     * @param UploadedFile $uploadedFile
251
     * @return string
252
     * @internal param string $uploadField
253
     */
254
    public function generateNewUploadFileName(UploadedFile $uploadedFile) : string
255
    {
256
        if (!$this->id && $this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
257
            return '';
258
        }
259
260
        //check if file need a new name
261
        $newName = $this->calcolateNewUploadFileName($uploadedFile);
262
        if ($newName != '') {
263
            return $newName;
264
        }
265
266
        //no new file name, return original file name
267
        return $uploadedFile->getFilename();
268
    }
269
270
    /**
271
     * Check if file need a new name and return it, otherwise return empty string.
272
     * @param UploadedFile $uploadedFile
273
     * @return string
274
     */
275
    public function calcolateNewUploadFileName(UploadedFile $uploadedFile) : string
276
    {
277
        if (!$this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
278
            return '';
279
        }
280
281
        //retrive original file name and extension
282
        $filenameWithoutExtension = getUploadedFilenameWithoutExtension($uploadedFile);
283
        $ext = $uploadedFile->getClientOriginalExtension();
284
285
        $newName = $filenameWithoutExtension . $this->getUploadOptionsOrDefault()->uploadFileNameSuffixSeparator . $this->id . '.' . $ext;
286
        return sanitize_filename($newName);
287
    }
288
289
    /**
290
     * delete all Uploaded Files
291
     */
292
    public function deleteUploadedFiles()
293
    {
294
        //loop for every upload model attributes
295
        foreach ($this->getUploadOptionsOrDefault()->uploads as $uploadField) {
296
            $this->deleteUploadedFile($uploadField);
297
        }
298
    }
299
300
    /**
301
     * Delete upload file related to passed attribute name
302
     * @param string $uploadField
303
     */
304
    public function deleteUploadedFile(string $uploadField)
305
    {
306
        if (!$uploadField) {
307
            return;
308
        }
309
310
        if (!$this->{$uploadField}) {
311
            return;
312
        }
313
314
        //retrive correct upload storage path for current attribute
315
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
316
317
        //unlink file
318
        $path = njoin($uploadFieldPath, $this->{$uploadField});
319
        if(!$this->getUploadOptionsOrDefault()->storage->delete($path)){
320
            Log::error('Error when Uploadable.deleteUploadedFile() try to delete file:'.$path);
321
        }
322
323
        //reset model attribute and update db field
324
        $this->setBlanckAttributeAndDB($uploadField);
325
    }
326
327
    /**
328
     * Reset model attribute and update db field
329
     * @param string $uploadField
330
     */
331
    public function setBlanckAttributeAndDB(string $uploadField)
332
    {
333
        if (!$uploadField) {
334
            return;
335
        }
336
337
        //old value
338
        $old = $this->{$uploadField};
339
340
        //set to black attribute
341
        $this->{$uploadField} = '';
342
343
        //save on db (not call model save because invoke event and entering in loop)
344
        $this->updateDb($uploadField, '');
345
346
        event(new UploadDeleteExecuted($uploadField, $old, $this));
347
    }
348
349
    /**
350
     * Return true If All Upload atrributes Are Empty or
351
     * if the uploads array is not set.
352
     * @return bool
353
     */
354
    public function checkIfAllUploadFieldsAreEmpty() : bool
355
    {
356
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
357
            //for performance if one attribute has value exit false
358
            if ($uploadField && $this->{$uploadField}) {
359
                return false;
360
            }
361
        }
362
363
        return true;
364
    }
365
366
    /**
367
     * Check all attributes upload path, and try to create dir if not already exists.
368
     * Return false if it fails to create all founded dirs.
369
     * @return bool
370
     */
371
    public function checkOrCreateAllUploadBasePaths() : bool
372
    {
373
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
374
            if (!$this->checkOrCreateUploadBasePath($uploadField)) {
375
                return false;
376
            }
377
        }
378
379
        return true;
380
    }
381
382
    /**
383
     * Check uploads property and return a uploads class field
384
     * or empty array if somethings wrong.
385
     * @return array
386
     */
387
    public function getUploadsAttributesSafe() : array
388
    {
389
        if (!is_array($this->getUploadOptionsOrDefault()->uploads)) {
390
            return [];
391
        }
392
393
        return $this->getUploadOptionsOrDefault()->uploads;
394
    }
395
396
    /**
397
     * Check attribute upload path, and try to create dir if not already exists.
398
     * Return false if it fails to create the dir.
399
     * @param string $uploadField
400
     * @return bool
401
     */
402
    public function checkOrCreateUploadBasePath(string $uploadField) : bool
0 ignored issues
show
Unused Code introduced by
The parameter $uploadField is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
403
    {
404
        /*
405
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
406
407
        return $uploadFieldPath == '' ? '' : DirHelper::checkDirExistOrCreate($uploadFieldPath,
408
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask);
409
        */
410
        //Laravel storage (Local, FTP, S3) seems that you can simply write
411
        // to a path and not worry whether directory exists or not.
412
413
        return true;
414
    }
415
416
    /**
417
     * Return the upload path for the passed attribute and try to create it if not exists.
418
     * Returns empty string if dir if not exists and fails to create it.
419
     * @param string $uploadField
420
     * @return string
421
     */
422
    public function getUploadFileBasePath(string $uploadField) : string
423
    {
424
        //default model upload path
425
        $uploadFieldPath = canonicalize($this->getUploadOptionsOrDefault()->uploadBasePath);
426
427
        //overwrite if there is specific path for the field
428
        $specificPath = $this->getUploadFileBasePathSpecific($uploadField);
429
        if ($specificPath != '') {
430
            $uploadFieldPath = $specificPath;
431
        }
432
433
        //check if exists or try to create dir
434
        //Laravel storage (Local, FTP, S3) seems that you can simply write
435
        // to a path and not worry whether directory exists or not.
436
        /*
437
        if (!DirHelper::checkDirExistOrCreate($uploadFieldPath,
438
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask)
439
        ) {
440
            return '';
441
        }*/
442
443
        return $uploadFieldPath;
444
    }
445
446
    /**
447
     * Return the specific upload path (by uploadPaths prop) for the passed attribute if exists,
448
     * otherwise return empty string.
449
     * If uploadPaths for this field is relative, a public_path was appended.
450
     * @param string $uploadField
451
     * @return string
452
     */
453
    public function getUploadFileBasePathSpecific(string $uploadField) : string
454
    {
455
        //check if there is a specified upload path for this field
456
        if (!$this->hasUploadPathSpecifiedForThisField($uploadField)) {
457
            return '';
458
        }
459
460
        $path = $this->getUploadOptionsOrDefault()->uploadPaths[$uploadField];
461
462
        //if path is relative, nothing to do because all is relative to storage disk root path
463
464
        return canonicalize($path);
465
    }
466
467
    /**
468
     * Check if there is a specified upload path setted for this field
469
     * @param string $uploadField
470
     * @return bool
471
     */
472
    public function hasUploadPathSpecifiedForThisField(string $uploadField) : bool
473
    {
474
        return !(empty($this->getUploadOptionsOrDefault()->uploadPaths)
475
            || count($this->getUploadOptionsOrDefault()->uploadPaths) < 1
476
            || !array_key_exists($uploadField, $this->getUploadOptionsOrDefault()->uploadPaths)
477
        );
478
    }
479
480
    /**
481
     * Return the full (path+filename) upload abs path for the passed attribute.
482
     * Returns empty string if dir if not exists.
483
     * @param string $uploadField
484
     * @return string
485
     */
486
    public
487
    function getUploadFileFullPath(
488
        string $uploadField
489
    ) : string
490
    {
491
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
492
        $uploadFieldPath = canonicalize($this->getUploadOptionsOrDefault()->storage->path(addFinalSlash($uploadFieldPath) . $this->{$uploadField}));
493
494
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
495
            return '';
496
        }
497
498
        return $uploadFieldPath;
499
    }
500
501
    /**
502
     * Return the full url (base url + filename) for the passed attribute.
503
     * Returns empty string if dir if not exists.
504
     * Ex.:  http://localhost/laravel/public/upload/news/pippo.jpg
505
     * @param string $uploadField
506
     * @return string
507
     */
508 View Code Duplication
    public
509
    function getUploadFileUrl(
510
        string $uploadField
511
    ) : string
512
    {
513
        $url = addFinalSlash($this->getUploadFileBasePath($uploadField)).  $this->{$uploadField};
514
515
        return $url == '' ? '' : $this->getUploadOptionsOrDefault()->storage->url($url);
516
    }
517
518
    /**
519
     * get a path and remove public_path.
520
     * @param string $path
521
     * @return string
522
     */
523
    public
524
    function removePublicPath(
525
        string $path
526
    ) : string
527
    {
528
        if ($path == '') {
529
            return '';
530
        }
531
        $len = strpos($path, DIRECTORY_SEPARATOR);
532
        if ($len === false) {
533
            return '';
534
        }
535
        $path = substr($path, 0, $len);
536
        if ($this->isSlashOrEmptyDir($path)) {
537
            return '';
538
        }
539
540
        return $path;
541
    }
542
543
    /**
544
     * Return the base url (without filename) for the passed attribute.
545
     * Returns empty string if dir if not exists.
546
     * Ex.:  http://localhost/laravel/public/upload/
547
     * @param string $uploadField
548
     * @return string
549
     */
550 View Code Duplication
    public
551
    function getUploadFileBaseUrl(
552
        string $uploadField
553
    ) : string
554
    {
555
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
556
        /*dump($uploadFieldPath);
557
        $uploadFieldPath = canonicalize(addFinalSlash($this->removePublicPath($uploadFieldPath)));*/
558
559
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
560
            return '';
561
        }
562
563
        return addFinalSlash($this->getUploadOptionsOrDefault()->storage->url($uploadFieldPath));
564
    }
565
566
    /**
567
     * Calcolate the new name for ALL uploaded files and set relative upload attributes
568
     */
569
    public
570
    function generateAllNewUploadFileNameAndSetAttribute()
571
    {
572
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
573
            $this->generateNewUploadFileNameAndSetAttribute($uploadField);
574
        }
575
    }
576
577
    /**
578
     * Calcolate the new name for uploaded file relative to passed attribute name and set the upload attribute
579
     * @param string $uploadField
580
     */
581
    public
582
    function generateNewUploadFileNameAndSetAttribute(
583
        string $uploadField
584
    ) {
585
        if (!trim($uploadField)) {
586
            return;
587
        }
588
589
        //generate new file name
590
        $uploadedFile = getCurrentRequestFileSafe($uploadField);
591
        if ($uploadedFile === null) {
592
            return;
593
        }
594
        $newName = $this->generateNewUploadFileName($uploadedFile);
0 ignored issues
show
Bug introduced by
It seems like $uploadedFile defined by getCurrentRequestFileSafe($uploadField) on line 590 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...
595
        if ($newName == '') {
596
            return;
597
        }
598
599
        //set attribute
600
        $this->{$uploadField} = $newName;
601
    }
602
603
    /**
604
     * @param string $uploadField
605
     * @param string $newName
606
     */
607
    public
608
    function updateDb(
609
        string $uploadField,
610
        string $newName
611
    ) {
612
        if ($this->id < 1) {
613
            return;
614
        }
615
        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...
616
            ->where('id', $this->id)
617
            ->update([$uploadField => $newName]);
618
    }
619
620
    /**
621
     * @param string $path
622
     * @return bool
623
     */
624
    public
625
    function isSlashOrEmptyDir(
626
        string $path
627
    ):bool
628
    {
629
        return isNullOrEmpty($path) || $path == '\/' || $path == DIRECTORY_SEPARATOR;
630
    }
631
632
    /**
633
     * Check if all uploadedFields are changed, so there is an old value
634
     * for attribute and if true try to unlink old file.
635
     */
636
    public
637
    function checkIfNeedToDeleteOldFiles()
638
    {
639
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
640
            $this->checkIfNeedToDeleteOldFile($uploadField);
641
        }
642
    }
643
644
    /**
645
     * Check if $uploadField are changed, so there is an old value
646
     * for $uploadField attribute and if true try to unlink old file.
647
     * @param string $uploadField
648
     */
649
    public
650
    function checkIfNeedToDeleteOldFile(
651
        string $uploadField
652
    ) {
653
        if (isNullOrEmpty($uploadField)
654
            || !$this->isDirty($uploadField)
655
            || ($this->{$uploadField} == $this->getOriginal($uploadField))
656
            || isNullOrEmpty($this->getOriginal($uploadField))
657
        ) {
658
            return;
659
        }
660
661
        $oldValue = $this->getOriginal($uploadField);
662
663
        $this->getUploadOptionsOrDefault()->storage->delete(njoin($this->getUploadFileBasePath($uploadField), $oldValue));
664
    }
665
}
666