Completed
Push — master ( f16361...a52b49 )
by Freek
02:00
created

HasMediaTrait   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 514
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 12

Importance

Changes 0
Metric Value
wmc 58
lcom 4
cbo 12
dl 0
loc 514
rs 4.8387
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getFirstMedia() 0 6 1
A getFirstMediaUrl() 0 10 2
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 22 3
B addMediaFromBase64() 0 30 4
A copyMedia() 0 4 1
A hasMedia() 0 4 2
A getMedia() 0 4 1
A getFirstTemporaryUrl() 0 10 2
A getFirstMediaPath() 0 10 2
B updateMedia() 0 30 4
A removeMediaItemsNotPresentInArray() 0 8 1
A clearMediaCollection() 0 13 2
B 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, trait_uses_recursive($entity))) {
0 ignored issues
show
Documentation introduced by
$entity is of type object<Spatie\MediaLibrary\HasMedia\HasMedia>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 mixed
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
        $tmpFile = tempnam(sys_get_temp_dir(), 'media-library');
127
        file_put_contents($tmpFile, $stream);
128
129
        $this->guardAgainstInvalidMimeType($tmpFile, $allowedMimeTypes);
130
131
        $filename = basename(parse_url($url, PHP_URL_PATH));
132
        if ($filename == '') {
133
            $mediaExt = explode('/', mime_content_type($tmpFile));
134
            $filename = 'file.'.$mediaExt[1];
135
        }
136
137
        return app(FileAdderFactory::class)
138
            ->create($this, $tmpFile)
139
            ->usingName(pathinfo($filename, PATHINFO_FILENAME))
140
            ->usingFileName($filename);
141
    }
142
143
    /**
144
     * Add a base64 encoded file to the medialibrary.
145
     *
146
     * @param string $base64data
147
     * @param string|array ...$allowedMimeTypes
148
     *
149
     * @throws InvalidBase64Data
150
     * @throws \Spatie\MediaLibrary\Exceptions\FileCannotBeAdded
151
     *
152
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
153
     */
154
    public function addMediaFromBase64(string $base64data, ...$allowedMimeTypes): FileAdder
