Completed
Push — master ( 67a667...a6dd06 )
by Freek
01:14
created

HasMediaTrait   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 559
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 10

Importance

Changes 0
Metric Value
wmc 65
lcom 3
cbo 10
dl 0
loc 559
rs 3.2
c 0
b 0
f 0

35 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 3
A getFirstTemporaryUrl() 0 10 3
A getMediaCollection() 0 9 1
A getDefaultMediaPath() 0 4 1
A getDefaultMediaUrl() 0 4 1
A getFirstMediaPath() 0 10 3
A updateMedia() 0 33 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\Arr;
8
use Illuminate\Support\Str;
9
use Illuminate\Support\Collection;
10
use Spatie\MediaLibrary\Models\Media;
11
use Spatie\MediaLibrary\MediaRepository;
12
use Illuminate\Support\Facades\Validator;
13
use Spatie\MediaLibrary\FileAdder\FileAdder;
14
use Illuminate\Database\Eloquent\SoftDeletes;
15
use Spatie\MediaLibrary\Conversion\Conversion;
16
use Spatie\MediaLibrary\FileAdder\FileAdderFactory;
17
use Spatie\MediaLibrary\Events\CollectionHasBeenCleared;
18
use Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted;
19
use Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated;
20
use Spatie\MediaLibrary\MediaCollection\MediaCollection;
21
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\UnreachableUrl;
22
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\InvalidBase64Data;
23
use Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\MimeTypeNotAllowed;
24
25
trait HasMediaTrait
26
{
27
    /** @var Conversion[] */
28
    public $mediaConversions = [];
29
30
    /** @var MediaCollection[] */
31
    public $mediaCollections = [];
32
33
    /** @var bool */
34
    protected $deletePreservingMedia = false;
35
36
    /** @var array */
37
    protected $unAttachedMediaLibraryItems = [];
38
39
    public static function bootHasMediaTrait()
40
    {
41
        static::deleting(function (HasMedia $entity) {
42
            if ($entity->shouldDeletePreservingMedia()) {
43
                return;
44
            }
45
46
            if (in_array(SoftDeletes::class, class_uses_recursive($entity))) {
47
                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...
48
                    return;
49
                }
50
            }
51
52
            $entity->media()->get()->each->delete();
53
        });
54
    }
55
56
    /**
57
     * Set the polymorphic relation.
58
     *
59
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
60
     */
61
    public function media()
62
    {
63
        return $this->morphMany(config('medialibrary.media_model'), 'model');
64
    }
65
66
    /**
67
     * Add a file to the medialibrary.
68
     *
69
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
70
     *
71
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
72
     */
73
    public function addMedia($file)
74
    {
75
        return app(FileAdderFactory::class)->create($this, $file);
76
    }
77
78
    /**
79
     * Add a file from a request.
80
     *
81
     * @param string $key
82
     *
83
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
84
     */
85
    public function addMediaFromRequest(string $key)
86
    {
87
        return app(FileAdderFactory::class)->createFromRequest($this, $key);
88
    }
89
90
    /**
91
     * Add multiple files from a request by keys.
92
     *
93
     * @param string[] $keys
94
     *
95
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder[]
96
     */
97
    public function addMultipleMediaFromRequest(array $keys)
98
    {
99
        return app(FileAdderFactory::class)->createMultipleFromRequest($this, $keys);
100
    }
101
102
    /**
103
     * Add all files from a request.
104
     *
105
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder[]
106
     */
107
    public function addAllMediaFromRequest()
108
    {
109
        return app(FileAdderFactory::class)->createAllFromRequest($this);
110
    }
111
112
    /**
113
     * Add a remote file to the medialibrary.
114
     *
115
     * @param string $url
116
     * @param string|array ...$allowedMimeTypes
117
     *
118
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
119
     *
120
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
121
     */
122
    public function addMediaFromUrl(string $url, ...$allowedMimeTypes)
123
    {
124
        if (! $stream = @fopen($url, 'r')) {
125
            throw UnreachableUrl::create($url);
126
        }
127
128
        $temporaryFile = tempnam(sys_get_temp_dir(), 'media-library');
129
        file_put_contents($temporaryFile, $stream);
130
131
        $this->guardAgainstInvalidMimeType($temporaryFile, $allowedMimeTypes);
132
133
        $filename = basename(parse_url($url, PHP_URL_PATH));
134
        $filename = str_replace('%20', ' ', $filename);
135
136
        if ($filename === '') {
137
            $filename = 'file';
138
        }
139
140
        $mediaExtension = explode('/', mime_content_type($temporaryFile));
141
142
        if (! Str::contains($filename, '.')) {
143
            $filename = "{$filename}.{$mediaExtension[1]}";
144
        }
145
146
        return app(FileAdderFactory::class)
147
            ->create($this, $temporaryFile)
148
            ->usingName(pathinfo($filename, PATHINFO_FILENAME))
149
            ->usingFileName($filename);
150
    }
