*/ protected $fillable = [ 'ticket_id', 'comment_id', 'user_id', 'disk', 'path', 'original_name', 'mime_type', 'size', 'extension', 'category', 'checksum_sha256', 'download_count', 'last_downloaded_at', 'deleted_at', 'deleted_by', ]; /** @var array */ protected $casts = [ 'size' => 'integer', 'download_count' => 'integer', 'last_downloaded_at' => 'datetime', 'deleted_at' => 'datetime', ]; /** @return BelongsTo */ public function ticket(): BelongsTo { return $this->belongsTo(Ticket::class, 'ticket_id'); } /** @return BelongsTo */ public function comment(): BelongsTo { return $this->belongsTo(Comment::class, 'comment_id'); } /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } /** * Return an attribute only when it was actually selected from the database. * * Laravel 13 can throw when accessing missing attributes. The Helpdesk * attachment metadata columns are introduced by an optional forward * migration, so model helpers must remain safe before that migration has * been applied. */ private function loadedAttribute(string $key): mixed { $attributes = $this->getAttributes(); return array_key_exists($key, $attributes) ? $this->getAttributeValue($key) : null; } /** * Return the original client filename with compatibility fallbacks. */ public function displayName(): string { $name = $this->loadedAttribute('original_name') ?? $this->loadedAttribute('original_filename') ?? $this->loadedAttribute('filename') ?? $this->loadedAttribute('file_name'); if (is_string($name) && $name !== '') { return $name; } return basename((string) ($this->loadedAttribute('path') ?? $this->loadedAttribute('file_path') ?? 'attachment')); } /** * Return the stored byte size, falling back to legacy names. */ public function byteSize(): int { return (int) ($this->loadedAttribute('size') ?? $this->loadedAttribute('size_bytes') ?? 0); } /** * Return the safe download count when the column exists. */ public function downloadCount(): int { return (int) ($this->loadedAttribute('download_count') ?? 0); } /** * Determine whether the attachment has been soft deleted. */ public function isDeletedAttachment(): bool { return filled($this->loadedAttribute('deleted_at')); } /** * Return the lower-case file extension from stored metadata or filename. */ public function extension(): string { $extension = $this->loadedAttribute('extension'); if (! is_string($extension) || $extension === '') { $extension = pathinfo($this->displayName(), PATHINFO_EXTENSION); } return strtolower(ltrim((string) $extension, '.')); } /** * Return the display category used by Helpdesk attachment cards. */ public function category(): string { $category = $this->loadedAttribute('category'); if (is_string($category) && $category !== '') { return $category; } return match (true) { $this->isImage() => 'image', $this->isPdf() => 'pdf', $this->isArchive() => 'archive', $this->isSourceCode() => 'source', default => 'file', }; } /** * Determine whether the attachment is a browser-previewable image. */ public function isImage(): bool { return in_array($this->extension(), ['png', 'jpg', 'jpeg', 'webp', 'gif'], true) || Str::startsWith((string) $this->loadedAttribute('mime_type'), 'image/'); } /** * Determine whether the attachment is a PDF document. */ public function isPdf(): bool { return $this->extension() === 'pdf' || $this->loadedAttribute('mime_type') === 'application/pdf'; } /** * Determine whether the attachment is an archive. */ public function isArchive(): bool { return in_array($this->extension(), ['zip', 'tar', 'gz', '7z', 'rar'], true); } /** * Determine whether the attachment is source/configuration text safe for the read-only viewer. */ public function isSourceCode(): bool { return in_array($this->extension(), [ 'php', 'phtml', 'phps', 'blade.php', 'js', 'jsx', 'ts', 'tsx', 'vue', 'css', 'scss', 'sass', 'less', 'html', 'htm', 'xml', 'json', 'yaml', 'yml', 'sql', 'sh', 'bash', 'zsh', 'ps1', 'bat', 'cmd', 'py', 'rb', 'go', 'rs', 'java', 'kt', 'kts', 'cs', 'cpp', 'c', 'h', 'hpp', 'swift', 'dart', 'rpg', 'rpgle', 'cl', 'clle', 'cbl', 'cob', 'copybook', 'inc', 'env', 'ini', 'conf', 'config', 'txt', 'log', 'md', ], true); } /** * Determine whether the attachment can be streamed inline by the preview route. */ public function isPreviewableBinary(): bool { return $this->isImage() || $this->isPdf(); } /** * Determine whether the Helpdesk UI can show an inline preview or viewer link. */ public function canPreview(): bool { return $this->isSourceCode() || $this->isPreviewableBinary(); } /** * Return a Font Awesome icon for the attachment type. */ public function icon(): string { return match ($this->category()) { 'image' => 'fa-regular fa-file-image', 'pdf' => 'fa-regular fa-file-pdf', 'source' => 'fa-solid fa-code', 'archive' => 'fa-regular fa-file-zipper', default => 'fa-regular fa-file-lines', }; } /** * Return the syntax language key used by the source viewer. */ public function language(): string { return match ($this->extension()) { 'php', 'phtml', 'phps', 'blade.php' => 'php', 'js', 'jsx' => 'javascript', 'ts', 'tsx' => 'typescript', 'vue' => 'vue', 'css', 'scss', 'sass', 'less' => 'css', 'html', 'htm' => 'html', 'xml' => 'xml', 'json' => 'json', 'yaml', 'yml' => 'yaml', 'sql' => 'sql', 'sh', 'bash', 'zsh' => 'shell', 'ps1' => 'powershell', 'py' => 'python', 'java', 'kt', 'kts' => 'java', 'c', 'h', 'cpp', 'hpp', 'cs' => 'c-like', 'rpg', 'rpgle' => 'rpgle', 'cl', 'clle' => 'clle', 'cbl', 'cob', 'copybook' => 'cobol', default => 'text', }; } public function url(): string { return Storage::disk((string) $this->loadedAttribute('disk'))->url((string) $this->loadedAttribute('path')); } }