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

plank / laravel-mediable / 4998323082

pending completion
4998323082

push

github

GitHub
Merge pull request #318 from plank/update-psr-message-dep

9 of 9 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1201 of 1260 relevant lines covered (95.32%)

118.73 hits per line

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

97.32
/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 Carbon $created_at
39
 * @property Carbon $updated_at
40
 * @property Pivot $pivot
41
 * @property Collection|Media[] $variants
42
 * @property Media $originalMedia
43
 * @method static Builder inDirectory(string $disk, string $directory, bool $recursive = false)
44
 * @method static Builder inOrUnderDirectory(string $disk, string $directory)
45
 * @method static Builder whereBasename(string $basename)
46
 * @method static Builder forPathOnDisk(string $disk, string $path)
47
 * @method static Builder unordered()
48
 * @method static Builder whereIsOriginal()
49
 * @method static Builder whereIsVariant(string $variant_name = null)
50
 */
51
class Media extends Model
52
{
53
    const TYPE_IMAGE = 'image';
54
    const TYPE_IMAGE_VECTOR = 'vector';
55
    const TYPE_PDF = 'pdf';
56
    const TYPE_VIDEO = 'video';
57
    const TYPE_AUDIO = 'audio';
58
    const TYPE_ARCHIVE = 'archive';
59
    const TYPE_DOCUMENT = 'document';
60
    const TYPE_SPREADSHEET = 'spreadsheet';
61
    const TYPE_PRESENTATION = 'presentation';
62
    const TYPE_OTHER = 'other';
63
    const TYPE_ALL = 'all';
64

65
    const VARIANT_NAME_ORIGINAL = 'original';
66

67
    protected $table = 'media';
68

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

82
    protected $casts = [
83
        'size' => 'int',
84
    ];
85

86
    /**
87
     * {@inheritdoc}
88
     */
89
    public static function boot()
90
    {
91
        parent::boot();
948✔
92

93
        //remove file on deletion
94
        static::deleted(function (Media $media) {
948✔
95
            $media->handleMediaDeletion();
72✔
96
        });
948✔
97
    }
474✔
98

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

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

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

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

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

155
        return $collection;
6✔
156
    }
157

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

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

170
        return $collection;
6✔
171
    }
172

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

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

184
        if ($this->isOriginal()) {
150✔
185
            return $this->variants->first($filter);
144✔
186
        }
187

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

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

196
        return null;
×
197
    }
198

199
    public function findOriginal(): Media
200
    {
201
        if ($this->isOriginal()) {
132✔
202
            return $this;
126✔
203
        }
204

205
        return $this->originalMedia;
18✔
206
    }
207

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

217
    /**
218
     * Query scope for to find media in a particular directory.
219
     * @param  Builder $q
220
     * @param  string $disk Filesystem disk to search in
221
     * @param  string $directory Path relative to disk
222
     * @param  bool $recursive (_optional_) If true, will find media in or under the specified directory
223
     * @return void
224
     */
225
    public function scopeInDirectory(Builder $q, string $disk, string $directory, bool $recursive = false): void
226
    {
227
        $q->where('disk', $disk);
60✔
228
        if ($recursive) {
60✔
229
            $directory = str_replace(['%', '_'], ['\%', '\_'], $directory);
42✔
230
            $q->where('directory', 'like', $directory . '%');
42✔
231
        } else {
232
            $q->where('directory', '=', $directory);
18✔
233
        }
234
    }
30✔
235

236
    /**
237
     * Query scope for finding media in a particular directory or one of its subdirectories.
238
     * @param  Builder|Media $q
239
     * @param  string $disk Filesystem disk to search in
240
     * @param  string $directory Path relative to disk
241
     * @return void
242
     */
243
    public function scopeInOrUnderDirectory(Builder $q, string $disk, string $directory): void
244
    {
245
        $q->inDirectory($disk, $directory, true);
6✔
246
    }
3✔
247

248
    /**
249
     * Query scope for finding media by basename.
250
     * @param  Builder $q
251
     * @param  string $basename filename and extension
252
     * @return void
253
     */
254
    public function scopeWhereBasename(Builder $q, string $basename): void
255
    {
256
        $q->where('filename', pathinfo($basename, PATHINFO_FILENAME))
6✔
257
            ->where('extension', pathinfo($basename, PATHINFO_EXTENSION));
6✔
258
    }
3✔
259

260
    /**
261
     * Query scope finding media at a path relative to a disk.
262
     * @param  Builder $q
263
     * @param  string $disk
264
     * @param  string $path directory, filename and extension
265
     * @return void
266
     */
267
    public function scopeForPathOnDisk(Builder $q, string $disk, string $path): void
268
    {
269
        $q->where('disk', $disk)
12✔
270
            ->where('directory', File::cleanDirname($path))
12✔
271
            ->where('filename', pathinfo($path, PATHINFO_FILENAME))
12✔
272
            ->where('extension', pathinfo($path, PATHINFO_EXTENSION));
12✔
273
    }
6✔
274

