Files
Helpddesk/Modules/Documentation/Http/Controllers/DocumentationController.php
T

397 lines
13 KiB
PHP
Executable File

<?php
namespace Modules\Documentation\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Storage;
use Modules\Documentation\Http\Requests\StoreDocumentationDocumentRequest;
use Modules\Documentation\Http\Requests\UpdateDocumentationDocumentRequest;
use Modules\Documentation\Models\DocumentationAttachment;
use Modules\Documentation\Models\DocumentationCategory;
use Modules\Documentation\Models\DocumentationDocument;
class DocumentationController extends Controller
{
/**
* Display the documentation dashboard and document list.
*/
public function index(Request $request): View
{
$query = DocumentationDocument::query()->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(),
'archived' => DocumentationDocument::onlyTrashed()->count(),
],
'statuses' => config('documentation.statuses', []),
]);
}
/**
* Display soft-deleted documents.
*/
public function archived(Request $request): View
{
$query = DocumentationDocument::onlyTrashed()
->with(['category', 'attachments'])
->latest('deleted_at');
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.archived', [
'documents' => $query->paginate(15)->withQueryString(),
'categories' => DocumentationCategory::query()->ordered()->get(),
]);
}
/**
* 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.');
}
/**
* Archive a document using Laravel soft deletes.
*
* Stored files and attachment records are deliberately retained. They are
* only removed when an archived document is permanently deleted.
*/
public function destroy(Request $request, DocumentationDocument $document): RedirectResponse
{
$document->forceFill([
'status' => DocumentationDocument::STATUS_ARCHIVED,
'updated_by' => $request->user()?->id,
])->save();
$document->delete();
return redirect()
->route('documentation.documents.index')
->with('status', 'Document archived successfully.');
}
/**
* Restore a previously archived document.
*/
public function restore(Request $request, int $document): RedirectResponse
{
$document = DocumentationDocument::onlyTrashed()->findOrFail($document);
$document->restore();
// Restored documents return to Draft so they cannot unexpectedly
// become publicly/operationally published again.
$document->forceFill([
'status' => DocumentationDocument::STATUS_DRAFT,
'updated_by' => $request->user()?->id,
])->save();
return redirect()
->route('documentation.documents.archived')
->with('status', 'Document restored as a draft.');
}
/**
* Permanently delete an archived document and all stored files.
*/
public function forceDestroy(int $document): RedirectResponse
{
$document = DocumentationDocument::onlyTrashed()
->with('attachments')
->findOrFail($document);
$this->deleteAttachment($document);
foreach ($document->attachments as $attachment) {
Storage::disk($attachment->disk)->delete($attachment->path);
$attachment->delete();
}
$document->forceDelete();
return redirect()
->route('documentation.documents.archived')
->with('status', 'Document permanently deleted.');
}
/**
* 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<string, mixed>
*/
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<string, null>
*/
private function emptyAttachmentData(): array
{
return [
'attachment_disk' => null,
'attachment_path' => null,
'attachment_name' => null,
'attachment_mime' => null,
'attachment_size' => null,
];
}
}