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

conedevelopment / root / 18585608047

17 Oct 2025 07:22AM UTC coverage: 76.113% (+0.1%) from 75.974%
18585608047

push

github

iamgergo
wip

3384 of 4446 relevant lines covered (76.11%)

34.08 hits per line

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

82.11
/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\Database\Eloquent\Attributes\Scope;
17
use Illuminate\Database\Eloquent\Builder;
18
use Illuminate\Database\Eloquent\Casts\Attribute;
19
use Illuminate\Database\Eloquent\Concerns\HasUuids;
20
use Illuminate\Database\Eloquent\Factories\HasFactory;
21
use Illuminate\Database\Eloquent\Model;
22
use Illuminate\Database\Eloquent\Relations\BelongsTo;
23
use Illuminate\Http\UploadedFile;
24
use Illuminate\Support\Facades\App;
25
use Illuminate\Support\Facades\Config;
26
use Illuminate\Support\Facades\Response;
27
use Illuminate\Support\Facades\Storage;
28
use Illuminate\Support\Facades\URL;
29
use Illuminate\Support\Number;
30
use Illuminate\Support\Str;
31
use Symfony\Component\HttpFoundation\BinaryFileResponse;
32

33
class Medium extends Model implements Contract
34
{
35
    use Filterable;
36
    use HasFactory;
37
    use HasUuids;
38
    use InteractsWithProxy;
39

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

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

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

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

82
    /**
83
     * The table associated with the model.
84
     *
85
     * @var string
86
     */
87
    protected $table = 'root_media';
88

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

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

107
    /**
108
     * Create a new factory instance for the model.
109
     */
110
    protected static function newFactory(): MediumFactory
26✔
111
    {
112
        return MediumFactory::new();
26✔
113
    }
114

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

125
        if (is_callable($callback)) {
×
126
            call_user_func_array($callback, [$medium, $file]);
×
127
        }
128

129
        $medium->save();
×
130

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

133
        MoveFile::withChain($medium->convertible() ? [new PerformConversions($medium)] : [])
×
134
            ->dispatch($medium, $path, false);
×
135

136
        return $medium;
×
137
    }
138

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

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

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

161
    /**
162
     * Get the attributes that should be cast.
163
     *
164
     * @return array{'properties':'json'}
165
     */
166
    protected function casts(): array
38✔
167
    {
168
        return [
38✔
169
            'properties' => 'json',
38✔
170
        ];
38✔
171
    }
172

173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function getMorphClass(): string
×
177
    {
178
        return static::getProxiedClass();
×
179
    }
180

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

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

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

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

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

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

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

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

266
        return $this;
1✔
267
    }
268

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

276
        if (! is_null($conversion) && $conversion !== 'original') {
13✔
277
            $path = substr_replace(
2✔
278
                $path, "-{$conversion}", -(mb_strlen(Str::afterLast($path, '.')) + 1), -mb_strlen("-{$conversion}")
2✔
279
            );
2✔
280
        }
281

282
        return $absolute ? Storage::disk($this->disk)->path($path) : $path;
13✔
283
    }
284

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

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

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

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

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

327
        return $query->where($query->qualifyColumn('name'), 'like', "%{$value}%");
1✔
328
    }
329

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

© 2025 Coveralls, Inc