275
    /**
276
     * Query scope to remove the order by clause from the query.
277
     * @param  Builder $q
278
     * @return void
279
     */
280
    public function scopeUnordered(Builder $q): void
281
    {
282
        $query = $q->getQuery();
6✔
283
        if ($query->orders) {
6✔
284
            $query->orders = null;
6✔
285
        }
286
    }
3✔
287

288
    public function scopeWhereIsOriginal(Builder $q): void
289
    {
290
        $q->whereNull('original_media_id');
6✔
291
    }
3✔
292

293
    public function scopeWhereIsVariant(Builder $q, string $variant_name = null)
294
    {
295
        $q->whereNotNull('original_media_id');
6✔
296
        if ($variant_name) {
6✔
297
            $q->where('variant_name', $variant_name);
6✔
298
        }
299
    }
3✔
300

301
    /**
302
     * Calculate the file size in human readable byte notation.
303
     * @param  int $precision (_optional_) Number of decimal places to include.
304
     * @return string
305
     */
306
    public function readableSize(int $precision = 1): string
307
    {
308
        return File::readableSize($this->size, $precision);
6✔
309
    }
310

311
    /**
312
     * Get the path to the file relative to the root of the disk.
313
     * @return string
314
     */
315
    public function getDiskPath(): string
316
    {
317
        return ltrim(File::joinPathComponents((string)$this->directory, (string)$this->basename), '/');
498✔
318
    }
319

320
    /**
321
     * Get the absolute filesystem path to the file.
322
     * @return string
323
     */
324
    public function getAbsolutePath(): string
325
    {
326
        return $this->getUrlGenerator()->getAbsolutePath();
42✔
327
    }
328

329
    /**
330
     * Check if the file is located below the public webroot.
331
     * @return bool
332
     */
333
    public function isPubliclyAccessible(): bool
334
    {
335
        return $this->getUrlGenerator()->isPubliclyAccessible();
12✔
336
    }
337

338
    /**
339
     * Get the absolute URL to the media file.
340
     * @throws MediaUrlException If media's disk is not publicly accessible
341
     * @return string
342
     */
343
    public function getUrl(): string
344
    {
345
        return $this->getUrlGenerator()->getUrl();
18✔
346
    }
347

348
    public function getTemporaryUrl(\DateTimeInterface $expiry): string
349
    {
350
        $generator = $this->getUrlGenerator();
12✔
351
        if ($generator instanceof TemporaryUrlGeneratorInterface) {
12✔
352
            return $generator->getTemporaryUrl($expiry);
6✔
353
        }
354

355
        throw MediaUrlException::temporaryUrlsNotSupported($this->disk);
6✔
356
    }
357

358
    /**
359
     * Check if the file exists on disk.
360
     * @return bool
361
     */
362
    public function fileExists(): bool
363
    {
364
        return $this->storage()->has($this->getDiskPath());
150✔
365
    }
366

367
    /**
368
     *
369
     * @return bool
370
     */
371
    public function isVisible(): bool
372
    {
373
        return $this->storage()->getVisibility($this->getDiskPath()) === 'public';
120✔
374
    }
375

376
    public function makePrivate(): void
377
    {
378
        $this->storage()->setVisibility($this->getDiskPath(), 'private');
66✔
379
    }
33✔
380

381
    public function makePublic(): void
382
    {
383
        $this->storage()->setVisibility($this->getDiskPath(), 'public');
66✔
384
    }
33✔
385

386
    /**
387
     * Retrieve the contents of the file.
388
     * @return string
389
     */
390
    public function contents(): string
391
    {
392
        return $this->storage()->get($this->getDiskPath());
138✔
393
    }
394

395
    /**
396
     * Get a read stream to the file
397
     * @return StreamInterface
398
     */
399
    public function stream()
400
    {
401
        $stream = $this->storage()->readStream($this->getDiskPath());
6✔
402
        if (method_exists(Utils::class, 'streamFor')) {
6✔
403
            return Utils::streamFor($stream);
6✔
404
        }
UNCOV
405
        return \GuzzleHttp\Psr7\stream_for($stream);
×
406
    }
407

408
    /**
409
     * Verify if the Media is an original file and not a variant
410
     * @return bool
411
     */
412
    public function isOriginal(): bool
413
    {
414
        return $this->original_media_id === null;
168✔
415
    }
416

417
    /**
418
     * Verify if the Media is a variant of another
419
     * @param string|null $variantName if specified, will check if the model if a specific kind of variant
420
     * @return bool
421
     */
422
    public function isVariant(string $variantName = null): bool
423
    {
424
        return $this->original_media_id !== null
6✔
425
            && (!$variantName || $this->variant_name === $variantName);
6✔
426
    }
427

428
    /**
429
     * Convert the model into an original.
430
     * Detaches the Media for its previous original and other variants
431
     * @return $this
432
     */
433
    public function makeOriginal(): self
434
    {
435
        if ($this->isOriginal()) {
12✔
436
            return $this;
×
437
        }
438

439
        $this->variant_name = null;
12✔
440
        $this->original_media_id = null;
12✔
441

442
        return $this;
12✔
443
    }
