Completed
Push — master ( 67c865...658a4b )
by Lorenzo
02:27
created

Uploadable::deleteUploadedFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 3
nc 2
nop 0
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
        static::saved(function (Model $model) {
53
            $model->uploadFiles();
54
        });
55
    }
56
57
    /**
58
     * Bind update model events.
59
     */
60
    protected static function bindUpdateEvents()
61
    {
62
        static::updating(function (Model $model) {
63
            //check if there is old files to delete
64
            $model->checkIfNeedToDeleteOldFiles();
65
        });
66
    }
67
68
    /**
69
     * Bind delete model events.
70
     */
71
    protected static function bindDeleteEvents()
72
    {
73
        static::deleting(function (Model $model) {
74
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
75
            $model->guardAgainstInvalidUploadOptions();
76
            //check if there is old files to delete
77
            $model->checkIfNeedToDeleteOldFiles();
78
        });
79
80
        static::deleted(function (Model $model) {
81
            $model->deleteUploadedFiles();
82
        });
83
    }
84
85
    /**
86
     * Retrive a specifice UploadOptions for this model, or return default UploadOptions
87
     * @return UploadOptions
88
     */
89
    public function getUploadOptionsOrDefault() : UploadOptions
90
    {
91
        if ($this->uploadOptions) {
92
            return $this->uploadOptions;
93
        }
94
95
        if (method_exists($this, 'getUploadOptions')) {
96
            $method = 'getUploadOptions';
97
            $this->uploadOptions = $this->{$method}();
98
        } else {
99
            $this->uploadOptions = UploadOptions::create()->getUploadOptionsDefault()
100
                ->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...
101
        }
102
103
        return $this->uploadOptions;
104
    }
105
106
    /**
107
     * This function will throw an exception when any of the options is missing or invalid.
108
     * @throws InvalidOption
109
     */
110
    public function guardAgainstInvalidUploadOptions()
111
    {
112
        if (!count($this->uploadOptions->uploads)) {
113
            throw InvalidOption::missingUploadFields();
114
        }
115
        if ($this->uploadOptions->uploadBasePath === null || $this->uploadOptions->uploadBasePath == '') {
116
            throw InvalidOption::missingUploadBasePath();
117
        }
118
    }
119
120
    /**
121
     * Handle file upload.
122
     */
123
    public function uploadFiles()
124
    {
125
        //invalid model
126
        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...
127
            return;
128
        }
129
130
        //loop for every upload model attributes and do upload if has a file in request
131
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
132
            $this->uploadFile($uploadField);
133
        }
134
    }
135
136
    /**
137
     * Upload a file releted to a passed attribute name
138
     * @param string $uploadField
139
     */
140
    public function uploadFile(string $uploadField)
141
    {
142
        //check if there is a valid file in request for current attribute
143
        if (!RequestHelper::isValidCurrentRequestUploadFile($uploadField,
144
            $this->getUploadOptionsOrDefault()->uploadsMimeType)
145
        ) {
146
            return;
147
        }
148
149
        //retrive the uploaded file
150
        $uploadedFile = RequestHelper::getCurrentRequestFileSafe($uploadField);
151
        if ($uploadedFile === null) {
152
            return;
153
        }
154
155
        //calcolate new file name and set it to model attribute
156
        $this->generateNewUploadFileNameAndSetAttribute($uploadField);
157
158
        //do the work
159
        $newName = $this->doUpload($uploadedFile, $uploadField);
160
161
        //save on db (not call model save because invoke event and entering in loop)
162
        $this->updateDb($uploadField, $newName);
163
    }
164
165
    /**
166
     * Get an UploadedFile, generate new name, and save it in destination path.
167
     * Return empty string if it fails, otherwise return the saved file name.
168
     * @param UploadedFile $uploadedFile
169
     * @param string $uploadAttribute
170
     * @return string
171
     */
172
    public function doUpload(UploadedFile $uploadedFile, $uploadAttribute) : string
173
    {
174
        if (!$uploadAttribute) {
175
            return '';
176
        }
177
178
        //get file name by attribute
179
        $newName = $this->{$uploadAttribute};
180
181
        //get upload path to store (method create dir if not exists and return '' if it failed)
182
        $pathToStore = $this->getUploadFileBasePath($uploadAttribute);
183
184
        //delete if file already exists
185
        FileHelper::unlinkSafe(DirHelper::njoin($pathToStore, $newName));
186
187
        //move file to destination folder
188
        try {
189
            $targetFile = $uploadedFile->move($pathToStore, $newName);
190
        } catch (\Symfony\Component\HttpFoundation\File\Exception\FileException $e) {
191
            $targetFile = null;
192
            Log::warning('Error in doUpload() when try to move ' . $newName . ' to folder: ' . $pathToStore . PHP_EOL . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
193
        }
194
195
        return $targetFile ? $newName : '';
196
    }
197
198
    /**
199
     * Check request for valid files and server for correct paths defined in model
200
     * @return bool
201
     */
202
    public function requestHasValidFilesAndCorrectPaths() : bool
203
    {
204
        //current request has not uploaded files
205
        if (!RequestHelper::currentRequestHasFiles()) {
206
            return false;
207
        }
208
209
        //ensure that all upload path are ok or create it.
210
        if (!$this->checkOrCreateAllUploadBasePaths()) {
211
            return false;
212
        }
213
214
        return true;
215
    }
216
217
    /**
218
     * Generate a new file name for uploaded file.
219
     * Return empty string if uploadedFile is null, otherwise return the new file name..
220
     * @param UploadedFile $uploadedFile
221
     * @return string
222
     * @internal param string $uploadField
223
     */
224
    public function generateNewUploadFileName(UploadedFile $uploadedFile) : string
225
    {
226
        if (!$uploadedFile) {
227
            return '';
228
        }
229
        if (!$this->id && $this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
230
            return '';
231
        }
232
233
        //check if file need a new name
234
        $newName = $this->calcolateNewUploadFileName($uploadedFile);
235
        if ($newName != '') {
236
            return $newName;
237
        }
238
239
        //no new file name, return original file name
240
        return $uploadedFile->getFilename();
241
    }
242
243
    /**
244
     * Check if file need a new name and return it, otherwise return empty string.
245
     * @param UploadedFile $uploadedFile
246
     * @return string
247
     */
248
    public function calcolateNewUploadFileName(UploadedFile $uploadedFile) : string
249
    {
250
        if (!$this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
251
            return '';
252
        }
253
254
        //retrive original file name and extension
255
        $filenameWithoutExtension = UploadedFileHelper::getFilenameWithoutExtension($uploadedFile);
256
        $ext = $uploadedFile->getClientOriginalExtension();
257
258
        $newName = $filenameWithoutExtension . $this->getUploadOptionsOrDefault()->uploadFileNameSuffixSeparator . $this->id . '.' . $ext;
259
        return sanitize_filename($newName);
260
    }
261
262
    /**
263
     * delete all Uploaded Files
264
     */
265
    public function deleteUploadedFiles()
266
    {
267
        //loop for every upload model attributes
268
        foreach ($this->getUploadOptionsOrDefault()->uploads as $uploadField) {
269
            $this->deleteUploadedFile($uploadField);
270
        }
271
    }
272
273
    /**
274
     * Delete upload file related to passed attribute name
275
     * @param string $uploadField
276
     */
277
    public function deleteUploadedFile(string $uploadField)
278
    {
279
        if (!$uploadField) {
280
            return;
281
        }
282
283
        if (!$this->{$uploadField}) {
284
            return;
285
        }
286
287
        //retrive correct upload storage path for current attribute
288
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
289
290
        //unlink file
291
        $path = DirHelper::njoin($uploadFieldPath, $this->{$uploadField});
292
        FileHelper::unlinkSafe($path);
293
294
        //reset model attribute and update db field
295
        $this->setBlanckAttributeAndDB($uploadField);
296
    }
297
298
    /**
299
     * Reset model attribute and update db field
300
     * @param string $uploadField
301
     */
302
    public function setBlanckAttributeAndDB(string $uploadField)
303
    {
304
        if (!$uploadField) {
305
            return;
306
        }
307
308
        //set to black attribute
309
        $this->{$uploadField} = '';
310
311
        //save on db (not call model save because invoke event and entering in loop)
312
        $this->updateDb($uploadField, '');
313
    }
314
315
    /**
316
     * Return true If All Upload atrributes Are Empty or
317
     * if the uploads array is not set.
318
     * @return bool
319
     */
320
    public function checkIfAllUploadFieldsAreEmpty() : bool
321
    {
322
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
323
            //for performance if one attribute has value exit false
324
            if ($uploadField && $this->{$uploadField}) {
325
                return false;
326
            }
327
        }
328
329
        return true;
330
    }
331
332
    /**
333
     * Check all attributes upload path, and try to create dir if not already exists.
334
     * Return false if it fails to create all founded dirs.
335
     * @return bool
336
     */
