File manager - Edit - /var/www/html/portal/app/Http/Services/Accounts/LinhasDoTempo/LinhasDoTempoService.php
Back
<?php namespace App\Http\Services\Accounts\LinhasDoTempo; use App\Enums\Accounts\LinhasDoTempoAttachmentEntity; use App\Enums\Accounts\LinhasDoTempoTimelineStatus; use App\Helpers\FileUploadHelper; use App\Http\Requests\Accounts\LinhasDoTempo\LinhasDoTempoRequest; use App\Http\Resources\Accounts\LinhasDoTempo\LinhasDoTempoResource; use App\Models\Accounts\LinhasDoTempo\Attachment; use App\Models\Accounts\LinhasDoTempo\Draft; use App\Models\Accounts\LinhasDoTempo\HistoricalPoint; use App\Models\Accounts\LinhasDoTempo\Note; use App\Models\Accounts\LinhasDoTempo\Timeline; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; /** * Persistência e operações de banco do módulo Linhas do Tempo. */ class LinhasDoTempoService { public function __construct( private readonly LinhasDoTempoResource $resource, ) {} /** * Lista timelines com filtros e ordenação (RF-01, RF-05). */ public function paginateIndex(Request $request, User $user): LengthAwarePaginator { $perPage = 15; $q = Timeline::query()->whereNull('deleted_at'); if (! $this->resource->canViewAllTimelines($user)) { $q->where('user_id', $user->id); } if ($request->filled('q')) { $term = trim((string) $request->get('q')); $q->where('name', 'like', '%' . $term . '%'); } if ($request->filled('status')) { $st = (string) $request->get('status'); if (in_array($st, [LinhasDoTempoTimelineStatus::Active->value, LinhasDoTempoTimelineStatus::Archived->value], true)) { $q->where('status', $st); } } if ($request->filled('date_from')) { $q->whereDate('event_date', '>=', $request->get('date_from')); } if ($request->filled('date_to')) { $q->whereDate('event_date', '<=', $request->get('date_to')); } if ($request->filled('tag')) { $tag = trim((string) $request->get('tag')); if ($tag !== '') { $q->whereJsonContains('tags', $tag); } } $sort = (string) $request->get('sort', 'event_date'); $dir = strtolower((string) $request->get('dir', 'desc')) === 'asc' ? 'asc' : 'desc'; if ($sort === 'name') { $q->orderBy('name', $dir); } elseif ($sort === 'created_at') { $q->orderBy('created_at', $dir); } elseif ($sort === 'updated_at') { $q->orderBy('updated_at', $dir); } else { $q->orderBy('event_date', $dir); } return $q->withCount(['historicalPoints as points_count' => function ($rel) { $rel->whereNull('deleted_at'); }]) ->withMax(['historicalPoints as last_point_date' => function ($rel) { $rel->whereNull('deleted_at'); }], 'event_date') ->paginate($perPage) ->withQueryString(); } /** * Cria linha do tempo e anexos opcionais. * * @param array<string, mixed> $data * @param list<UploadedFile>|null $files */ public function createTimeline(User $user, array $data, ?array $files): Timeline { return DB::transaction(function () use ($user, $data, $files) { $tags = $this->resource->parseTagsInput($data['tags_input'] ?? null); $timeline = new Timeline(); $timeline->user_id = $user->id; $timeline->name = $data['name']; $timeline->description = $this->resource->normalizeShortProcessField($data['description'] ?? null); $timeline->process_text = $this->resource->sanitizeProcessHtml($data['process_text']); $timeline->event_date = $data['event_date']; $timeline->tags = $tags; $timeline->status = LinhasDoTempoTimelineStatus::Active->value; $timeline->save(); $this->resource->logAudit($user->id, 'timeline', $timeline->id, 'create'); if (! empty($files)) { $this->storeAttachmentsForEntity(LinhasDoTempoAttachmentEntity::Timeline->value, $timeline->id, $user, $files, 0); } return $timeline->fresh(); }); } /** * Atualiza linha do tempo. * * @param array<string, mixed> $data * @param list<UploadedFile>|null $files */ public function updateTimeline(Timeline $timeline, User $user, array $data, ?array $files, ?array $removeAttachmentIds): Timeline { return DB::transaction(function () use ($timeline, $user, $data, $files, $removeAttachmentIds) { $old = $timeline->only(['name', 'description', 'process_text', 'event_date', 'tags', 'status']); $tags = $this->resource->parseTagsInput($data['tags_input'] ?? null); $timeline->name = $data['name']; $timeline->description = $this->resource->normalizeShortProcessField($data['description'] ?? null); $timeline->process_text = $this->resource->sanitizeProcessHtml($data['process_text']); $timeline->event_date = $data['event_date']; $timeline->tags = $tags; $timeline->save(); $this->resource->logModelChanges($user->id, $timeline, 'timeline', $old, $timeline->fresh()->toArray(), ['name', 'description', 'process_text', 'event_date', 'tags', 'status']); if (! empty($removeAttachmentIds)) { $this->softDeleteAttachmentsIfOwned($timeline, $user, $removeAttachmentIds); } if (! empty($files)) { $currentBytes = $this->sumAttachmentBytes(LinhasDoTempoAttachmentEntity::Timeline->value, $timeline->id); $this->storeAttachmentsForEntity(LinhasDoTempoAttachmentEntity::Timeline->value, $timeline->id, $user, $files, $currentBytes); } return $timeline->fresh(); }); } /** * Arquivamento (status archived). */ public function archiveTimeline(Timeline $timeline, User $user): void { DB::transaction(function () use ($timeline, $user) { $old = $timeline->status; $timeline->status = LinhasDoTempoTimelineStatus::Archived->value; $timeline->save(); $this->resource->logAudit($user->id, 'timeline', $timeline->id, 'archive', 'status', $old, $timeline->status); }); } /** * Soft delete da linha e cascata lógica em pontos, notas e vínculos de anexos. */ public function softDeleteTimeline(Timeline $timeline, User $user): void { DB::transaction(function () use ($timeline, $user) { foreach ($timeline->historicalPoints()->withTrashed()->orderBy('id')->get() as $point) { $this->softDeletePointInternal($point, $user, false); } $timeline->delete(); $this->resource->logAudit($user->id, 'timeline', $timeline->id, 'delete'); }); } /** * Cria ponto histórico. * * @param array<string, mixed> $data * @param list<UploadedFile>|null $files */ public function createHistoricalPoint(Timeline $timeline, User $user, array $data, ?array $files): HistoricalPoint { if ($this->resource->historicalPointIsDuplicate($timeline, $data['name'], $data['event_date'])) { throw ValidationException::withMessages([ 'name' => [__('Já existe um ponto com este nome e data de acontecimento.')], ]); } return DB::transaction(function () use ($timeline, $user, $data, $files) { $point = new HistoricalPoint(); $point->timeline_id = $timeline->id; $point->name = $data['name']; $point->event_date = $data['event_date']; $point->process_text = $this->resource->sanitizeProcessHtml($data['process_text']); $point->save(); $this->resource->logAudit($user->id, 'historical_point', $point->id, 'create'); if (! empty($files)) { $this->storeAttachmentsForEntity(LinhasDoTempoAttachmentEntity::HistoricalPoint->value, $point->id, $user, $files, 0); } return $point->fresh(); }); } /** * Atualiza ponto histórico. * * @param array<string, mixed> $data * @param list<UploadedFile>|null $files */ public function updateHistoricalPoint(HistoricalPoint $point, User $user, array $data, ?array $files, ?array $removeAttachmentIds): HistoricalPoint { $timeline = $point->timeline; if ($this->resource->historicalPointIsDuplicate($timeline, $data['name'], $data['event_date'], $point->id)) { throw ValidationException::withMessages([ 'name' => [__('Já existe um ponto com este nome e data de acontecimento.')], ]); } return DB::transaction(function () use ($point, $user, $data, $files, $removeAttachmentIds) { $old = $point->only(['name', 'event_date', 'process_text']); $point->name = $data['name']; $point->event_date = $data['event_date']; $point->process_text = $this->resource->sanitizeProcessHtml($data['process_text']); $point->save(); $this->resource->logModelChanges($user->id, $point, 'historical_point', $old, $point->fresh()->toArray(), ['name', 'event_date', 'process_text']); if (! empty($removeAttachmentIds)) { $this->softDeletePointAttachmentsIfOwned($point, $user, $removeAttachmentIds); } if (! empty($files)) { $bytes = $this->sumAttachmentBytes(LinhasDoTempoAttachmentEntity::HistoricalPoint->value, $point->id); $this->storeAttachmentsForEntity(LinhasDoTempoAttachmentEntity::HistoricalPoint->value, $point->id, $user, $files, $bytes); } return $point->fresh(); }); } /** * Soft delete de ponto histórico. */ public function softDeleteHistoricalPoint(HistoricalPoint $point, User $user): void { DB::transaction(function () use ($point, $user) { $this->softDeletePointInternal($point, $user, true); }); } private function softDeletePointInternal(HistoricalPoint $point, User $user, bool $logTimelineContext): void { foreach ($point->notes()->get() as $note) { $note->delete(); } Attachment::query() ->where('entity_type', LinhasDoTempoAttachmentEntity::HistoricalPoint->value) ->where('entity_id', $point->id) ->delete(); $point->delete(); if ($logTimelineContext) { $this->resource->logAudit($user->id, 'historical_point', $point->id, 'delete'); } } /** * Cria nota em ponto histórico (máx. 50 por ponto). */ public function createNote(HistoricalPoint $point, User $user, string $content): Note { $count = Note::query()->where('historical_point_id', $point->id)->whereNull('deleted_at')->count(); if ($count >= 50) { throw ValidationException::withMessages([ 'content' => [__('Limite de 50 notas por ponto histórico atingido.')], ]); } return DB::transaction(function () use ($point, $user, $content) { $note = new Note(); $note->historical_point_id = $point->id; $note->author_id = $user->id; $note->content = $this->resource->sanitizeProcessHtml($content); $note->save(); $this->resource->logAudit($user->id, 'note', $note->id, 'create'); return $note->fresh(['author']); }); } public function updateNote(Note $note, User $user, string $content): Note { $this->resource->assertNoteIsMutable($note); return DB::transaction(function () use ($note, $user, $content) { $old = $note->content; $note->content = $this->resource->sanitizeProcessHtml($content); $note->save(); $this->resource->logAudit($user->id, 'note', $note->id, 'update', 'content', $old, $note->content); return $note->fresh(['author']); }); } public function softDeleteNote(Note $note, User $user): void { $this->resource->assertNoteIsMutable($note); DB::transaction(function () use ($note, $user) { $note->delete(); $this->resource->logAudit($user->id, 'note', $note->id, 'delete'); }); } /** * Salva ou atualiza rascunho (RF-13). * * @param array<string, mixed> $formData */ public function upsertDraft(User $user, string $formType, ?string $entityId, ?string $timelineId, array $formData): Draft { $expires = now()->addDays(7); $q = Draft::query()->where('user_id', $user->id)->where('form_type', $formType); if ($entityId === null) { $q->whereNull('entity_id'); } else { $q->where('entity_id', $entityId); } if ($timelineId === null) { $q->whereNull('timeline_id'); } else { $q->where('timeline_id', $timelineId); } $draft = $q->first() ?? new Draft(); $draft->user_id = $user->id; $draft->form_type = $formType; $draft->entity_id = $entityId; $draft->timeline_id = $timelineId; $draft->form_data = $formData; $draft->expires_at = $expires; $draft->save(); return $draft; } public function findDraft(User $user, string $formType, ?string $entityId, ?string $timelineId = null): ?Draft { $q = Draft::query() ->where('user_id', $user->id) ->where('form_type', $formType) ->where(function ($q2) use ($entityId) { if ($entityId === null) { $q2->whereNull('entity_id'); } else { $q2->where('entity_id', $entityId); } }); if ($timelineId === null) { $q->whereNull('timeline_id'); } else { $q->where('timeline_id', $timelineId); } return $q->where('expires_at', '>', now()) ->orderByDesc('updated_at') ->first(); } public function deleteDraft(User $user, string $formType, ?string $entityId, ?string $timelineId = null): void { $q = Draft::query() ->where('user_id', $user->id) ->where('form_type', $formType) ->where(function ($q2) use ($entityId) { if ($entityId === null) { $q2->whereNull('entity_id'); } else { $q2->where('entity_id', $entityId); } }); if ($timelineId === null) { $q->whereNull('timeline_id'); } else { $q->where('timeline_id', $timelineId); } $q->delete(); } /** * @param list<UploadedFile> $files */ public function appendTimelineAttachments(Timeline $timeline, User $user, array $files): void { DB::transaction(function () use ($timeline, $user, $files) { $this->assertTimelineWritable($timeline, $user); $bytes = $this->sumAttachmentBytes(LinhasDoTempoAttachmentEntity::Timeline->value, $timeline->id); $this->storeAttachmentsForEntity(LinhasDoTempoAttachmentEntity::Timeline->value, $timeline->id, $user, $files, $bytes); }); } /** * @param list<UploadedFile> $files */ public function appendPointAttachments(HistoricalPoint $point, User $user, array $files): void { DB::transaction(function () use ($point, $user, $files) { $this->assertTimelineWritable($point->timeline, $user); $bytes = $this->sumAttachmentBytes(LinhasDoTempoAttachmentEntity::HistoricalPoint->value, $point->id); $this->storeAttachmentsForEntity(LinhasDoTempoAttachmentEntity::HistoricalPoint->value, $point->id, $user, $files, $bytes); }); } /** * @param list<UploadedFile> $files */ private function storeAttachmentsForEntity(string $entityType, string $entityId, User $user, array $files, int $existingBytes): void { $maxTotalMb = (int) config('linhas_do_tempo.max_point_total_mb', 200); $maxTotalBytes = $maxTotalMb * 1024 * 1024; $sum = $existingBytes; foreach ($files as $file) { if (! $file instanceof UploadedFile) { continue; } $this->assertFileAllowed($file); $sum += $file->getSize(); if ($sum > $maxTotalBytes) { throw ValidationException::withMessages([ 'attachments' => [__('O tamanho total dos anexos excede o limite permitido para esta entidade.')], ]); } } $disk = config('filesystems.default', 'local'); foreach ($files as $file) { if (! $file instanceof UploadedFile) { continue; } $this->assertFileAllowed($file); $ext = strtolower($file->getClientOriginalExtension() ?: 'bin'); $storageKey = 'linhas_do_tempo/' . Str::uuid()->toString() . '.' . $ext; Storage::disk($disk)->put($storageKey, file_get_contents($file->getRealPath())); Attachment::query()->create([ 'entity_type' => $entityType, 'entity_id' => $entityId, 'original_name' => $file->getClientOriginalName(), 'storage_key' => $storageKey, 'mime_type' => (string) ($file->getMimeType() ?: 'application/octet-stream'), 'file_size_bytes' => $file->getSize(), 'uploaded_by' => $user->id, ]); } } private function assertFileAllowed(UploadedFile $file): void { $ext = strtolower($file->getClientOriginalExtension() ?: ''); $allowed = LinhasDoTempoRequest::allowedExtensions(); if (! in_array($ext, $allowed, true)) { throw ValidationException::withMessages([ 'attachments' => [__('Tipo de arquivo não permitido.')], ]); } if (FileUploadHelper::containsDangerousContent($file)) { throw ValidationException::withMessages([ 'attachments' => [__('Arquivo rejeitado por motivo de segurança.')], ]); } } private function sumAttachmentBytes(string $entityType, string $entityId): int { return (int) Attachment::query() ->where('entity_type', $entityType) ->where('entity_id', $entityId) ->whereNull('deleted_at') ->sum('file_size_bytes'); } /** * @param list<string> $ids */ private function softDeleteAttachmentsIfOwned(Timeline $timeline, User $user, array $ids): void { $this->assertTimelineWritable($timeline, $user); Attachment::query() ->whereIn('id', $ids) ->where('entity_type', LinhasDoTempoAttachmentEntity::Timeline->value) ->where('entity_id', $timeline->id) ->delete(); } /** * @param list<string> $ids */ private function softDeletePointAttachmentsIfOwned(HistoricalPoint $point, User $user, array $ids): void { $timeline = $point->timeline; $this->assertTimelineWritable($timeline, $user); Attachment::query() ->whereIn('id', $ids) ->where('entity_type', LinhasDoTempoAttachmentEntity::HistoricalPoint->value) ->where('entity_id', $point->id) ->delete(); } public function assertAttachmentDownloadable(Timeline $timeline, Attachment $attachment, User $user): void { $this->assertTimelineVisible($timeline, $user); if ($attachment->entity_type === LinhasDoTempoAttachmentEntity::Timeline->value) { if ($attachment->entity_id !== $timeline->id) { abort(404); } return; } if ($attachment->entity_type === LinhasDoTempoAttachmentEntity::HistoricalPoint->value) { $belongs = HistoricalPoint::query() ->where('id', $attachment->entity_id) ->where('timeline_id', $timeline->id) ->exists(); if (! $belongs) { abort(404); } return; } abort(404); } public function assertTimelineWritable(Timeline $timeline, User $user): void { if (! $this->resource->canManageTimeline($user, $timeline)) { abort(403); } } public function assertTimelineVisible(Timeline $timeline, User $user): void { if (! $this->resource->canViewTimeline($user, $timeline)) { abort(403); } } public function assertPointBelongsToTimeline(HistoricalPoint $point, Timeline $timeline): void { if ($point->timeline_id !== $timeline->id) { abort(404); } } public function assertNoteBelongsToPoint(Note $note, HistoricalPoint $point): void { if ($note->historical_point_id !== $point->id) { abort(404); } } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings