• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

plank / laravel-mediable / 8585433686

07 Apr 2024 02:04AM UTC coverage: 95.282% (-0.5%) from 95.789%
8585433686

push

github

web-flow
Merge pull request #343 from plank/v6

V6

338 of 375 new or added lines in 30 files covered. (90.13%)

1 existing line in 1 file now uncovered.

1434 of 1505 relevant lines covered (95.28%)

117.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.46
/src/Media.php
1
<?php
2
declare(strict_types=1);
3

4
namespace Plank\Mediable;
5

6
use Carbon\Carbon;
7
use GuzzleHttp\Psr7\Utils;
8
use Illuminate\Contracts\Filesystem\Filesystem;
9
use Illuminate\Database\Eloquent\Builder;
10
use Illuminate\Database\Eloquent\Collection;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\BelongsTo;
13
use Illuminate\Database\Eloquent\Relations\HasMany;
14
use Illuminate\Database\Eloquent\Relations\MorphToMany;
15
use Illuminate\Database\Eloquent\Relations\Pivot;
16
use Illuminate\Database\Eloquent\SoftDeletingScope;
17
use Illuminate\Support\Arr;
18
use Plank\Mediable\Exceptions\MediaMoveException;
19
use Plank\Mediable\Exceptions\MediaUrlException;
20
use Plank\Mediable\Helpers\File;
21
use Plank\Mediable\UrlGenerators\TemporaryUrlGeneratorInterface;
22
use Plank\Mediable\UrlGenerators\UrlGeneratorInterface;
23
use Psr\Http\Message\StreamInterface;
24

25
/**
26
 * Media Model.
27
 * @property int|string|null $id
28
 * @property string|null $disk
29
 * @property string|null $directory
30
 * @property string|null $filename
31
 * @property string|null $extension
32
 * @property string|null $basename
33
 * @property string|null $mime_type
34
 * @property string|null $aggregate_type
35
 * @property string|null $variant_name
36
 * @property int|string|null $original_media_id
37
 * @property int|null $size
38
 * @property string|null $alt
39
 * @property Carbon $created_at
40
 * @property Carbon $updated_at
41
 * @property Pivot $pivot
42
 * @property Collection|Media[] $variants
43
 * @property Media $originalMedia
44
 * @method static Builder inDirectory(string $disk, string $directory, bool $recursive = false)
45
 * @method static Builder inOrUnderDirectory(string $disk, string $directory)
46
 * @method static Builder whereBasename(string $basename)
47
 * @method static Builder forPathOnDisk(string $disk, string $path)
48
 * @method static Builder unordered()
49
 * @method static Builder whereIsOriginal()
50
 * @method static Builder whereIsVariant(string $variant_name = null)
51
 */
52
class Media extends Model
53
{
54
    const TYPE_IMAGE = 'image';
55
    const TYPE_IMAGE_VECTOR = 'vector';
56
    const TYPE_PDF = 'pdf';
57
    const TYPE_VIDEO = 'video';
58
    const TYPE_AUDIO = 'audio';
59
    const TYPE_ARCHIVE = 'archive';
60
    const TYPE_DOCUMENT = 'document';
61
    const TYPE_SPREADSHEET = 'spreadsheet';
62
    const TYPE_PRESENTATION = 'presentation';
63
    const TYPE_OTHER = 'other';
64
    const TYPE_ALL = 'all';
65

66
    const VARIANT_NAME_ORIGINAL = 'original';
67

68
    protected $table = 'media';
69

70
    protected $guarded = [
71
        'id',
72
        'disk',
73
        'directory',
74
        'filename',
75
        'extension',
76
        'size',
77
        'mime_type',
78
        'aggregate_type',
79
        'variant_name',
80
        'original_media_id',
81
        'alt',
82
    ];
83

84
    protected $casts = [
85
        'size' => 'int',
86
    ];
87

88
    /**
89
     * {@inheritdoc}
90
     */
91
    public static function boot()
92
    {
93
        parent::boot();
996✔
94

95
        //remove file on deletion
96
        static::deleted(function (Media $media) {
996✔
97
            $media->handleMediaDeletion();
72✔
98
        });
996✔
99
    }
100

101
    /**
102
     * Retrieve all associated models of given class.
103
     * @param  string $class FQCN
104
     * @return MorphToMany
105
     */
106
    public function models(string $class): MorphToMany
107
    {
108
        return $this
6✔
109
            ->morphedByMany(
6✔
110
                $class,
6✔
111
                'mediable',
6✔
112
                config('mediable.mediables_table', 'mediables')
6✔
113
            )
6✔
114
            ->withPivot('tag', 'order');
6✔
115
    }
116

117
    /**
118
     * Relationship to variants derived from this file
119
     * @return HasMany
120
     */
121
    public function variants(): HasMany
122
    {
123
        return $this->hasMany(
210✔
124
            get_class($this),
210✔
125
            'original_media_id'
210✔
126
        );
210✔
127
    }
128

129
    /**
130
     * Relationship to the file that this file was derived from
131
     * @return BelongsTo
132
     */
133
    public function originalMedia(): BelongsTo
134
    {
135
        return $this->belongsTo(
66✔
136
            get_class($this),
66✔
137
            'original_media_id'
66✔
138
        );
66✔
139
    }
140

141
    /**
142
     * Retrieve all other variants and originals of the media
143
     * @return Collection|Media[]
144
     */
145
    public function getAllVariants(): Collection
146
    {
147
        // if we have an original, then the variants relation should contain all others
148
        if ($this->isOriginal()) {
6✔
149
            return $this->variants->keyBy('variant_name');
6✔
150
        }
151

152
        // otherwise, get the original's variants, remove this one and add the original
153
        $collection = $this->originalMedia->variants->except($this->getKey())
6✔
154
            ->keyBy('variant_name');
6✔
155
        $collection->offsetSet(self::VARIANT_NAME_ORIGINAL, $this->originalMedia);
6✔
156

157
        return $collection;
6✔
158
    }
159

160
    public function getAllVariantsAndSelf(): Collection
161
    {
162
        if ($this->isOriginal()) {
12✔
163
            $collection = $this->variants->keyBy('variant_name');
12✔
164
            $collection->offsetSet(self::VARIANT_NAME_ORIGINAL, $this);
12✔
165
            return $collection;
12✔
166
        }
167

168
        // otherwise, get the original's variants, remove this one and add the original
169
        $collection = $this->originalMedia->variants->keyBy('variant_name');
6✔
170
        $collection->offsetSet(self::VARIANT_NAME_ORIGINAL, $this->originalMedia);
6✔
171

172
        return $collection;
6✔
173
    }
174

175
    public function hasVariant(string $variantName): bool
176
    {
177
        return $this->findVariant($variantName) !== null;
156✔
178
    }
179

180
    public function findVariant(string $variantName): ?Media
181
    {
182
        $filter = function (Media $media) use ($variantName) {
156✔
183
            return $media->variant_name === $variantName;
30✔
184
        };
156✔
185

186
        if ($this->isOriginal()) {
156✔
187
            return $this->variants->first($filter);
150✔
188
        }
189

190
        if ($variantName == $this->variant_name) {
24✔
191
            return $this;
18✔
192
        }
193

194
        if ($this->originalMedia) {
12✔
195
            return $this->originalMedia->variants->first($filter);
12✔
196
        }
197

198
        return null;
×
199
    }
200

201
    public function findOriginal(): Media
202
    {
203
        if ($this->isOriginal()) {
138✔
204
            return $this;
132✔
205
        }
206

207
        return $this->originalMedia;
18✔
208
    }
209

210
    /**
211
     * Retrieve the file extension.
212
     * @return string
213
     */
214
    public function getBasenameAttribute(): string
215
    {
216
        return $this->filename . '.' . $this->extension;
540✔
217
    }
218

219
    /**
220
     * Retrieve the file url.
221
     * @return string
222
     */
223
    public function getUrlAttribute(): string
224
    {
NEW
225
        return $this->getUrl();
×
226
    }
227

228
    /**
229
     * Query scope for to find media in a particular directory.
230
     * @param  Builder $q
231
     * @param  string $disk Filesystem disk to search in
232
     * @param  string $directory Path relative to disk
233
     * @param  bool $recursive (_optional_) If true, will find media in or under the specified directory
234
     * @return void
235
     */
236
    public function scopeInDirectory(Builder $q, string $disk, string $directory, bool $recursive = false): void
237
    {
238
        $q->where('disk', $disk);
60✔
239
        if ($recursive) {
60✔
240
            $directory = str_replace(['%', '_'], ['\%', '\_'], $directory);
42✔
241
            $q->where('directory', 'like', $directory . '%');
42✔
242
        } else {
243
            $q->where('directory', '=', $directory);
18✔
244
        }
245
    }
246

247
    /**
248
     * Query scope for finding media in a particular directory or one of its subdirectories.
249
     * @param  Builder $q
250
     * @param  string $disk Filesystem disk to search in
251
     * @param  string $directory Path relative to disk
252
     * @return void
253
     */
254
    public function scopeInOrUnderDirectory(Builder $q, string $disk, string $directory): void
255
    {
256
        $q->inDirectory($disk, $directory, true);
6✔
257
    }
258

259
    /**
260
     * Query scope for finding media by basename.
261
     * @param  Builder $q
262
     * @param  string $basename filename and extension
263
     * @return void
264
     */
265
    public function scopeWhereBasename(Builder $q, string $basename): void
266
    {
267
        $q->where('filename', pathinfo($basename, PATHINFO_FILENAME))
6✔
268
            ->where('extension', pathinfo($basename, PATHINFO_EXTENSION));
6✔
269
    }
270

271
    /**
272
     * Query scope finding media at a path relative to a disk.
273
     * @param  Builder $q
274
     * @param  string $disk
275
     * @param  string $path directory, filename and extension
276
     * @return void
277
     */
278
    public function scopeForPathOnDisk(Builder $q, string $disk, string $path): void
279
    {
280
        $q->where('disk', $disk)
12✔
281
            ->where('directory', File::cleanDirname($path))
12✔
282
            ->where('filename', pathinfo($path, PATHINFO_FILENAME))
12✔
283
            ->where('extension', pathinfo($path, PATHINFO_EXTENSION));
12✔
284
    }
285

286
    /**
287
     * Query scope to remove the order by clause from the query.
288
     * @param  Builder $q
289
     * @return void
290
     */
291
    public function scopeUnordered(Builder $q): void
292
    {
293
        $query = $q->getQuery();
6✔
294
        if ($query->orders) {
6✔
295
            $query->orders = null;
6✔
296
        }
297
    }
298

299
    public function scopeWhereIsOriginal(Builder $q): void
300
    {
301
        $q->whereNull('original_media_id');
6✔
302
    }
303

304
    public function scopeWhereIsVariant(Builder $q, string $variant_name = null): void
305
    {
306
        $q->whereNotNull('original_media_id');
6✔
307
        if ($variant_name) {
6✔
308
            $q->where('variant_name', $variant_name);
6✔
309
        }
310
    }
311

312
    /**
313
     * Calculate the file size in human readable byte notation.
314
     * @param  int $precision (_optional_) Number of decimal places to include.
315
     * @return string
316
     */
317
    public function readableSize(int $precision = 1): string
318
    {
319
        return File::readableSize($this->size, $precision);
6✔
320
    }
321

322
    /**
323
     * Get the path to the file relative to the root of the disk.
324
     * @return string
325
     */
326
    public function getDiskPath(): string
327
    {
328
        return ltrim(File::joinPathComponents((string)$this->directory, (string)$this->basename), '/');
540✔
329
    }
330

331
    /**
332
     * Get the absolute filesystem path to the file.
333
     * @return string
334
     */
335
    public function getAbsolutePath(): string
336
    {
337
        return $this->getUrlGenerator()->getAbsolutePath();
42✔
338
    }
339

340
    /**
341
     * Check if the file is located below the public webroot.
342
     * @return bool
343
     */
344
    public function isPubliclyAccessible(): bool
345
    {
346
        return $this->getUrlGenerator()->isPubliclyAccessible();
12✔
347
    }
348

349
    /**
350
     * Get the absolute URL to the media file.
351
     * @throws MediaUrlException If media's disk is not publicly accessible
352
     * @return string
353
     */
354
    public function getUrl(): string
355
    {
356
        return $this->getUrlGenerator()->getUrl();
18✔
357
    }
358

359
    public function getTemporaryUrl(\DateTimeInterface $expiry): string
360
    {
361
        $generator = $this->getUrlGenerator();
12✔
362
        if ($generator instanceof TemporaryUrlGeneratorInterface) {
12✔
363
            return $generator->getTemporaryUrl($expiry);
6✔
364
        }
365

366
        throw MediaUrlException::temporaryUrlsNotSupported($this->disk);
6✔
367
    }
368

369
    /**
370
     * Check if the file exists on disk.
371
     * @return bool
372
     */
373
    public function fileExists(): bool
374
    {
375
        return $this->storage()->exists($this->getDiskPath());
168✔
376
    }
377

378
    /**
379
     *
380
     * @return bool
381
     */
382
    public function isVisible(): bool
383
    {
384
        return $this->storage()->getVisibility($this->getDiskPath()) === 'public';
120✔
385
    }
386

387
    public function makePrivate(): void
388
    {
389
        $this->storage()->setVisibility($this->getDiskPath(), 'private');
66✔
390
    }
391

392
    public function makePublic(): void
393
    {
394
        $this->storage()->setVisibility($this->getDiskPath(), 'public');
66✔
395
    }
396

397
    /**
398
     * Retrieve the contents of the file.
399
     * @return string
400
     */
401
    public function contents(): string
402
    {
403
        return $this->storage()->get($this->getDiskPath());
144✔
404
    }
405

406
    /**
407
     * Get a read stream to the file
408
     * @return StreamInterface
409
     */
410
    public function stream(): StreamInterface
411
    {
412
        $stream = $this->storage()->readStream($this->getDiskPath());
6✔
413
        return Utils::streamFor($stream);
6✔
414
    }
415

416
    /**
417
     * Verify if the Media is an original file and not a variant
418
     * @return bool
419
     */
420
    public function isOriginal(): bool
421
    {
422
        return $this->original_media_id === null;
174✔
423
    }
424

425
    /**
426
     * Verify if the Media is a variant of another
427
     * @param string|null $variantName if specified, will check if the model if a specific kind of variant
428
     * @return bool
429
     */
430
    public function isVariant(string $variantName = null): bool
431
    {
432
        return $this->original_media_id !== null
6✔
433
            && (!$variantName || $this->variant_name === $variantName);
6✔
434
    }
435

436
    /**
437
     * Convert the model into an original.
438
     * Detaches the Media for its previous original and other variants
439
     * @return $this
440
     */
441
    public function makeOriginal(): self
442
    {
443
        if ($this->isOriginal()) {
12✔
444
            return $this;
×
445
        }
446

447
        $this->variant_name = null;
12✔
448
        $this->original_media_id = null;
12✔
449

450
        return $this;
12✔
451
    }
452

453
    /**
454
     * @param Media|string|int $media
455
     * @param string $variantName
456
     * @return $this
457
     */
458
    public function makeVariantOf($media, string $variantName): self
459
    {
460
        if (!$media instanceof self) {
12✔
461
            $media = $this->newQuery()->findOrFail($media);
12✔
462
        }
463
        /** @var Media $media */
464
        $this->variant_name = $variantName;
6✔
465
        $this->original_media_id = $media->isOriginal()
6✔
466
            ? $media->getKey()
6✔
467
            : $media->original_media_id;
6✔
468

469
        return $this;
6✔
470
    }
471

472
    /**
473
     * Move the file to a new location on disk.
474
     *
475
     * Will invoke the `save()` method on the model after the associated file has been moved to prevent synchronization errors
476
     * @param  string $destination directory relative to disk root
477
     * @param  string $filename filename. Do not include extension
478
     * @return void
479
     * @throws MediaMoveException
480
     */
481
    public function move(string $destination, string $filename = null): void
482
    {
483
        $this->getMediaMover()->move($this, $destination, $filename);
12✔
484
    }
485

486
    /**
487
     * Rename the file in place.
488
     * @param  string $filename
489
     * @return void
490
     * @see Media::move()
491
     */
492
    public function rename(string $filename): void
493
    {
494
        $this->move($this->directory, $filename);
6✔
495
    }
496

497
    /**
498
     * Copy the file from one Media object to another one.
499
     *
500
     * Will invoke the `save()` method on the model after the associated file has been copied to prevent synchronization errors
501
     * @param  string $destination directory relative to disk root
502
     * @param  string $filename optional filename. Do not include extension
503
     * @return Media
504
     * @throws MediaMoveException
505
     */
506
    public function copyTo(string $destination, string $filename = null): self
507
    {
508
        return $this->getMediaMover()->copyTo($this, $destination, $filename);
6✔
509
    }
510

511
    /**
512
     * Move the file to a new location on another disk.
513
     *
514
     * Will invoke the `save()` method on the model after the associated file has been moved to prevent synchronization errors
515
     * @param  string $disk the disk to move the file to
516
     * @param  string $destination directory relative to disk root
517
     * @param  string $filename filename. Do not include extension
518
     * @return void
519
     * @throws MediaMoveException If attempting to change the file extension or a file with the same name already exists at the destination
520
     */
521
    public function moveToDisk(
522
        string $disk,
523
        string $destination,
524
        string $filename = null,
525
        array $options = []
526
    ): void {
527
        $this->getMediaMover()
12✔
528
            ->moveToDisk($this, $disk, $destination, $filename, $options);
12✔
529
    }
530

531
    /**
532
     * Copy the file from one Media object to another one on a different disk.
533
     *
534
     * This method creates a new Media object as well as duplicates the associated file on the disk.
535
     *
536
     * @param  string $disk the disk to copy the file to
537
     * @param  string $destination directory relative to disk root
538
     * @param  string $filename optional filename. Do not include extension
539
     *
540
     * @return Media
541
     * @throws MediaMoveException If a file with the same name already exists at the destination or it fails to copy the file
542
     */
543
    public function copyToDisk(
544
        string $disk,
545
        string $destination,
546
        string $filename = null,
547
        array $options = []
548
    ): self {
549
        return $this->getMediaMover()
12✔
550
            ->copyToDisk($this, $disk, $destination, $filename, $options);
12✔
551
    }
552

553
    protected function getMediaMover(): MediaMover
554
    {
555
        return app('mediable.mover');
42✔
556
    }
557

558
    protected function handleMediaDeletion(): void
559
    {
560
        // optionally detach mediable relationships on soft delete
561
        if (static::hasGlobalScope(SoftDeletingScope::class)
72✔
562
            && (!property_exists($this, 'forceDeleting') || !$this->forceDeleting)
72✔
563
        ) {
564
            if (config('mediable.detach_on_soft_delete')) {
18✔
565
                $this->newBaseQueryBuilder()
14✔
566
                    ->from(config('mediable.mediables_table', 'mediables'))
14✔
567
                    ->where('media_id', $this->getKey())
14✔
568
                    ->delete();
14✔
569
            }
570
        } elseif ($this->storage()->exists($this->getDiskPath())) {
54✔
571
            // unlink associated file on delete
572
            $this->storage()->delete($this->getDiskPath());
24✔
573
        }
574
    }
575

576
    /**
577
     * Get the filesystem object for this media.
578
     * @return Filesystem
579
     */
580
    protected function storage(): Filesystem
581
    {
582
        return app('filesystem')->disk($this->disk);
354✔
583
    }
584

585
    /**
586
     * Get a UrlGenerator instance for the media.
587
     * @return UrlGeneratorInterface
588
     */
589
    protected function getUrlGenerator(): UrlGeneratorInterface
590
    {
591
        return app('mediable.url.factory')->create($this);
84✔
592
    }
593

594
    /**
595
     * {@inheritdoc}
596
     */
597
    public function getConnectionName()
598
    {
599
        return config('mediable.connection_name', parent::getConnectionName());
756✔
600
    }
601
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc