with(['category', 'attachments'])->latest(); if ($request->filled('status')) { $query->where('status', $request->string('status')->toString()); } if ($request->filled('category')) { $query->where('documentation_category_id', $request->integer('category')); } if ($request->filled('search')) { $search = '%'.$request->string('search')->trim()->toString().'%'; $query->where(function ($builder) use ($search): void { $builder->where('title', 'like', $search) ->orWhere('summary', 'like', $search) ->orWhere('reference', 'like', $search) ->orWhere('body', 'like', $search); }); } return view('documentation::documents.index', [ 'documents' => $query->paginate(15)->withQueryString(), 'categories' => DocumentationCategory::query()->ordered()->get(), 'featuredDocuments' => DocumentationDocument::query()->with('category')->featured()->latest()->limit(4)->get(), 'stats' => [ 'total' => DocumentationDocument::query()->count(), 'published' => DocumentationDocument::query()->published()->count(), 'drafts' => DocumentationDocument::query()->where('status', DocumentationDocument::STATUS_DRAFT)->count(), 'categories' => DocumentationCategory::query()->count(), ], 'statuses' => config('documentation.statuses', []), ]); } /** * Show the create document form. */ public function create(): View { return view('documentation::documents.create', [ 'document' => new DocumentationDocument(['status' => DocumentationDocument::STATUS_DRAFT, 'version' => '1.0.0']), 'categories' => DocumentationCategory::query()->active()->ordered()->get(), 'statuses' => config('documentation.statuses', []), ]); } /** * Store a newly created document. */ public function store(StoreDocumentationDocumentRequest $request): RedirectResponse { $data = $request->validated(); $data['is_featured'] = $request->boolean('is_featured'); $data['created_by'] = $request->user()?->id; $data['updated_by'] = $request->user()?->id; if ($request->hasFile('attachment')) { if (blank($data['body'] ?? null)) { $data['body'] = $this->extractMarkdownBody($request) ?: ($data['body'] ?? null); } $data = array_merge($data, $this->storeAttachment($request)); } unset($data['attachment'], $data['attachments']); $document = DocumentationDocument::query()->create($data); $this->storeAttachments($request, $document); return redirect()->route('documentation.documents.show', $document)->with('status', 'Document created successfully.'); } /** * Display a document. */ public function show(DocumentationDocument $document): View { return view('documentation::documents.show', [ 'document' => $document->load(['category', 'attachments']), ]); } /** * Show the edit document form. */ public function edit(DocumentationDocument $document): View { return view('documentation::documents.edit', [ 'document' => $document->load('attachments'), 'categories' => DocumentationCategory::query()->active()->ordered()->get(), 'statuses' => config('documentation.statuses', []), ]); } /** * Update an existing document. */ public function update(UpdateDocumentationDocumentRequest $request, DocumentationDocument $document): RedirectResponse { $data = $request->validated(); $data['is_featured'] = $request->boolean('is_featured'); $data['updated_by'] = $request->user()?->id; if ($request->boolean('remove_attachment')) { $this->deleteAttachment($document); $data = array_merge($data, $this->emptyAttachmentData()); } if ($request->hasFile('attachment')) { $this->deleteAttachment($document); if (blank($data['body'] ?? null)) { $data['body'] = $this->extractMarkdownBody($request) ?: ($data['body'] ?? null); } $data = array_merge($data, $this->storeAttachment($request)); } unset($data['attachment'], $data['attachments'], $data['remove_attachment']); $document->update($data); $this->storeAttachments($request, $document); return redirect()->route('documentation.documents.show', $document)->with('status', 'Document updated successfully.'); } /** * Remove a document. */ public function destroy(DocumentationDocument $document): RedirectResponse { $document->load('attachments'); $this->deleteAttachment($document); foreach ($document->attachments as $attachment) { Storage::disk($attachment->disk)->delete($attachment->path); $attachment->delete(); } $document->delete(); return redirect()->route('documentation.documents.index')->with('status', 'Document archived successfully.'); } /** * Download the attached document file. */ public function download(DocumentationDocument $document) { abort_if(blank($document->attachment_path), 404); return Storage::disk($document->attachment_disk ?: config('documentation.storage_disk', 'public')) ->download($document->attachment_path, $document->attachment_name); } /** * Download one of the document's additional attachments. */ public function downloadAttachment(DocumentationDocument $document, DocumentationAttachment $attachment) { abort_if($attachment->documentation_document_id !== $document->id, 404); return Storage::disk($attachment->disk)->download($attachment->path, $attachment->original_name); } /** * Delete one of the document's additional attachments. */ public function destroyAttachment(DocumentationDocument $document, DocumentationAttachment $attachment): RedirectResponse { abort_if($attachment->documentation_document_id !== $document->id, 404); Storage::disk($attachment->disk)->delete($attachment->path); $attachment->delete(); return redirect()->route('documentation.documents.edit', $document)->with('status', 'Attachment removed successfully.'); } /** * Extract body content from an uploaded Markdown file when requested. */ private function extractMarkdownBody(Request $request): ?string { if (! config('documentation.import_markdown_attachments', true)) { return null; } $file = $request->file('attachment'); if ($file === null || ! $this->isMarkdownUpload($request)) { return null; } if ($file->getSize() > (int) config('documentation.max_markdown_import_bytes', 1048576)) { return null; } $content = file_get_contents($file->getRealPath()); if ($content === false) { return null; } return trim($content); } /** * Determine whether the uploaded attachment is a Markdown file. */ private function isMarkdownUpload(Request $request): bool { $file = $request->file('attachment'); if ($file === null) { return false; } $extension = strtolower($file->getClientOriginalExtension()); $mime = strtolower((string) $file->getMimeType()); return $extension === 'md' || in_array($mime, ['text/markdown', 'text/x-markdown'], true); } /** * Store an uploaded attachment and return persisted metadata. * * @return array */ private function storeAttachment(Request $request): array { $file = $request->file('attachment'); $disk = config('documentation.storage_disk', 'public'); $path = $file->store(config('documentation.upload_path', 'documentation'), $disk); return [ 'attachment_disk' => $disk, 'attachment_path' => $path, 'attachment_name' => $file->getClientOriginalName(), 'attachment_mime' => $file->getMimeType(), 'attachment_size' => $file->getSize(), ]; } /** * Store all additional uploaded attachments for a document. */ private function storeAttachments(Request $request, DocumentationDocument $document): void { if (! $request->hasFile('attachments')) { return; } $disk = config('documentation.storage_disk', 'public'); foreach ((array) $request->file('attachments') as $file) { if ($file === null || ! $file->isValid()) { continue; } $path = $file->store(config('documentation.upload_path', 'documentation'), $disk); $document->attachments()->create([ 'disk' => $disk, 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), 'uploaded_by' => $request->user()?->id, ]); } } /** * Delete a stored attachment if it exists. */ private function deleteAttachment(DocumentationDocument $document): void { if (filled($document->attachment_path)) { Storage::disk($document->attachment_disk ?: config('documentation.storage_disk', 'public'))->delete($document->attachment_path); } } /** * Empty attachment metadata. * * @return array */ private function emptyAttachmentData(): array { return [ 'attachment_disk' => null, 'attachment_path' => null, 'attachment_name' => null, 'attachment_mime' => null, 'attachment_size' => null, ]; } }