create([ 'parent_id' => $parent?->id, 'name' => $name, 'inherits_permissions' => $parent !== null, 'created_by' => $user->id, ]); if (! $parent) { $folder->permissionRules()->create([ 'subject_type' => DocumentationFolderPermission::SUBJECT_ALL, 'can_read' => true, 'can_write' => false, ]); } $this->accessService->forgetCachedRules(); return $folder; }); } public function renameFolder(DocumentationFolder $folder, string $name): void { $folder->update(['name' => $name]); } /** @param array $data */ public function updatePermissions(DocumentationFolder $folder, array $data): void { DB::transaction(function () use ($folder, $data): void { $inherit = $folder->parent_id !== null && (bool) ($data['inherit_permissions'] ?? false); $folder->permissionRules()->delete(); $folder->update(['inherits_permissions' => $inherit]); if ($inherit) { return; } $rules = []; $this->mergeRule( $rules, DocumentationFolderPermission::SUBJECT_ALL, null, (bool) ($data['all_read'] ?? false), (bool) ($data['all_write'] ?? false), ); foreach ((array) ($data['role_read_ids'] ?? []) as $roleId) { $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_ROLE, (int) $roleId, true, false); } foreach ((array) ($data['role_write_ids'] ?? []) as $roleId) { $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_ROLE, (int) $roleId, true, true); } foreach ((array) ($data['user_read_ids'] ?? []) as $userId) { $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_USER, (int) $userId, true, false); } foreach ((array) ($data['user_write_ids'] ?? []) as $userId) { $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_USER, (int) $userId, true, true); } foreach ($rules as $rule) { $folder->permissionRules()->create($rule); } }); $this->accessService->forgetCachedRules(); } public function createDocument( DocumentationFolder $folder, ?string $name, UploadedFile $uploadedFile, User $user, ): DocumentationDocument { $file = $this->fileService->saveUploadedPrivateFile("documentation/{$folder->id}", $uploadedFile, $user->id); try { return DB::transaction(function () use ($folder, $name, $uploadedFile, $user, $file): DocumentationDocument { $document = DocumentationDocument::query()->create([ 'folder_id' => $folder->id, 'name' => $name ?: $file->original_name, 'last_version_number' => 1, 'created_by' => $user->id, ]); $document->versions()->create([ 'file_id' => $file->id, 'version' => 1, 'file_size' => max(0, (int) $uploadedFile->getSize()), 'created_by' => $user->id, ]); return $document; }); } catch (Throwable $exception) { $this->deleteStoredFile($file); throw $exception; } } public function renameDocument(DocumentationDocument $document, string $name): void { $document->update(['name' => $name]); } public function addVersion( DocumentationDocument $document, UploadedFile $uploadedFile, User $user, ): DocumentationDocumentVersion { $file = $this->fileService->saveUploadedPrivateFile("documentation/{$document->folder_id}", $uploadedFile, $user->id); try { return DB::transaction(function () use ($document, $uploadedFile, $user, $file): DocumentationDocumentVersion { $lockedDocument = DocumentationDocument::query()->lockForUpdate()->findOrFail($document->id); $nextVersion = $lockedDocument->last_version_number + 1; $version = $lockedDocument->versions()->create([ 'file_id' => $file->id, 'version' => $nextVersion, 'file_size' => max(0, (int) $uploadedFile->getSize()), 'created_by' => $user->id, ]); $lockedDocument->update(['last_version_number' => $nextVersion]); return $version; }); } catch (Throwable $exception) { $this->deleteStoredFile($file); throw $exception; } } public function deleteVersion(DocumentationDocumentVersion $version): void { $version->loadMissing('file'); $file = $version->file; $documentId = $version->document_id; DB::transaction(function () use ($version, $documentId): void { $version->delete(); $document = DocumentationDocument::query()->lockForUpdate()->find($documentId); if ($document && ! $document->versions()->exists()) { $document->delete(); } }); if ($file) { $this->deleteStoredFile($file); } } public function deleteDocument(DocumentationDocument $document): void { $document->loadMissing('versions.file'); $files = $document->versions->pluck('file')->filter(); DB::transaction(function () use ($document): void { $document->versions()->delete(); $document->delete(); }); foreach ($files as $file) { $this->deleteStoredFile($file); } } public function deleteFolder(DocumentationFolder $folder): void { $folderIds = $this->descendantIds($folder); $documents = DocumentationDocument::query() ->whereIn('folder_id', $folderIds) ->with('versions.file') ->get(); $files = $documents->flatMap->versions->pluck('file')->filter(); DB::transaction(fn (): bool => $folder->delete()); foreach ($files as $file) { $this->deleteStoredFile($file); } $this->accessService->forgetCachedRules(); } /** * @param array> $rules */ private function mergeRule( array &$rules, string $subjectType, ?int $subjectId, bool $canRead, bool $canWrite, ): void { if (! $canRead && ! $canWrite) { return; } $key = "{$subjectType}:".($subjectId ?? 'all'); $rules[$key] ??= [ 'subject_type' => $subjectType, 'role_id' => $subjectType === DocumentationFolderPermission::SUBJECT_ROLE ? $subjectId : null, 'user_id' => $subjectType === DocumentationFolderPermission::SUBJECT_USER ? $subjectId : null, 'can_read' => false, 'can_write' => false, ]; $rules[$key]['can_read'] = (bool) ($rules[$key]['can_read'] || $canRead || $canWrite); $rules[$key]['can_write'] = (bool) ($rules[$key]['can_write'] || $canWrite); } /** @return list */ private function descendantIds(DocumentationFolder $folder): array { $ids = [$folder->id]; $pending = [$folder->id]; while ($pending !== []) { $children = DocumentationFolder::query()->whereIn('parent_id', $pending)->pluck('id')->all(); $pending = array_map('intval', $children); $ids = [...$ids, ...$pending]; } return $ids; } private function deleteStoredFile(File $file): void { $this->fileService->deletePrivateFile($file); $file->delete(); } }