Files
WMS/Modules/Helpdesk/Services/AttachmentService.php
2026-09-21 10:14:57 +01:00

172 lines
5.7 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Modules\Helpdesk\Models\Attachment;
use Modules\Helpdesk\Models\Comment;
use Modules\Helpdesk\Models\Ticket;
final class AttachmentService
{
/**
* Store multiple Helpdesk attachments against a ticket or comment.
*
* Livewire create/comment forms pass TemporaryUploadedFile instances, while
* controller endpoints pass Symfony/Laravel UploadedFile instances. Both
* are supported here so uploads are never silently skipped on ticket create.
*
* @param array<int|string, mixed> $files
*/
public function storeMany(Ticket $ticket, array $files, int $userId, ?Comment $comment = null): void
{
foreach ($files as $file) {
if (! $this->isSupportedUpload($file)) {
continue;
}
$disk = (string) config('helpdesk.disk', 'public');
$originalName = $this->originalName($file);
$mimeType = $this->mimeType($file);
$size = $this->size($file);
$path = $file->store('tickets/' . $ticket->id, $disk);
if (! is_string($path) || $path === '') {
continue;
}
if ($size === 0) {
$size = (int) Storage::disk($disk)->size($path);
}
Attachment::query()->create($this->attachmentAttributes($ticket, $comment, $userId, [
'disk' => $disk,
'path' => $path,
'original_name' => $originalName,
'mime_type' => $mimeType,
'size' => $size,
]));
}
}
/**
* Delete a stored attachment and its backing file.
*/
public function delete(Attachment $attachment): void
{
Storage::disk($attachment->disk)->delete($attachment->path);
$attachment->delete();
}
/**
* Build attachment attributes while remaining compatible with older databases.
*
* @return array<string, mixed>
*/
private function attachmentAttributes(Ticket $ticket, ?Comment $comment, int $userId, array $file): array
{
$attributes = [
'ticket_id' => $ticket->id,
'comment_id' => $comment?->id,
'user_id' => $userId,
'disk' => $file['disk'],
'path' => $file['path'],
'original_name' => $file['original_name'],
'mime_type' => $file['mime_type'],
'size' => $file['size'],
];
if (Schema::hasColumn('helpdesk_attachments', 'extension')) {
$attributes['extension'] = strtolower((string) pathinfo($file['original_name'], PATHINFO_EXTENSION));
}
if (Schema::hasColumn('helpdesk_attachments', 'category')) {
$attributes['category'] = $this->categoryFor($file['original_name'], $file['mime_type']);
}
if (Schema::hasColumn('helpdesk_attachments', 'checksum_sha256') && Storage::disk($file['disk'])->exists($file['path'])) {
$stream = Storage::disk($file['disk'])->readStream($file['path']);
if (is_resource($stream)) {
$context = hash_init('sha256');
hash_update_stream($context, $stream);
$attributes['checksum_sha256'] = hash_final($context);
fclose($stream);
}
}
return $attributes;
}
/**
* Categorise uploaded files for icons, previews and safe code viewing.
*/
private function categoryFor(string $originalName, string $mimeType): string
{
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
return match (true) {
str_starts_with($mimeType, 'image/'), in_array($extension, ['png', 'jpg', 'jpeg', 'webp', 'gif'], true) => 'image',
$extension === 'pdf', $mimeType === 'application/pdf' => 'pdf',
in_array($extension, ['zip', 'tar', 'gz', '7z', 'rar'], true) => 'archive',
in_array($extension, config('helpdesk.allowed_source_extensions', []), true) => 'source',
default => 'file',
};
}
/**
* Soft-delete a stored attachment while retaining audit metadata.
*/
public function softDelete(Attachment $attachment, int $userId): void
{
DB::table('helpdesk_attachments')
->where('id', $attachment->id)
->update(array_filter([
'deleted_at' => now(),
'deleted_by' => Schema::hasColumn('helpdesk_attachments', 'deleted_by') ? $userId : null,
], static fn (mixed $value): bool => $value !== null));
}
/**
* Determine whether the value is a storeable uploaded file.
*/
private function isSupportedUpload(mixed $file): bool
{
if ($file instanceof TemporaryUploadedFile) {
return true;
}
return $file instanceof UploadedFile && $file->isValid();
}
/**
* Return the uploaded file's original client filename.
*/
private function originalName(mixed $file): string
{
return (string) $file->getClientOriginalName();
}
/**
* Return the uploaded file's reported mime type.
*/
private function mimeType(mixed $file): string
{
return (string) ($file->getClientMimeType() ?: $file->getMimeType() ?: 'application/octet-stream');
}
/**
* Return the uploaded file's byte size.
*/
private function size(mixed $file): int
{
return (int) ($file->getSize() ?: 0);
}
}