Files
Helpddesk/Modules/Helpdesk/Models/Attachment.php
2026-09-02 17:08:57 +00:00

256 lines
7.4 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
final class Attachment extends Model
{
use HasFactory;
protected $table = 'helpdesk_attachments';
/** @var list<string> */
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<string, string> */
protected $casts = [
'size' => 'integer',
'download_count' => 'integer',
'last_downloaded_at' => 'datetime',
'deleted_at' => 'datetime',
];
/** @return BelongsTo<Ticket, Attachment> */
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
/** @return BelongsTo<Comment, Attachment> */
public function comment(): BelongsTo
{
return $this->belongsTo(Comment::class, 'comment_id');
}
/** @return BelongsTo<User, Attachment> */
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'));
}
}