Completed
Push — master ( a50e59...22377b )
by Freek
10:42
created

HasMediaTrait   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 521
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 14

Importance

Changes 0
Metric Value
wmc 59
lcom 4
cbo 14
dl 0
loc 521
rs 4.08
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A bootHasMediaTrait() 0 16 4
A media() 0 4 1
A addMedia() 0 4 1
A addMediaFromRequest() 0 4 1
A addMultipleMediaFromRequest() 0 4 1
A addAllMediaFromRequest() 0 4 1
A addMediaFromUrl() 0 29 4
A addMediaFromBase64() 0 30 4
A copyMedia() 0 4 1
A hasMedia() 0 4 2
A getMedia() 0 4 1
A getFirstMedia() 0 6 1
A getFirstMediaUrl() 0 10 2
A getFirstTemporaryUrl() 0 10 2
A getFirstMediaPath() 0 10 2
A updateMedia() 0 30 4
A removeMediaItemsNotPresentInArray() 0 8 1
A clearMediaCollection() 0 13 2
A clearMediaCollectionExcept() 0 24 4
A deleteMedia() 0 14 3
A addMediaConversion() 0 8 1
A addMediaCollection() 0 8 1
A deletePreservingMedia() 0 6 1
A shouldDeletePreservingMedia() 0 4 1
A mediaIsPreloaded() 0 4 1
A loadMedia() 0 17 3
A prepareToAttachMedia() 0 4 1
A processUnattachedMedia() 0 8 2
A guardAgainstInvalidMimeType() 0 17 3
A registerMediaConversions() 0 3 1
A registerMediaCollections() 0 3 1
A registerAllMediaConversions() 0 23 1

How to fix   Complexity   

Complex Class