337
    public function checkOrCreateAllUploadBasePaths() : bool
338
    {
339
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
340
            if (!$this->checkOrCreateUploadBasePath($uploadField)) {
341
                return false;
342
            }
343
        }
344
345
        return true;
346
    }
347
348
    /**
349
     * Check uploads property and return a uploads class field
350
     * or empty array if somethings wrong.
351
     * @return array
352
     */
353
    public function getUploadsAttributesSafe() : array
354
    {
355
        if (!is_array($this->getUploadOptionsOrDefault()->uploads)) {
356
            return [];
357
        }
358
359
        return $this->getUploadOptionsOrDefault()->uploads;
360
    }
361
362
    /**
363
     * Check attribute upload path, and try to create dir if not already exists.
364
     * Return false if it fails to create the dir.
365
     * @param string $uploadField
366
     * @return bool
367
     */
368
    public function checkOrCreateUploadBasePath(string $uploadField) : bool
369
    {
370
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
371
372
        return $uploadFieldPath == '' ? '' : DirHelper::checkDirExistOrCreate($uploadFieldPath,
373
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask);
374
    }
375
376
    /**
377
     * Return the upload path for the passed attribute and try to create it if not exists.
378
     * Returns empty string if dir if not exists and fails to create it.
379
     * @param string $uploadField
380
     * @return string
381
     */
382
    public function getUploadFileBasePath(string $uploadField) : string
383
    {
384
        //default model upload path
385
        $uploadFieldPath = DirHelper::canonicalize($this->getUploadOptionsOrDefault()->uploadBasePath);
386
387
        //overwrite if there is specific path for the field
388
        $specificPath = $this->getUploadFileBasePathSpecific($uploadField);
389
        if ($specificPath != '') {
390
            $uploadFieldPath = $specificPath;
391
        }
392
393
        //check if exists or try to create dir
394
        if (!DirHelper::checkDirExistOrCreate($uploadFieldPath,
395
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask)
396
        ) {
397
            return '';
398
        }
399
400
        return $uploadFieldPath;
401
    }
402
403
    /**
404
     * Return the specific upload path (by uploadPaths prop) for the passed attribute if exists,
405
     * otherwise return empty string.
406
     * If uploadPaths for this field is relative, a public_path was appended.
407
     * @param string $uploadField
408
     * @return string
409
     */
410
    public function getUploadFileBasePathSpecific(string $uploadField) : string
411
    {
412
        //check if there is a specified upload path
413
        if (empty($this->getUploadOptionsOrDefault()->uploadPaths) || count($this->getUploadOptionsOrDefault()->uploadPaths) < 1
414
            || !array_key_exists($uploadField, $this->getUploadOptionsOrDefault()->uploadPaths)
415
        ) {
416
            return '';
417
        }
418
419
        $path = $this->getUploadOptionsOrDefault()->uploadPaths[$uploadField];
420
421
        if (DirHelper::isRelative($path)) {
422
            $path = public_path($path);
423
        }
424
425
        return DirHelper::canonicalize($path);
426
    }
427
428
    /**
429
     * Return the full (path+filename) upload abs path for the passed attribute.
430
     * Returns empty string if dir if not exists.
431
     * @param string $uploadField
432
     * @return string
433
     */
434 View Code Duplication
    public
1 ignored issue
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...
435
    function getUploadFileFullPath(
436
        string $uploadField
437
    ) : string
438
    {
439
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
440
        $uploadFieldPath = DirHelper::canonicalize(DirHelper::addFinalSlash($uploadFieldPath) . $this->{$uploadField});
441
442
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
443
            return '';
444
        }
445
446
        return $uploadFieldPath;
447
    }
448
449
    /**
450
     * Return the full url (base url + filename) for the passed attribute.
451
     * Returns empty string if dir if not exists.
452
     * Ex.:  http://localhost/laravel/public/upload/news/pippo.jpg
453
     * @param string $uploadField
454
     * @return string
455
     */
456 View Code Duplication
    public
1 ignored issue
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...
457
    function getUploadFileUrl(
458
        string $uploadField
459
    ) : string
460
    {
461
        $Url = $this->getUploadFileFullPath($uploadField);
462
463
        $uploadFieldPath = DirHelper::canonicalize($this->removePublicPath($Url));
464
465
        return $uploadFieldPath == '' ? '' : URL::to($uploadFieldPath);
466
    }
