Completed
Push — master ( 658a4b...85543c )
by Lorenzo
02:19
created

Uploadable::checkOrCreateUploadBasePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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