belongsToMany(User::class); } /** * The relationship between a mod and its license. */ public function license(): BelongsTo { return $this->belongsTo(License::class); } /** * The relationship between a mod and its versions. */ public function versions(): HasMany { return $this->hasMany(ModVersion::class)->orderByDesc('version'); } /** * Scope a query to include the total number of downloads for a mod. */ public function scopeWithTotalDownloads($query) { return $query->addSelect([ 'total_downloads' => ModVersion::selectRaw('SUM(downloads) AS total_downloads') ->whereColumn('mod_id', 'mods.id'), ]); } /** * The relationship between a mod and its last updated version. */ public function lastUpdatedVersion(): HasOne { return $this->hasOne(ModVersion::class) ->orderByDesc('updated_at'); } /** * The data that is searchable by Scout. */ public function toSearchableArray(): array { $latestVersion = $this->latestVersion()->with('sptVersion')->first(); return [ 'id' => (int) $this->id, 'name' => $this->name, 'slug' => $this->slug, 'description' => $this->description, 'thumbnail' => $this->thumbnail, 'featured' => $this->featured, 'created_at' => strtotime($this->created_at), 'updated_at' => strtotime($this->updated_at), 'published_at' => strtotime($this->published_at), 'latestVersion' => $latestVersion?->sptVersion->version, 'latestVersionColorClass' => $latestVersion?->sptVersion->color_class, ]; } /** * The relationship to the latest mod version, dictated by the mod version number. */ public function latestVersion(): HasOne { return $this->hasOne(ModVersion::class) ->orderByDesc('version') ->orderByDesc('updated_at') ->take(1); } /** * Determine if the model instance should be searchable. */ public function shouldBeSearchable(): bool { return ! $this->disabled; } /** * Build the URL to the mod's thumbnail. */ public function thumbnailUrl(): Attribute { return Attribute::get(function (): string { return $this->thumbnail ? Storage::disk($this->thumbnailDisk())->url($this->thumbnail) : ''; }); } /** * Get the disk where the thumbnail is stored based on the current environment. */ protected function thumbnailDisk(): string { return match (config('app.env')) { 'production' => 'r2', // Cloudflare R2 Storage default => 'public', // Local }; } /** * Scope a query by applying QueryFilter filters. */ public function scopeFilter(Builder $builder, QueryFilter $filters): Builder { return $filters->apply($builder); } /** * The attributes that should be cast to native types. */ protected function casts(): array { return [ 'featured' => 'boolean', 'contains_ai_content' => 'boolean', 'contains_ads' => 'boolean', 'disabled' => 'boolean', ]; } /** * Mutate the slug attribute to always be lower case on get and slugified on set. */ protected function slug(): Attribute { return Attribute::make( get: fn (string $value) => Str::lower($value), set: fn (string $value) => Str::slug($value), ); } }