467
468
    /**
469
     * get a path and remove public_path.
470
     * @param string $path
471
     * @return string
472
     */
473
    public
474
    function removePublicPath(
475
        string $path
476
    ) : string
477
    {
478
        if ($path == '') {
479
            return '';
480
        }
481
        $path = str_replace(DirHelper::canonicalize(public_path()), '', DirHelper::canonicalize($path));
482
        if ($this->isSlashOrEmptyDir($path)) {
483
            return '';
484
        }
485
486
        return $path;
487
    }
488
489
    /**
490
     * Return the base url (without filename) for the passed attribute.
491
     * Returns empty string if dir if not exists.
492
     * Ex.:  http://localhost/laravel/public/upload/
493
     * @param string $uploadField
494
     * @return string
495
     */
496 View Code Duplication
    public
1 ignored issue
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...
497
    function getUploadFileBaseUrl(
498
        string $uploadField
499
    ) : string
500
    {
501
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
502
503
        $uploadFieldPath = DirHelper::canonicalize(DirHelper::addFinalSlash($this->removePublicPath($uploadFieldPath)));
504
505
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
506
            return '';
507
        }
508
509
        return URL::to($uploadFieldPath);
510
    }
511
512
    /**
513
     * Calcolate the new name for ALL uploaded files and set relative upload attributes
514
     */
515
    public
516
    function generateAllNewUploadFileNameAndSetAttribute()
517
    {
518
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
519
            $this->generateNewUploadFileNameAndSetAttribute($uploadField);
520
        }
521
    }
522
523
    /**
524
     * Calcolate the new name for uploaded file relative to passed attribute name and set the upload attribute
525
     * @param string $uploadField
526
     */
527
    public
528
    function generateNewUploadFileNameAndSetAttribute(
529
        string $uploadField
530
    ) {
531
        if (!trim($uploadField)) {
532
            return;
533
        }
534
535
        //generate new file name
536
        $uploadedFile = RequestHelper::getCurrentRequestFileSafe($uploadField);
537
        if ($uploadedFile === null) {
538
            return;
539
        }
540
        $newName = $this->generateNewUploadFileName($uploadedFile);
0 ignored issues
show
Bug introduced by
It seems like $uploadedFile defined by \Padosoft\Laravel\Reques...tFileSafe($uploadField) on line 536 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...
541
        if ($newName == '') {
542
            return;
543
        }
544
545
        //set attribute
546
        $this->{$uploadField} = $newName;
547
    }
548
549
    /**
550
     * @param string $uploadField
551
     * @param string $newName
552
     */
553
    public
554
    function updateDb(
555
        string $uploadField,
556
        string $newName
557
    ) {
558
        if ($this->id < 1) {
559
            return;
560
        }
561
        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...
562
            ->where('id', $this->id)
563
            ->update([$uploadField => $newName]);
564
    }
565
566
    /**
567
     * @param string $path
568
     * @return bool
569
     */
570
    public
571
    function isSlashOrEmptyDir(
572
        string $path
573
    ):bool
574
    {
575
        return isNullOrEmpty($path) || $path == '\/' || $path == DIRECTORY_SEPARATOR;
576
    }
577
578
    /**
579
     * Check if all uploadedFields are changed, so there is an old value
580
     * for attribute and if true try to unlink old file.
581
     */
582
    public
583
    function checkIfNeedToDeleteOldFiles()
584
    {
585
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
586
            $this->checkIfNeedToDeleteOldFile($uploadField);
587
        }
588
    }
589
590
    /**
591
     * Check if $uploadField are changed, so there is an old value
592
     * for $uploadField attribute and if true try to unlink old file.
593
     * @param string $uploadField
594
     */
595
    public
596
    function checkIfNeedToDeleteOldFile(
597
        string $uploadField
598
    ) {
599
        if (isNullOrEmpty($uploadField)
600
            || !$this->isDirty($uploadField)
0 ignored issues
show
Bug introduced by
It seems like isDirty() 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...
601
            || ($this->{$uploadField} == $this->getOriginal($uploadField))
0 ignored issues
show
Bug introduced by
It seems like getOriginal() 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...
602
        ) {
603
            return;
604
        }
605
606
        $oldValue = $this->getOriginal($uploadField);
0 ignored issues
show
Bug introduced by
It seems like getOriginal() 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...
607
608
        FileHelper::unlinkSafe(DirHelper::njoin($this->getUploadFileBasePath($uploadField), $oldValue));
609
    }
610
}
611