155
    {
156
        // strip out data uri scheme information (see RFC 2397)
157
        if (strpos($base64data, ';base64') !== false) {
158
            [$_, $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...
159
            [$_, $base64data] = explode(',', $base64data);
160
        }
161
162
        // strict mode filters for non-base64 alphabet characters
163
        if (base64_decode($base64data, true) === false) {
164
            throw InvalidBase64Data::create();
165
        }
166
167
        // decoding and then reeconding should not change the data
168
        if (base64_encode(base64_decode($base64data)) !== $base64data) {
169
            throw InvalidBase64Data::create();
170
        }
171
172
        $binaryData = base64_decode($base64data);
173
174
        // temporarily store the decoded data on the filesystem to be able to pass it to the fileAdder
175
        $tmpFile = tempnam(sys_get_temp_dir(), 'medialibrary');
176
        file_put_contents($tmpFile, $binaryData);
177
178
        $this->guardAgainstInvalidMimeType($tmpFile, $allowedMimeTypes);
179
180
        $file = app(FileAdderFactory::class)->create($this, $tmpFile);
181
182
        return $file;
183
    }
184
185
    /**
186
     * Copy a file to the medialibrary.
187
     *
188
     * @param string|\Symfony\Component\HttpFoundation\File\UploadedFile $file
189
     *
190
     * @return \Spatie\MediaLibrary\FileAdder\FileAdder
191
     */
192
    public function copyMedia($file)
193
    {
194
        return $this->addMedia($file)->preservingOriginal();
195
    }
196
197
    /*
198
     * Determine if there is media in the given collection.
199
     */
200
    public function hasMedia(string $collectionName = 'default'): bool
201
    {
202
        return count($this->getMedia($collectionName)) ? true : false;
203
    }
204
205
    /**
206
     * Get media collection by its collectionName.
207
     *
208
     * @param string $collectionName
209
     * @param array|callable $filters
210
     *
211
     * @return \Illuminate\Support\Collection
212
     */
213
    public function getMedia(string $collectionName = 'default', $filters = []): Collection
214
    {
215
        return app(MediaRepository::class)->getCollection($this, $collectionName, $filters);
216
    }
217
218
    public function getFirstMedia(string $collectionName = 'default', array $filters = []): ?Media
219
    {
220
        $media = $this->getMedia($collectionName, $filters);
221
222
        return $media->first();
223
    }
224
225
    /*
226
     * Get the url of the image for the given conversionName
227
     * for first media for the given collectionName.
228
     * If no profile is given, return the source's url.
229
     */
230
    public function getFirstMediaUrl(string $collectionName = 'default', string $conversionName = ''): string
231
    {
232
        $media = $this->getFirstMedia($collectionName);
233
234
        if (! $media) {
235
            return '';
236
        }
237
238
        return $media->getUrl($conversionName);
239
    }
240
241
    /*
242
     * Get the url of the image for the given conversionName
243
     * for first media for the given collectionName.
244
     * If no profile is given, return the source's url.
245
     */
246
    public function getFirstTemporaryUrl(DateTimeInterface $expiration, string $collectionName = 'default', string $conversionName = ''): string
247
    {
248
        $media = $this->getFirstMedia($collectionName);
249
250
        if (! $media) {
251
            return '';
252
        }
253
254
        return $media->getTemporaryUrl($expiration, $conversionName);
255
    }
256
257
    /*
258
     * Get the url of the image for the given conversionName
259
     * for first media for the given collectionName.
260
     * If no profile is given, return the source's url.
261
     */
262
    public function getFirstMediaPath(string $collectionName = 'default', string $conversionName = ''): string
263
    {
264
        $media = $this->getFirstMedia($collectionName);
265
266
        if (! $media) {
267
            return '';
268
        }
269
270
        return $media->getPath($conversionName);
271
    }
272
273
    /**
274
     * Update a media collection by deleting and inserting again with new values.
275
     *
276
     * @param array $newMediaArray
277
     * @param string $collectionName
278
     *
279
     * @return \Illuminate\Support\Collection
280
     *
281
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeUpdated
282
     */
283
    public function updateMedia(array $newMediaArray, string $collectionName = 'default'): Collection
284
    {
285
        $this->removeMediaItemsNotPresentInArray($newMediaArray, $collectionName);
286
287
        return collect($newMediaArray)
288
            ->map(function (array $newMediaItem) use ($collectionName) {
289
                static $orderColumn = 1;
290
291
                $mediaClass = config('medialibrary.media_model');
292
                $currentMedia = $mediaClass::findOrFail($newMediaItem['id']);
293
294
                if ($currentMedia->collection_name != $collectionName) {
295
                    throw MediaCannotBeUpdated::doesNotBelongToCollection($collectionName, $currentMedia);
296
                }
297
298
                if (array_key_exists('name', $newMediaItem)) {
299
                    $currentMedia->name = $newMediaItem['name'];
300
                }
301
302
                if (array_key_exists('custom_properties', $newMediaItem)) {
303
                    $currentMedia->custom_properties = $newMediaItem['custom_properties'];
304
                }
305
306
                $currentMedia->order_column = $orderColumn++;
307
308
                $currentMedia->save();
309
310
                return $currentMedia;
311
            });
312
    }
313
314
    protected function removeMediaItemsNotPresentInArray(array $newMediaArray, string $collectionName = 'default')
315
    {
316
        $this->getMedia($collectionName)
317
            ->reject(function (Media $currentMediaItem) use ($newMediaArray) {
318
                return in_array($currentMediaItem->id, array_column($newMediaArray, 'id'));
319
            })
320
            ->each->delete();
321
    }
322
323
    /**
324
     * Remove all media in the given collection.
325
     *
326
     * @param string $collectionName
327
     *
328
     * @return $this
329
     */
330
    public function clearMediaCollection(string $collectionName = 'default'): self
331
    {
332
        $this->getMedia($collectionName)
333
            ->each->delete();
334
335
        event(new CollectionHasBeenCleared($this, $collectionName));
336
337
        if ($this->mediaIsPreloaded()) {
338
            unset($this->media);
339
        }
340
341
        return $this;
342
    }
343
344
    /**
345
     * Remove all media in the given collection except some.
346
     *
347
     * @param string $collectionName
348
     * @param \Spatie\MediaLibrary\Media[]|\Illuminate\Support\Collection $excludedMedia
349
     *
350
     * @return $this
351
     */
352
    public function clearMediaCollectionExcept(string $collectionName = 'default', $excludedMedia = [])
353
    {
354
        if ($excludedMedia instanceof Media) {
355
            $excludedMedia = collect()->push($excludedMedia);
356
        }
357
358
        $excludedMedia = collect($excludedMedia);
359
360
        if ($excludedMedia->isEmpty()) {
361
            return $this->clearMediaCollection($collectionName);
362
        }
363
364
        $this->getMedia($collectionName)
365
            ->reject(function (Media $media) use ($excludedMedia) {
366
                return $excludedMedia->where('id', $media->id)->count();
367
            })
368
            ->each->delete();
369
370
        if ($this->mediaIsPreloaded()) {
371
            unset($this->media);
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378
     * Delete the associated media with the given id.
379
     * You may also pass a media object.
380
     *
381
     * @param int|\Spatie\MediaLibrary\Models\Media $mediaId
382
     *
383
     * @throws \Spatie\MediaLibrary\Exceptions\MediaCannotBeDeleted
384
     */
385
    public function deleteMedia($mediaId)
386
    {
387
        if ($mediaId instanceof Media) {
388
            $mediaId = $mediaId->id;
389
        }
390
391
        $media = $this->media->find($mediaId);
392
393
        if (! $media) {
394
            throw MediaCannotBeDeleted::doesNotBelongToModel($mediaId, $this);
395
        }
396
397
        $media->delete();
398
    }
399
400
    /*
401
     * Add a conversion.
402
     */
403
    public function addMediaConversion(string $name): Conversion
404
    {
405
        $conversion = Conversion::create($name);
406
407
        $this->mediaConversions[] = $conversion;
408
409
        return $conversion;
410
    }
411
412
    public function addMediaCollection(string $name): MediaCollection
413
    {
414
        $mediaCollection = MediaCollection::create($name);
415
416
        $this->mediaCollections[] = $mediaCollection;
417
418
        return $mediaCollection;
419
    }
420
421
    /**
422
     * Delete the model, but preserve all the associated media.
423
     *
424
     * @return bool
425
     */
426
    public function deletePreservingMedia(): bool
427
    {
428
        $this->deletePreservingMedia = true;
429
430
        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...
431
    }
432
433
    /**
434
     * Determines if the media files should be preserved when the media object gets deleted.
435
     *
436
     * @return \Spatie\MediaLibrary\Media
437
     */
438
    public function shouldDeletePreservingMedia()
439
    {
440
        return $this->deletePreservingMedia ?? false;
441
    }
442
443
    protected function mediaIsPreloaded(): bool
444
    {
445
        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...
446
    }
447
448
    /**
449
     * Cache the media on the object.
450
     *
451
     * @param string $collectionName
452
     *
453
     * @return mixed
454
     */
455
    public function loadMedia(string $collectionName)
456
    {
457
        $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...
458
            ? $this->media
459
            : collect($this->unAttachedMediaLibraryItems)->pluck('media');
460
461
        return $collection
462
            ->filter(function (Media $mediaItem) use ($collectionName) {
463
                if ($collectionName == '') {
464
                    return true;
465
                }
466
467
                return $mediaItem->collection_name === $collectionName;
468
            })
469
            ->sortBy('order_column')
470
            ->values();
471
    }
472
473
    public function prepareToAttachMedia(Media $media, FileAdder $fileAdder)
474
    {
475
        $this->unAttachedMediaLibraryItems[] = compact('media', 'fileAdder');
476
    }
477
478
    public function processUnattachedMedia(callable $callable)
479
    {
480
        foreach ($this->unAttachedMediaLibraryItems as $item) {
481
            $callable($item['media'], $item['fileAdder']);
482
        }
483
484
        $this->unAttachedMediaLibraryItems = [];
485
    }
486
487
    protected function guardAgainstInvalidMimeType(string $file, ...$allowedMimeTypes)
488
    {
489
        $allowedMimeTypes = array_flatten($allowedMimeTypes);
490
491
        if (empty($allowedMimeTypes)) {
492
            return;
493
        }
494
495
        $validation = Validator::make(
496
            ['file' => new File($file)],
497
            ['file' => 'mimetypes:'.implode(',', $allowedMimeTypes)]
498
        );
499
500
        if ($validation->fails()) {
501
            throw MimeTypeNotAllowed::create($file, $allowedMimeTypes);
502
        }
503
    }
504
505
    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...
506
    {
507
    }
508
509
    public function registerMediaCollections()
510
    {
511
    }
512
513
    public function registerAllMediaConversions(Media $media = null)
514
    {
515
        $this->registerMediaCollections();
516
517
        collect($this->mediaCollections)->each(function (MediaCollection $mediaCollection) use ($media) {
518
            $actualMediaConversions = $this->mediaConversions;
519
520
            $this->mediaConversions = [];
521
522
            ($mediaCollection->mediaConversionRegistrations)($media);
523
524
            $preparedMediaConversions = collect($this->mediaConversions)
525
                ->each(function (Conversion $conversion) use ($mediaCollection) {
526
                    $conversion->performOnCollections($mediaCollection->name);
527
                })
528
                ->values()
529
                ->toArray();
530
531
            $this->mediaConversions = array_merge($actualMediaConversions, $preparedMediaConversions);
532
        });
533
534
        $this->registerMediaConversions($media);
535
    }
536
}
537