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

conedevelopment / root / 20774894827

07 Jan 2026 08:12AM UTC coverage: 76.466% (+1.8%) from 74.636%
20774894827

push

github

iamgergo
add coverage

3574 of 4674 relevant lines covered (76.47%)

45.03 hits per line

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

76.77
/src/Models/Medium.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Root\Models;
6

7
use Closure;
8
use Cone\Root\Database\Factories\MediumFactory;
9
use Cone\Root\Interfaces\Models\Medium as Contract;
10
use Cone\Root\Interfaces\Models\User;
11
use Cone\Root\Jobs\MoveFile;
12
use Cone\Root\Jobs\PerformConversions;
13
use Cone\Root\Support\Facades\Conversion;
14
use Cone\Root\Traits\Filterable;
15
use Cone\Root\Traits\InteractsWithProxy;
16
use Illuminate\Contracts\Mail\Attachable;
17
use Illuminate\Database\Eloquent\Attributes\Scope;
18
use Illuminate\Database\Eloquent\Builder;
19
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
20
use Illuminate\Database\Eloquent\Casts\Attribute;
21
use Illuminate\Database\Eloquent\Concerns\HasUuids;
22
use Illuminate\Database\Eloquent\Factories\HasFactory;
23
use Illuminate\Database\Eloquent\Model;
24
use Illuminate\Database\Eloquent\Relations\BelongsTo;
25
use Illuminate\Http\UploadedFile;
26
use Illuminate\Mail\Attachment;
27
use Illuminate\Support\Facades\App;
28
use Illuminate\Support\Facades\Config;
29
use Illuminate\Support\Facades\Response;
30
use Illuminate\Support\Facades\Storage;
31
use Illuminate\Support\Facades\URL;
32
use Illuminate\Support\Number;
33
use Illuminate\Support\Str;
34
use Symfony\Component\HttpFoundation\BinaryFileResponse;
35

36
class Medium extends Model implements Attachable, Contract
37
{
38
    use Filterable;
39
    use HasFactory;
40
    use HasUuids;
41
    use InteractsWithProxy;
42

43
    /**
44
     * The accessors to append to the model's array form.
45
     *
46
     * @var list<string>
47
     */
48
    protected $appends = [
49
        'is_image',
50
        'urls',
51
    ];
52

53
    /**
54
     * The attributes that should have default values.
55
     *
56
     * @var array<string, string>
57
     */
58
    protected $attributes = [
59
        'properties' => '{"conversions":[]}',
60
    ];
61

62
    /**
63
     * The attributes that are mass assignable.
64
     *
65
     * @var list<string>
66
     */
67
    protected $fillable = [
68
        'disk',
69
        'file_name',
70
        'height',
71
        'mime_type',
72
        'name',
73
        'properties',
74
        'size',
75
        'width',
76
    ];
77

78
    /**
79
     * The number of models to return for pagination.
80
     *
81
     * @var int
82
     */
83
    protected $perPage = 25;
84

85
    /**
86
     * The table associated with the model.
87
     *
88
     * @var string
89
     */
90
    protected $table = 'root_media';
91

92
    /**
93
     * The "booted" method of the model.
94
     */
95
    protected static function booted(): void
50✔
96
    {
97
        static::deleting(static function (self $medium): void {
50✔
98
            Storage::disk($medium->disk)->deleteDirectory($medium->uuid);
4✔
99
        });
50✔
100
    }
101

102
    /**
103
     * Get the proxied interface.
104
     */
105
    public static function getProxiedInterface(): string
1✔
106
    {
107
        return Contract::class;
1✔
108
    }
109

110
    /**
111
     * Create a new factory instance for the model.
112
     */
113
    protected static function newFactory(): MediumFactory
38✔
114
    {
115
        return MediumFactory::new();
38✔
116
    }
117

118
    /**
119
     * Upload the given file.
120
     */
121
    public static function upload(UploadedFile $file, ?Closure $callback = null): static
×
122
    {
123
        $medium = static::fromPath($file->getPathname(), [
×
124
            'file_name' => $file->getClientOriginalName(),
×
125
            'name' => pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME),
×
126
        ]);
×
127

128
        if (is_callable($callback)) {
×
129
            call_user_func_array($callback, [$medium, $file]);
×
130
        }
131

132
        $medium->save();
×
133

134
        $path = Storage::disk('local')->path(Storage::disk('local')->putFile('root-tmp', $file));
×
135

136
        MoveFile::withChain($medium->convertible() ? [new PerformConversions($medium)] : [])
×
137
            ->dispatch($medium, $path, false);
×
138

139
        return $medium;
×
140
    }
141

142
    /**
143
     * Make a new medium instance from the given path.
144
     */
145
    public static function fromPath(string $path, array $attributes = []): static
2✔
146
    {
147
        $type = mime_content_type($path);
2✔
148

149
        if (! Str::is('image/svg*', $type) && Str::is('image/*', $type)) {
2✔
150
            [$width, $height] = getimagesize($path);
2✔
151
        }
152

153
        return new static(array_merge([
2✔
154
            'file_name' => $name = basename($path),
2✔
155
            'mime_type' => $type,
2✔
156
            'width' => $width ?? null,
2✔
157
            'height' => $height ?? null,
2✔
158
            'disk' => Config::get('root.media.disk', 'public'),
2✔
159
            'size' => max(round(filesize($path) / 1024), 1),
2✔
160
            'name' => pathinfo($name, PATHINFO_FILENAME),
2✔
161
        ], $attributes));
2✔
162
    }
163

164
    /**
165
     * Get the attributes that should be cast.
166
     *
167
     * @return array{'values':'\Illuminate\Database\Eloquent\Casts\AsArrayObject'}
168
     */
