Completed
Push — master ( b0ffd4...2192f5 )
by Lorenzo
02:32
created

Uploadable::getUploadFileUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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