444

445
    public function makeVariantOf($media, string $variantName): self
446
    {
447
        if (!$media instanceof static) {
12✔
448
            $media = $this->newQuery()->findOrFail($media);
12✔
449
        }
450

451
        $this->variant_name = $variantName;
6✔
452
        $this->original_media_id = $media->isOriginal()
6✔
453
            ? $media->getKey()
6✔
454
            : $media->original_media_id;
6✔
455

456
        return $this;
6✔
457
    }
458

459
    /**
460
     * Move the file to a new location on disk.
461
     *
462
     * Will invoke the `save()` method on the model after the associated file has been moved to prevent synchronization errors
463
     * @param  string $destination directory relative to disk root
464
     * @param  string $filename filename. Do not include extension
465
     * @return void
466
     * @throws MediaMoveException
467
     */
468
    public function move(string $destination, string $filename = null): void
469
    {
470
        $this->getMediaMover()->move($this, $destination, $filename);
12✔
471
    }
3✔
472

473
    /**
474
     * Rename the file in place.
475
     * @param  string $filename
476
     * @return void
477
     * @see Media::move()
478
     */
479
    public function rename(string $filename): void
480
    {
481
        $this->move($this->directory, $filename);
6✔
482
    }
3✔
483

484
    /**
485
     * Copy the file from one Media object to another one.
486
     *
487
     * Will invoke the `save()` method on the model after the associated file has been copied to prevent synchronization errors
488
     * @param  string $destination directory relative to disk root
489
     * @param  string $filename optional filename. Do not include extension
490
     * @return Media
491
     * @throws MediaMoveException
492
     */
493
    public function copyTo(string $destination, string $filename = null): self
494
    {
495
        return $this->getMediaMover()->copyTo($this, $destination, $filename);
6✔
496
    }
497

498
    /**
499
     * Move the file to a new location on another disk.
500
     *
501
     * Will invoke the `save()` method on the model after the associated file has been moved to prevent synchronization errors
502
     * @param  string $disk the disk to move the file to
503
     * @param  string $directory directory relative to disk root
504
     * @param  string $filename filename. Do not include extension
505
     * @return void
506
     * @throws MediaMoveException If attempting to change the file extension or a file with the same name already exists at the destination
507
     */
508
    public function moveToDisk(
509
        string $disk,
510
        string $destination,
511
        string $filename = null,
512
        array $options = []
513
    ): void {
514
        $this->getMediaMover()
12✔
515
            ->moveToDisk($this, $disk, $destination, $filename, $options);
12✔
516
    }
6✔
517

518
    /**
519
     * Copy the file from one Media object to another one on a different disk.
520
     *
521
     * This method creates a new Media object as well as duplicates the associated file on the disk.
522
     *
523
     * @param  Media $media The media to copy from
524
     * @param  string $disk the disk to copy the file to
525
     * @param  string $directory directory relative to disk root
526
     * @param  string $filename optional filename. Do not include extension
527
     *
528
     * @return Media
529
     * @throws MediaMoveException If a file with the same name already exists at the destination or it fails to copy the file
530
     */
531
    public function copyToDisk(
532
        string $disk,
533
        string $destination,
534
        string $filename = null,
535
        array $options = []
536
    ): self {
537
        return $this->getMediaMover()
12✔
538
            ->copyToDisk($this, $disk, $destination, $filename, $options);
12✔
539
    }
540

541
    protected function getMediaMover(): MediaMover
542
    {
543
        return app('mediable.mover');
42✔
544
    }
545

546
    protected function handleMediaDeletion(): void
547
    {
548
        // optionally detach mediable relationships on soft delete
549
        if (static::hasGlobalScope(SoftDeletingScope::class) && !$this->forceDeleting) {
72✔
550
            if (config('mediable.detach_on_soft_delete')) {
18✔
551
                $this->newBaseQueryBuilder()
15✔
552
                    ->from(config('mediable.mediables_table', 'mediables'))
15✔
553
                    ->where('media_id', $this->getKey())
15✔
554
                    ->delete();
18✔
555
            }
556
        // unlink associated file on delete
557
        } elseif ($this->storage()->has($this->getDiskPath())) {
54✔
558
            $this->storage()->delete($this->getDiskPath());
24✔
559
        }
560
    }
36✔
561

562
    /**
563
     * Get the filesystem object for this media.
564
     * @return Filesystem
565
     */
566
    protected function storage(): Filesystem
567
    {
568
        return app('filesystem')->disk($this->disk);
330✔
569
    }
570

571
    /**
572
     * Get a UrlGenerator instance for the media.
573
     * @return UrlGeneratorInterface
574
     */
575
    protected function getUrlGenerator(): UrlGeneratorInterface
576
    {
577
        return app('mediable.url.factory')->create($this);
84✔
578
    }
579

580
    /**
581
     * {@inheritdoc}
582
     */
583
    public function getConnectionName()
584
    {
585
        return config('mediable.connection_name', parent::getConnectionName());
714✔
586
    }
587
}
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