169
    protected function casts(): array
50✔
170
    {
171
        return [
50✔
172
            'properties' => AsArrayObject::class,
50✔
173
        ];
50✔
174
    }
175

176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function getMorphClass(): string
×
180
    {
181
        return static::getProxiedClass();
×
182
    }
183

184
    /**
185
     * Get the columns that should receive a unique identifier.
186
     */
187
    public function uniqueIds(): array
42✔
188
    {
189
        return ['uuid'];
42✔
190
    }
191

192
    /**
193
     * Get the user for the medium.
194
     */
195
    public function user(): BelongsTo
2✔
196
    {
197
        return $this->belongsTo(App::make(User::class)::class);
2✔
198
    }
199

200
    /**
201
     * Determine if the file is image.
202
     *
203
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<bool, never>
204
     */
205
    protected function isImage(): Attribute
5✔
206
    {
207
        return new Attribute(
5✔
208
            get: static fn (mixed $value, array $attributes): bool => Str::is('image/*', $attributes['mime_type'])
5✔
209
        );
5✔
210
    }
211

212
    /**
213
     * Get the conversion URLs.
214
     *
215
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<array<string, string>, never>
216
     */
217
    protected function urls(): Attribute
3✔
218
    {
219
        return new Attribute(
3✔
220
            get: fn (): array => array_reduce(
3✔
221
                $this->properties['conversions'] ?? [],
3✔
222
                fn (array $urls, string $conversion): array => array_merge($urls, [$conversion => $this->getUrl($conversion)]),
3✔
223
                ['original' => $this->getUrl()]
3✔
224
            )
3✔
225
        );
3✔
226
    }
227

228
    /**
229
     * Get the formatted size attribute.
230
     *
231
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
232
     */
233
    protected function formattedSize(): Attribute
1✔
234
    {
235
        return new Attribute(
1✔
236
            get: fn (): string => Number::fileSize($this->size ?: 0)
1✔
237
        );
1✔
238
    }
239

240
    /**
241
     * Get the dimensions attribute.
242
     *
243
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string|null, never>
244
     */
245
    protected function dimensions(): Attribute
1✔
246
    {
247
        return new Attribute(
1✔
248
            get: static fn (mixed $value, array $attributes): ?string => isset($attributes['width'], $attributes['height'])
1✔
249
                ? sprintf('%dx%d px', $attributes['width'], $attributes['height'])
×
250
                : null
×
251
        );
1✔
252
    }
253

254
    /**
255
     * Determine if the medium should is convertible.
256
     */
257
    public function convertible(): bool
2✔
258
    {
259
        return $this->isImage && ! Str::is(['image/svg*', 'image/gif'], $this->mime_type);
2✔
260
    }
261

262
    /**
263
     * Perform the conversions on the medium.
264
     */
265
    public function convert(): static
1✔
266
    {
267
        Conversion::perform($this);
1✔
268

269
        return $this;
1✔
270
    }
271

272
    /**
273
     * Get the path to the conversion.
274
     */
275
    public function getPath(?string $conversion = null, bool $absolute = false): string
16✔
276
    {
277
        $path = sprintf('%s/%s', $this->uuid, $this->file_name);
16✔
278

279
        if (! is_null($conversion) && $conversion !== 'original') {
16✔
280
            $path = substr_replace(
2✔
281
                $path, "-{$conversion}", -(mb_strlen(Str::afterLast($path, '.')) + 1), -mb_strlen("-{$conversion}")
2✔
282
            );
2✔
283
        }
284

285
        return $absolute ? Storage::disk($this->disk)->path($path) : $path;
16✔
286
    }
287

288
    /**
289
     * Get the full path to the conversion.
290
     */
291
    public function getAbsolutePath(?string $conversion = null): string
8✔
292
    {
293
        return $this->getPath($conversion, true);
8✔
294
    }
295

296
    /**
297
     * Get the url to the conversion.
298
     */
299
    public function getUrl(?string $conversion = null): string
5✔
300
    {
301
        return URL::to(Storage::disk($this->disk)->url($this->getPath($conversion)));
5✔
302
    }
303

304
    /**
305
     * Check if the medium has the given conversion.
306
     */
307
    public function hasConversion(string $conversion): bool
×
308
    {
309
        return in_array($conversion, $this->properties['conversions'] ?? []);
×
310
    }
311

312
    /**
313
     * Download the medium.
314
     */
315
    public function download(?string $conversion = null): BinaryFileResponse
1✔
316
    {
317
        return Response::download($this->getAbsolutePath($conversion));
1✔
318
    }
319

320
    /**
321
     * Scope the query only to the given search term.
322
     */
323
    #[Scope]
1✔
324
    protected function search(Builder $query, string $value): Builder
325
    {
326
        if (is_null($value)) {
1✔
327
            return $query;
×
328
        }
329

330
        return $query->where($query->qualifyColumn('name'), 'like', "%{$value}%");
1✔
331
    }
332

333
    /**
334
     * Scope the query only to the given type.
335
     */
336
    #[Scope]
1✔
337
    protected function type(Builder $query, string $value): Builder
338
    {
339
        return match ($value) {
1✔
340
            'image' => $query->where($query->qualifyColumn('mime_type'), 'like', 'image%'),
1✔
341
            'file' => $query->where($query->qualifyColumn('mime_type'), 'not like', 'image%'),
1✔
342
            default => $query,
1✔
343
        };
1✔
344
    }
345

346
    /**
347
     * Get an attachment instance for this entity.
348
     */
349
    public function toMailAttachment(): Attachment
×
350
    {
351
        return Attachment::fromPath($this->getAbsolutePath())
×
352
            ->as($this->file_name)
×
353
            ->mime($this->mime_type);
×
354
    }
355
}
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