Complex classes like HasMediaTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasMediaTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spatie\MediaLibrary\HasMedia;
4
5
use DateTimeInterface;
6
use Illuminate\Http\File;
7
use Illuminate\Support\Collection;
8
use Spatie\MediaLibrary\Models\Media;
9
use Spatie\MediaLibrary\MediaRepository;
10
use Illuminate\Support\Facades\Validator;
11
use Spatie\MediaLibrary\FileAdder\FileAdder;
12
use Illuminate\Database\Eloquent\SoftDeletes;
13
use Spatie\MediaLibrary\Conversion\Conversion;
14
use Spatie\MediaLibrary\FileAdder\FileAdderFactory;
15
use Spatie\MediaLibrary\Events\CollectionHasBeenCleared;
16
use Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted;
17
use Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated;
18
use Spatie\MediaLibrary\MediaCollection\MediaCollection;
19
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\UnreachableUrl;
20
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\InvalidBase64Data;
21
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\MimeTypeNotAllowed;
22
23
trait HasMediaTrait
24
{
25
    /** @var array */
26
    public $mediaConversions = [];
27
28
    /** @var array */
29
    public $mediaCollections = [];
30
31
    /** @var bool */
32
    protected $deletePreservingMedia = false;
33
34
    /** @var array */
35
    protected $unAttachedMediaLibraryItems = [];
36
37
    public static function bootHasMediaTrait()
38
    {
39
        static::deleting(function (HasMedia $entity) {
40
            if ($entity->shouldDeletePreservingMedia()) {
41
                return;
42
            }
43
44
            if (in_array(SoftDeletes::class, class_uses_recursive($entity))) {
45
                if (! $entity->forceDeleting) {
0 ignored issues
show
Bug introduced by
Accessing forceDeleting on the interface Spatie\MediaLibrary\HasMedia\HasMedia suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
46
                    return;
47
                }
48
            }
49
50
            $entity->media()->get()->each->delete();
51
        });
52
    }
53
54
    /**
55
     * Set the polymorphic relation.
56
     *
57
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
58
     */
59
    public function media()
60
    {
61
        return $this->morphMany(config('medialibrary.media_model'), 'model');
62
    }
63
64
    /**
65
     * Add a file to the medialibrary.
66
     *
67
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
68
     *
69
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
70
     */
71
    public function addMedia($file)
72
    {
73
        return app(FileAdderFactory::class)->create($this, $file);
74
    }
75
76
    /**
77
     * Add a file from a request.
78
     *
79
     * @param string $key
80
     *
81
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
82
     */
83
    public function addMediaFromRequest(string $key)
84
    {
85
        return app(FileAdderFactory::class)->createFromRequest($this, $key);
86
    }
87
88
    /**
89
     * Add multiple files from a request by keys.
90
     *
91
     * @param string[] $keys
92
     *
93
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder[]
94
     */
95
    public function addMultipleMediaFromRequest(array $keys)
96
    {
97
        return app(FileAdderFactory::class)->createMultipleFromRequest($this, $keys);
98
    }
99
100
    /**
101
     * Add all files from a request.
102
     *
103
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder[]
104
     */
105
    public function addAllMediaFromRequest()
106
    {
107
        return app(FileAdderFactory::class)->createAllFromRequest($this);
108
    }
109
110
    /**
111
     * Add a remote file to the medialibrary.
112
     *
113
     * @param string $url
114
     * @param string|array ...$allowedMimeTypes
115
     *
116
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
117
     *
118
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
119
     */
120
    public function addMediaFromUrl(string $url, ...$allowedMimeTypes)
121
    {
122
        if (! $stream = @fopen($url, 'r')) {
123
            throw UnreachableUrl::create($url);
124
        }
125
126
        $temporaryFile = tempnam(sys_get_temp_dir(), 'media-library');
127
        file_put_contents($temporaryFile, $stream);
128
129
        $this->guardAgainstInvalidMimeType($temporaryFile, $allowedMimeTypes);
130
131
        $filename = basename(parse_url($url, PHP_URL_PATH));
132
        $filename = str_replace('%20', ' ', $filename);
133
134
        if ($filename === '') {
135
            $filename = 'file';
136
        }
137
138
        $mediaExtension = explode('/', mime_content_type($temporaryFile));
139
140
        if (! str_contains($filename, '.')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated with message: Str::contains() should be used directly instead. Will be removed in Laravel 5.9.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
141
            $filename = "{$filename}.{$mediaExtension[1]}";
142
        }
143
144
        return app(FileAdderFactory::class)
145
            ->create($this, $temporaryFile)
146
            ->usingName(pathinfo($filename, PATHINFO_FILENAME))
147
            ->usingFileName($filename);
148
    }
149
150
    /**
151
     * Add a base64 encoded file to the medialibrary.
152
     *
153
     * @param string $base64data
154
     * @param string|array ...$allowedMimeTypes
155
     *
156
     * @throws InvalidBase64Data
157
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
158
     *
159
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
160
     */
161
    public function addMediaFromBase64(string $base64data, ...$allowedMimeTypes): FileAdder
162
    {
163
        // strip out data uri scheme information (see RFC 2397)
164
        if (strpos($base64data, ';base64') !== false) {
165
            [$_, $base64data] = explode(';', $base64data);
0 ignored issues
show
Bug introduced by
The variable $_ does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
166
            [$_, $base64data] = explode(',', $base64data);
167
        }
168
169
        // strict mode filters for non-base64 alphabet characters
170
        if (base64_decode($base64data, true) === false) {
171
            throw InvalidBase64Data::create();
172
        }
173
174
        // decoding and then reencoding should not change the data
175
        if (base64_encode(base64_decode($base64data)) !== $base64data) {
176
            throw InvalidBase64Data::create();
177
        }
178
179
        $binaryData = base64_decode($base64data);
180
181
        // temporarily store the decoded data on the filesystem to be able to pass it to the fileAdder
182
        $tmpFile = tempnam(sys_get_temp_dir(), 'medialibrary');
183
        file_put_contents($tmpFile, $binaryData);
184
185
        $this->guardAgainstInvalidMimeType($tmpFile, $allowedMimeTypes);
186
187
        $file = app(FileAdderFactory::class)->create($this, $tmpFile);
188
189
        return $file;
190
    }
191
192
    /**
193
     * Copy a file to the medialibrary.
194
     *
195
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
196
     *
197
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
198
     */
199
    public function copyMedia($file)
200
    {
201
        return $this->addMedia($file)->preservingOriginal();
202
    }
203
204
    /*
205
     * Determine if there is media in the given collection.
206
     */
207
    public function hasMedia(string $collectionName = 'default'): bool
208
    {
209
        return count($this->getMedia($collectionName)) ? true : false;
210
    }
211
212
    /**
213
     * Get media collection by its collectionName.
214
     *
215
     * @param string $collectionName
216
     * @param array|callable $filters
217
     *
218
     * @return \Illuminate\Support\Collection
219
     */
220
    public function getMedia(string $collectionName = 'default', $filters = []): Collection
221
    {
222
        return app(MediaRepository::class)->getCollection($this, $collectionName, $filters);
223
    }
224
225
    public function getFirstMedia(string $collectionName = 'default', array $filters = []): ?Media
226
    {
227
        $media = $this->getMedia($collectionName, $filters);
228
229
        return $media->first();
230
    }
231
232
    /*
233
     * Get the url of the image for the given conversionName
234
     * for first media for the given collectionName.
235
     * If no profile is given, return the source's url.
236
     */
237
    public function getFirstMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
238
    {
239
        $media = $this->getFirstMedia($collectionName);
240
241
        if (! $media) {
242
            return '';
243
        }
244
245
        return $media->getUrl($conversionName);
246
    }
247
248
    /*
249
     * Get the url of the image for the given conversionName
250
     * for first media for the given collectionName.
251
     * If no profile is given, return the source's url.
252
     */
253
    public function getFirstTemporaryUrl(DateTimeInterface $expiration, string $collectionName = 'default', string $conversionName = ''): string
254
    {
255
        $media = $this->getFirstMedia($collectionName);
256
257
        if (! $media) {
258
            return '';
259
        }
260
261
        return $media->getTemporaryUrl($expiration, $conversionName);
262
    }
263
264
    /*
265
     * Get the url of the image for the given conversionName
266
     * for first media for the given collectionName.
267
     * If no profile is given, return the source's url.
268
     */
269
    public function getFirstMediaPath(string $collectionName = 'default', string $conversionName = ''): string
270
    {
271
        $media = $this->getFirstMedia($collectionName);
272
273
        if (! $media) {
274
            return '';
275
        }
276
277
        return $media->getPath($conversionName);
278
    }
279
280
    /**
281
     * Update a media collection by deleting and inserting again with new values.
282
     *
283
     * @param array $newMediaArray
284
     * @param string $collectionName
285
     *
286
     * @return \Illuminate\Support\Collection
287
     *
288
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated
289
     */
290
    public function updateMedia(array $newMediaArray, string $collectionName = 'default'): Collection
291
    {
292
        $this->removeMediaItemsNotPresentInArray($newMediaArray, $collectionName);
293
294
        return collect($newMediaArray)
295
            ->map(function (array $newMediaItem) use ($collectionName) {
296
                static $orderColumn = 1;
297
298
                $mediaClass = config('medialibrary.media_model');
299
                $currentMedia = $mediaClass::findOrFail($newMediaItem['id']);
300
301
                if ($currentMedia->collection_name !== $collectionName) {
302
                    throw MediaCannotBeUpdated::doesNotBelongToCollection($collectionName, $currentMedia);
303
                }
304
305
                if (array_key_exists('name', $newMediaItem)) {
306
                    $currentMedia->name = $newMediaItem['name'];
307
                }
308
309
                if (array_key_exists('custom_properties', $newMediaItem)) {
310
                    $currentMedia->custom_properties = $newMediaItem['custom_properties'];
311
                }
312
313
                $currentMedia->order_column = $orderColumn++;
314
315
                $currentMedia->save();
316
317
                return $currentMedia;
318
            });
319
    }
320
321
    protected function removeMediaItemsNotPresentInArray(array $newMediaArray, string $collectionName = 'default')
322
    {
323
        $this->getMedia($collectionName)
324
            ->reject(function (Media $currentMediaItem) use ($newMediaArray) {
325
                return in_array($currentMediaItem->id, array_column($newMediaArray, 'id'));
326
            })
327
            ->each->delete();
328
    }
329
330
    /**
331
     * Remove all media in the given collection.
332
     *
333
     * @param string $collectionName
334
     *
335
     * @return $this
336
     */
337
    public function clearMediaCollection(string $collectionName = 'default'): self
338
    {
339
        $this->getMedia($collectionName)
340
            ->each->delete();
341
342
        event(new CollectionHasBeenCleared($this, $collectionName));
343
344
        if ($this->mediaIsPreloaded()) {
345
            unset($this->media);
346
        }
347
348
        return $this;
349
    }
350
351
    /**
352
     * Remove all media in the given collection except some.
353
     *
354
     * @param string $collectionName
355
     * @param \Spatie\MediaLibrary\Models\Media[]|\Illuminate\Support\Collection $excludedMedia
356
     *
357
     * @return $this
358
     */
359
    public function clearMediaCollectionExcept(string $collectionName = 'default', $excludedMedia = [])
360
    {
361
        if ($excludedMedia instanceof Media) {
362
            $excludedMedia = collect()->push($excludedMedia);
363
        }
364
365
        $excludedMedia = collect($excludedMedia);
366
367
        if ($excludedMedia->isEmpty()) {
368
            return $this->clearMediaCollection($collectionName);
369
        }
370
371
        $this->getMedia($collectionName)
372
            ->reject(function (Media $media) use ($excludedMedia) {
373
                return $excludedMedia->where('id', $media->id)->count();
374
            })
375
            ->each->delete();
376
377
        if ($this->mediaIsPreloaded()) {
378
            unset($this->media);
379
        }
380
381
        return $this;
382
    }
383
384
    /**
385
     * Delete the associated media with the given id.
386
     * You may also pass a media object.
387
     *
388
     * @param int|\Spatie\MediaLibrary\Models\Media $mediaId
389
     *
390
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted
391
     */
392
    public function deleteMedia($mediaId)
393
    {
394
        if ($mediaId instanceof Media) {
395
            $mediaId = $mediaId->id;
396
        }
397
398
        $media = $this->media->find($mediaId);
399
400
        if (! $media) {
401
            throw MediaCannotBeDeleted::doesNotBelongToModel($mediaId, $this);
402
        }
403
404
        $media->delete();
405
    }
406
407
    /*
408
     * Add a conversion.
409
     */
410
    public function addMediaConversion(string $name): Conversion
411
    {
412
        $conversion = Conversion::create($name);
413
414
        $this->mediaConversions[] = $conversion;
415
416
        return $conversion;
417
    }
418
419
    public function addMediaCollection(string $name): MediaCollection
420
    {
421
        $mediaCollection = MediaCollection::create($name);
422
423
        $this->mediaCollections[] = $mediaCollection;
424
425
        return $mediaCollection;
426
    }
427
428
    /**
429
     * Delete the model, but preserve all the associated media.
430
     *
431
     * @return bool
432
     */
433
    public function deletePreservingMedia(): bool
434
    {
435
        $this->deletePreservingMedia = true;
436
437
        return $this->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Spatie\MediaLibrary\HasMedia\HasMediaTrait. Did you maybe mean deleteMedia()?

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

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

Loading history...
438
    }
439
440
    /**
441
     * Determines if the media files should be preserved when the media object gets deleted.
442
     *
443
     * @return bool
444
     */
445
    public function shouldDeletePreservingMedia()
446
    {
447
        return $this->deletePreservingMedia ?? false;
448
    }
449
450
    protected function mediaIsPreloaded(): bool
451
    {
452
        return $this->relationLoaded('media');
0 ignored issues
show
Bug introduced by
It seems like relationLoaded() 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...
453
    }
454
455
    /**
456
     * Cache the media on the object.
457
     *
458
     * @param string $collectionName
459
     *
460
     * @return mixed
461
     */
462
    public function loadMedia(string $collectionName)
463
    {
464
        $collection = $this->exists
0 ignored issues
show
Bug introduced by
The property exists 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...
465
            ? $this->media
466
            : collect($this->unAttachedMediaLibraryItems)->pluck('media');
467
468
        return $collection
469
            ->filter(function (Media $mediaItem) use ($collectionName) {
470
                if ($collectionName == '') {
471
                    return true;
472
                }
473
474
                return $mediaItem->collection_name === $collectionName;
475
            })
476
            ->sortBy('order_column')
477
            ->values();
478
    }
479
480
    public function prepareToAttachMedia(Media $media, FileAdder $fileAdder)
481
    {
482
        $this->unAttachedMediaLibraryItems[] = compact('media', 'fileAdder');
483
    }
484
485
    public function processUnattachedMedia(callable $callable)
486
    {
487
        foreach ($this->unAttachedMediaLibraryItems as $item) {
488
            $callable($item['media'], $item['fileAdder']);
489
        }
490
491
        $this->unAttachedMediaLibraryItems = [];
492
    }
493
494
    protected function guardAgainstInvalidMimeType(string $file, ...$allowedMimeTypes)
495
    {
496
        $allowedMimeTypes = array_flatten($allowedMimeTypes);
0 ignored issues
show
Deprecated Code introduced by
The function array_flatten() has been deprecated with message: Arr::flatten() should be used directly instead. Will be removed in Laravel 5.9.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
497
498
        if (empty($allowedMimeTypes)) {
499
            return;
500
        }
501
502
        $validation = Validator::make(
503
            ['file' => new File($file)],
504
            ['file' => 'mimetypes:'.implode(',', $allowedMimeTypes)]
505
        );
506
507
        if ($validation->fails()) {
508
            throw MimeTypeNotAllowed::create($file, $allowedMimeTypes);
509
        }
510
    }
511
512
    public function registerMediaConversions(Media $media = null)
0 ignored issues
show
Unused Code introduced by
The parameter $media 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...
513
    {
514
    }
515
516
    public function registerMediaCollections()
517
    {
518
    }
519
520
    public function registerAllMediaConversions(Media $media = null)
521
    {
522
        $this->registerMediaCollections();
523
524
        collect($this->mediaCollections)->each(function (MediaCollection $mediaCollection) use ($media) {
525
            $actualMediaConversions = $this->mediaConversions;
526
527
            $this->mediaConversions = [];
528
529
            ($mediaCollection->mediaConversionRegistrations)($media);
530
531
            $preparedMediaConversions = collect($this->mediaConversions)
532
                ->each(function (Conversion $conversion) use ($mediaCollection) {
533
                    $conversion->performOnCollections($mediaCollection->name);
534
                })
535
                ->values()
536
                ->toArray();
537
538
            $this->mediaConversions = array_merge($actualMediaConversions, $preparedMediaConversions);
539
        });
540
541
        $this->registerMediaConversions($media);
542
    }
543
}
544