151
152
    /**
153
     * Add a base64 encoded file to the medialibrary.
154
     *
155
     * @param string $base64data
156
     * @param string|array ...$allowedMimeTypes
157
     *
158
     * @throws InvalidBase64Data
159
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
160
     *
161
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
162
     */
163
    public function addMediaFromBase64(string $base64data, ...$allowedMimeTypes): FileAdder
164
    {
165
        // strip out data uri scheme information (see RFC 2397)
166
        if (strpos($base64data, ';base64') !== false) {
167
            [$_, $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...
168
            [$_, $base64data] = explode(',', $base64data);
169
        }
170
171
        // strict mode filters for non-base64 alphabet characters
172
        if (base64_decode($base64data, true) === false) {
173
            throw InvalidBase64Data::create();
174
        }
175
176
        // decoding and then reencoding should not change the data
177
        if (base64_encode(base64_decode($base64data)) !== $base64data) {
178
            throw InvalidBase64Data::create();
179
        }
180
181
        $binaryData = base64_decode($base64data);
182
183
        // temporarily store the decoded data on the filesystem to be able to pass it to the fileAdder
184
        $tmpFile = tempnam(sys_get_temp_dir(), 'medialibrary');
185
        file_put_contents($tmpFile, $binaryData);
186
187
        $this->guardAgainstInvalidMimeType($tmpFile, $allowedMimeTypes);
188
189
        $file = app(FileAdderFactory::class)->create($this, $tmpFile);
190
191
        return $file;
192
    }
193
194
    /**
195
     * Copy a file to the medialibrary.
196
     *
197
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
198
     *
199
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
200
     */
201
    public function copyMedia($file)
202
    {
203
        return $this->addMedia($file)->preservingOriginal();
204
    }
205
206
    /*
207
     * Determine if there is media in the given collection.
208
     */
209
    public function hasMedia(string $collectionName = 'default'): bool
210
    {
211
        return count($this->getMedia($collectionName)) ? true : false;
212
    }
213
214
    /**
215
     * Get media collection by its collectionName.
216
     *
217
     * @param string $collectionName
218
     * @param array|callable $filters
219
     *
220
     * @return \Illuminate\Support\Collection
221
     */
222
    public function getMedia(string $collectionName = 'default', $filters = []): Collection
223
    {
224
        return app(MediaRepository::class)->getCollection($this, $collectionName, $filters);
225
    }
226
227
    public function getFirstMedia(string $collectionName = 'default', array $filters = []): ?Media
228
    {
229
        $media = $this->getMedia($collectionName, $filters);
230
231
        return $media->first();
232
    }
233
234
    /*
235
     * Get the url of the image for the given conversionName
236
     * for first media for the given collectionName.
237
     * If no profile is given, return the source's url.
238
     */
239
    public function getFirstMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
240
    {
241
        $media = $this->getFirstMedia($collectionName);
242
243
        if (! $media) {
244
            return $this->getDefaultMediaUrl($collectionName) ?: '';
245
        }
246
247
        return $media->getUrl($conversionName);
248
    }
249
250
    /*
251
     * Get the url of the image for the given conversionName
252
     * for first media for the given collectionName.
253
     * If no profile is given, return the source's url.
254
     */
255
    public function getFirstTemporaryUrl(DateTimeInterface $expiration, string $collectionName = 'default', string $conversionName = ''): string
256
    {
257
        $media = $this->getFirstMedia($collectionName);
258
259
        if (! $media) {
260
            return $this->getDefaultMediaUrl($collectionName) ?: '';
261
        }
262
263
        return $media->getTemporaryUrl($expiration, $conversionName);
264
    }
265
266
    /**
267
     * Gets the given media collection.
268
     *
269
     * @return \Spatie\MediaLibrary\MediaCollection\MediaCollection
270
     */
271
    public function getMediaCollection(string $collectionName = 'default'): ?MediaCollection
272
    {
273
        $this->registerMediaCollections();
274
275
        return collect($this->mediaCollections)
276
            ->first(function (MediaCollection $collection) use ($collectionName) {
277
                return $collection->name === $collectionName;
278
            });
279
    }
280
281
    /**
282
     * Gets the default path to return for the given collection.
283
     *
284
     * @return string
285
     */
286
    public function getDefaultMediaPath(string $collectionName = 'default')
287
    {
288
        return optional($this->getMediaCollection($collectionName))->fallbackPath;
289
    }
290
291
    /**
292
     * Gets the default URL to return for the given collection.
293
     *
294
     * @return string
295
     */
296
    public function getDefaultMediaUrl(string $collectionName = 'default')
297
    {
298
        return optional($this->getMediaCollection($collectionName))->fallbackUrl;
299
    }
300
301
    /*
302
     * Get the url of the image for the given conversionName
303
     * for first media for the given collectionName.
304
     * If no profile is given, return the source's url.
305
     */
306
    public function getFirstMediaPath(string $collectionName = 'default', string $conversionName = ''): string
307
    {
308
        $media = $this->getFirstMedia($collectionName);
309
310
        if (! $media) {
311
            return $this->getDefaultMediaPath($collectionName) ?: '';
312
        }
313
314
        return $media->getPath($conversionName);
315
    }
316
317
    /**
318
     * Update a media collection by deleting and inserting again with new values.
319
     *
320
     * @param array $newMediaArray
321
     * @param string $collectionName
322
     *
323
     * @return \Illuminate\Support\Collection
324
     *
325
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated
326
     */
327
    public function updateMedia(array $newMediaArray, string $collectionName = 'default'): Collection
328
    {
329
        $this->removeMediaItemsNotPresentInArray($newMediaArray, $collectionName);
330
331
        $mediaClass = config('medialibrary.media_model');
332
        $mediaInstance = new $mediaClass();
333
        $keyName = $mediaInstance->getKeyName();
334
335
        return collect($newMediaArray)
336
            ->map(function (array $newMediaItem) use ($collectionName, $mediaClass, $keyName) {
337
                static $orderColumn = 1;
338
339
                $currentMedia = $mediaClass::findOrFail($newMediaItem[$keyName]);
340
341
                if ($currentMedia->collection_name !== $collectionName) {
342
                    throw MediaCannotBeUpdated::doesNotBelongToCollection($collectionName, $currentMedia);
343
                }
344
345
                if (array_key_exists('name', $newMediaItem)) {
346
                    $currentMedia->name = $newMediaItem['name'];
347
                }
348
349
                if (array_key_exists('custom_properties', $newMediaItem)) {
350
                    $currentMedia->custom_properties = $newMediaItem['custom_properties'];
351
                }
352
353
                $currentMedia->order_column = $orderColumn++;
354
355
                $currentMedia->save();
356
357
                return $currentMedia;
358
            });
359
    }
360
361
    protected function removeMediaItemsNotPresentInArray(array $newMediaArray, string $collectionName = 'default')
362
    {
363
        $this->getMedia($collectionName)
364
            ->reject(function (Media $currentMediaItem) use ($newMediaArray) {
365
                return in_array($currentMediaItem->getKey(), array_column($newMediaArray, $currentMediaItem->getKeyName()));
366
            })
367
            ->each->delete();
368
    }
369
370
    /**
371
     * Remove all media in the given collection.
372
     *
373
     * @param string $collectionName
374
     *
375
     * @return $this
376
     */
377
    public function clearMediaCollection(string $collectionName = 'default'): self
378
    {
379
        $this->getMedia($collectionName)
380
            ->each->delete();
381
382
        event(new CollectionHasBeenCleared($this, $collectionName));
383
384
        if ($this->mediaIsPreloaded()) {
385
            unset($this->media);
386
        }
387
388
        return $this;
389
    }
390
391
    /**
392
     * Remove all media in the given collection except some.
393
     *
394
     * @param string $collectionName
395
     * @param \Spatie\MediaLibrary\Models\Media[]|\Illuminate\Support\Collection $excludedMedia
396
     *
397
     * @return $this
398
     */
399
    public function clearMediaCollectionExcept(string $collectionName = 'default', $excludedMedia = [])
400
    {
401
        if ($excludedMedia instanceof Media) {
402
            $excludedMedia = collect()->push($excludedMedia);
403
        }
404
405
        $excludedMedia = collect($excludedMedia);
406
407
        if ($excludedMedia->isEmpty()) {
408
            return $this->clearMediaCollection($collectionName);
409
        }
410
411
        $this->getMedia($collectionName)
412
            ->reject(function (Media $media) use ($excludedMedia) {
413
                return $excludedMedia->where($media->getKeyName(), $media->getKey())->count();
414
            })
415
            ->each->delete();
416
417
        if ($this->mediaIsPreloaded()) {
418
            unset($this->media);
419
        }
420
421
        return $this;
422
    }
423
424
    /**
425
     * Delete the associated media with the given id.
426
     * You may also pass a media object.
427
     *
428
     * @param int|\Spatie\MediaLibrary\Models\Media $mediaId
429
     *
430
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted
431
     */
432
    public function deleteMedia($mediaId)
433
    {
434
        if ($mediaId instanceof Media) {
435
            $mediaId = $mediaId->getKey();
436
        }
437
438
        $media = $this->media->find($mediaId);
439
440
        if (! $media) {
441
            throw MediaCannotBeDeleted::doesNotBelongToModel($mediaId, $this);
442
        }
443
444
        $media->delete();
445
    }
446
447
    /*
448
     * Add a conversion.
449
     */
450
    public function addMediaConversion(string $name): Conversion
451
    {
452
        $conversion = Conversion::create($name);
453
454
        $this->mediaConversions[] = $conversion;
455
456
        return $conversion;
457
    }
458
459
    public function addMediaCollection(string $name): MediaCollection
460
    {
461
        $mediaCollection = MediaCollection::create($name);
462
463
        $this->mediaCollections[] = $mediaCollection;
464
465
        return $mediaCollection;
466
    }
467
468
    /**
469
     * Delete the model, but preserve all the associated media.
470
     *
471
     * @return bool
472
     */
473
    public function deletePreservingMedia(): bool
474
    {
475
        $this->deletePreservingMedia = true;
476
477
        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...
478
    }
479
480
    /**
481
     * Determines if the media files should be preserved when the media object gets deleted.
482
     *
483
     * @return bool
484
     */
485
    public function shouldDeletePreservingMedia()
486
    {
487
        return $this->deletePreservingMedia ?? false;
488
    }
489
490
    protected function mediaIsPreloaded(): bool
491
    {
492
        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...
493
    }
494
495
    /**
496
     * Cache the media on the object.
497
     *
498
     * @param string $collectionName
499
     *
500
     * @return mixed
501
     */
502
    public function loadMedia(string $collectionName)
503
    {
504
        $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...
505
            ? $this->media
506
            : collect($this->unAttachedMediaLibraryItems)->pluck('media');
507
508
        return $collection
509
            ->filter(function (Media $mediaItem) use ($collectionName) {
510
                if ($collectionName == '') {
511
                    return true;
512
                }
513
514
                return $mediaItem->collection_name === $collectionName;
515
            })
516
            ->sortBy('order_column')
517
            ->values();
518
    }
519
520
    public function prepareToAttachMedia(Media $media, FileAdder $fileAdder)
521
    {
522
        $this->unAttachedMediaLibraryItems[] = compact('media', 'fileAdder');
523
    }
524
525
    public function processUnattachedMedia(callable $callable)
526
    {
527
        foreach ($this->unAttachedMediaLibraryItems as $item) {
528
            $callable($item['media'], $item['fileAdder']);
529
        }
530
531
        $this->unAttachedMediaLibraryItems = [];
532
    }
533
534
    protected function guardAgainstInvalidMimeType(string $file, ...$allowedMimeTypes)
535
    {
536
        $allowedMimeTypes = Arr::flatten($allowedMimeTypes);
537
538
        if (empty($allowedMimeTypes)) {
539
            return;
540
        }
541
542
        $validation = Validator::make(
543
            ['file' => new File($file)],
544
            ['file' => 'mimetypes:'.implode(',', $allowedMimeTypes)]
545
        );
546
547
        if ($validation->fails()) {
548
            throw MimeTypeNotAllowed::create($file, $allowedMimeTypes);
549
        }
550
    }
551
552
    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...
553
    {
554
    }
555
556
    public function registerMediaCollections()
557
    {
558
    }
559
560
    public function registerAllMediaConversions(Media $media = null)
561
    {
562
        $this->registerMediaCollections();
563
564
        collect($this->mediaCollections)->each(function (MediaCollection $mediaCollection) use ($media) {
565
            $actualMediaConversions = $this->mediaConversions;
566
567
            $this->mediaConversions = [];
568
569
            ($mediaCollection->mediaConversionRegistrations)($media);
570
571
            $preparedMediaConversions = collect($this->mediaConversions)
572
                ->each(function (Conversion $conversion) use ($mediaCollection) {
573
                    $conversion->performOnCollections($mediaCollection->name);
574
                })
575
                ->values()
576
                ->toArray();
577
578
            $this->mediaConversions = array_merge($actualMediaConversions, $preparedMediaConversions);
579
        });
580
581
        $this->registerMediaConversions($media);
582
    }
583
}
584