Files
thrillwiki_laravel/app/Models/Park.php

329 lines
8.5 KiB
PHP

<?php
namespace App\Models;
use App\Enums\ParkStatus;
use App\Traits\HasLocation;
use App\Models\Operator;
use App\Traits\HasSlugHistory;
use App\Traits\HasParkStatistics;
use App\Traits\TrackedModel;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Park extends Model
{
use HasFactory, HasSlugHistory, HasParkStatistics, HasLocation;
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'slug',
'description',
'status',
'opening_date',
'closing_date',
'operating_season',
'size_acres',
'website',
'operator_id',
'total_areas',
'operating_areas',
'closed_areas',
'total_rides',
'total_coasters',
'total_flat_rides',
'total_water_rides',
'total_daily_capacity',
'average_wait_time',
'average_rating',
'total_rides_operated',
'total_rides_retired',
'last_expansion_date',
'last_major_update',
'utilization_rate',
'peak_daily_attendance',
'guest_satisfaction',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'status' => ParkStatus::class,
'opening_date' => 'date',
'closing_date' => 'date',
'size_acres' => 'decimal:2',
'total_areas' => 'integer',
'operating_areas' => 'integer',
'closed_areas' => 'integer',
'total_rides' => 'integer',
'total_coasters' => 'integer',
'total_flat_rides' => 'integer',
'total_water_rides' => 'integer',
'total_daily_capacity' => 'integer',
'average_wait_time' => 'integer',
'average_rating' => 'decimal:2',
'total_rides_operated' => 'integer',
'total_rides_retired' => 'integer',
'last_expansion_date' => 'date',
'last_major_update' => 'date',
'utilization_rate' => 'decimal:2',
'peak_daily_attendance' => 'integer',
'guest_satisfaction' => 'decimal:2',
];
/**
* Get the operator that owns the park.
*/
public function operator(): BelongsTo
{
return $this->belongsTo(Operator::class);
}
/**
* Get the owner that owns the park.
* @deprecated Use operator() relationship instead until Company model is implemented
*/
public function owner(): BelongsTo
{
return $this->belongsTo(Operator::class, 'owner_id');
}
/**
* Get the areas in the park.
*/
public function areas(): HasMany
{
return $this->hasMany(ParkArea::class);
}
/**
* Get the photos for the park.
*/
public function photos(): MorphMany
{
return $this->morphMany(Photo::class, 'photoable');
}
/**
* Get the featured photo for the park.
*/
public function featuredPhoto()
{
return $this->photos()->where('is_featured', true)->first()
?? $this->photos()->orderBy('position')->first();
}
/**
* Get the URL of the featured photo or a default image.
*/
public function getFeaturedPhotoUrlAttribute(): string
{
$photo = $this->featuredPhoto();
return $photo ? $photo->url : asset('images/placeholders/default-park.jpg');
}
/**
* Add a photo to the park.
*
* @param array<string, mixed> $attributes
* @return \App\Models\Photo
*/
public function addPhoto(array $attributes): Photo
{
// Set position to be the last in the collection if not specified
if (!isset($attributes['position'])) {
$lastPosition = $this->photos()->max('position') ?? 0;
$attributes['position'] = $lastPosition + 1;
}
// If this is the first photo or is_featured is true, make it featured
if ($this->photos()->count() === 0 || ($attributes['is_featured'] ?? false)) {
$attributes['is_featured'] = true;
} else {
$attributes['is_featured'] = false;
}
return $this->photos()->create($attributes);
}
/**
* Set a photo as the featured photo.
*
* @param \App\Models\Photo|int $photo
* @return bool
*/
public function setFeaturedPhoto($photo): bool
{
if (is_numeric($photo)) {
$photo = $this->photos()->findOrFail($photo);
}
return $photo->setAsFeatured();
}
/**
* Reorder photos.
*
* @param array<int, int> $photoIds Ordered array of photo IDs
* @return bool
*/
public function reorderPhotos(array $photoIds): bool
{
// Begin transaction
DB::beginTransaction();
try {
foreach ($photoIds as $position => $photoId) {
$this->photos()->where('id', $photoId)->update(['position' => $position + 1]);
}
DB::commit();
return true;
} catch (\Exception $e) {
DB::rollBack();
return false;
}
}
/**
* Get formatted website URL (ensures proper URL format).
*/
public function getWebsiteUrlAttribute(): string
{
if (!$this->website) {
return '';
}
$website = $this->website;
if (!str_starts_with($website, 'http://') && !str_starts_with($website, 'https://')) {
$website = 'https://' . $website;
}
return $website;
}
/**
* Get the status display classes for Tailwind CSS.
*/
public function getStatusClassesAttribute(): string
{
return $this->status->getStatusClasses();
}
/**
* Get a formatted display of the park's size.
*/
public function getSizeDisplayAttribute(): string
{
return $this->size_acres ? number_format($this->size_acres, 1) . ' acres' : 'Unknown size';
}
/**
* Get the formatted opening year.
*/
public function getOpeningYearAttribute(): ?string
{
return $this->opening_date?->format('Y');
}
/**
* Get a brief description suitable for cards and previews.
*/
public function getBriefDescriptionAttribute(): string
{
$description = $this->description ?? '';
return strlen($description) > 200 ? substr($description, 0, 200) . '...' : $description;
}
/**
* Scope a query to only include operating parks.
*/
public function scopeOperating($query)
{
return $query->where('status', ParkStatus::OPERATING);
}
/**
* Scope a query to only include closed parks.
*/
public function scopeClosed($query)
{
return $query->whereIn('status', [
ParkStatus::CLOSED_TEMP,
ParkStatus::CLOSED_PERM,
ParkStatus::DEMOLISHED,
]);
}
/**
* Get the formatted location for the park.
*/
public function getFormattedLocationAttribute(): string
{
return $this->formatted_address ?? '';
}
/**
* Get the absolute URL for the park detail page.
*/
public function getAbsoluteUrl(): string
{
return route('parks.show', ['slug' => $this->slug]);
}
/**
* Get a park by its current or historical slug.
*
* @param string $slug
* @return array{0: \App\Models\Park|null, 1: bool} Park and whether a historical slug was used
*/
public static function getBySlug(string $slug): array
{
// Try current slug
$park = static::where('slug', $slug)->first();
if ($park) {
return [$park, false];
}
// Try historical slug
$slugHistory = SlugHistory::where('slug', $slug)
->where('sluggable_type', static::class)
->latest()
->first();
if ($slugHistory) {
$park = static::find($slugHistory->sluggable_id);
return [$park, true];
}
return [null, false];
}
/**
* Boot the model.
*/
protected static function boot()
{
parent::boot();
static::created(function (Park $park) {
$park->operator?->updateStatistics();
});
static::deleted(function (Park $park) {
$park->operator?->updateStatistics();
});
}
}