DocumentationService.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Services;
  4. use App\Models\DocumentationDocument;
  5. use App\Models\DocumentationDocumentVersion;
  6. use App\Models\DocumentationFolder;
  7. use App\Models\DocumentationFolderPermission;
  8. use App\Models\File;
  9. use App\Models\User;
  10. use Illuminate\Http\UploadedFile;
  11. use Illuminate\Support\Facades\DB;
  12. use Throwable;
  13. class DocumentationService
  14. {
  15. public function __construct(
  16. private readonly FileService $fileService,
  17. private readonly DocumentationAccessService $accessService,
  18. ) {}
  19. public function createFolder(?DocumentationFolder $parent, string $name, User $user): DocumentationFolder
  20. {
  21. return DB::transaction(function () use ($parent, $name, $user): DocumentationFolder {
  22. $folder = DocumentationFolder::query()->create([
  23. 'parent_id' => $parent?->id,
  24. 'name' => $name,
  25. 'inherits_permissions' => $parent !== null,
  26. 'created_by' => $user->id,
  27. ]);
  28. if (! $parent) {
  29. $folder->permissionRules()->create([
  30. 'subject_type' => DocumentationFolderPermission::SUBJECT_ALL,
  31. 'can_read' => true,
  32. 'can_write' => false,
  33. ]);
  34. }
  35. $this->accessService->forgetCachedRules();
  36. return $folder;
  37. });
  38. }
  39. public function renameFolder(DocumentationFolder $folder, string $name): void
  40. {
  41. $folder->update(['name' => $name]);
  42. }
  43. /** @param array<string, mixed> $data */
  44. public function updatePermissions(DocumentationFolder $folder, array $data): void
  45. {
  46. DB::transaction(function () use ($folder, $data): void {
  47. $inherit = $folder->parent_id !== null && (bool) ($data['inherit_permissions'] ?? false);
  48. $folder->permissionRules()->delete();
  49. $folder->update(['inherits_permissions' => $inherit]);
  50. if ($inherit) {
  51. return;
  52. }
  53. $rules = [];
  54. $this->mergeRule(
  55. $rules,
  56. DocumentationFolderPermission::SUBJECT_ALL,
  57. null,
  58. (bool) ($data['all_read'] ?? false),
  59. (bool) ($data['all_write'] ?? false),
  60. );
  61. foreach ((array) ($data['role_read_ids'] ?? []) as $roleId) {
  62. $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_ROLE, (int) $roleId, true, false);
  63. }
  64. foreach ((array) ($data['role_write_ids'] ?? []) as $roleId) {
  65. $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_ROLE, (int) $roleId, true, true);
  66. }
  67. foreach ((array) ($data['user_read_ids'] ?? []) as $userId) {
  68. $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_USER, (int) $userId, true, false);
  69. }
  70. foreach ((array) ($data['user_write_ids'] ?? []) as $userId) {
  71. $this->mergeRule($rules, DocumentationFolderPermission::SUBJECT_USER, (int) $userId, true, true);
  72. }
  73. foreach ($rules as $rule) {
  74. $folder->permissionRules()->create($rule);
  75. }
  76. });
  77. $this->accessService->forgetCachedRules();
  78. }
  79. public function createDocument(
  80. DocumentationFolder $folder,
  81. ?string $name,
  82. UploadedFile $uploadedFile,
  83. User $user,
  84. ): DocumentationDocument {
  85. $file = $this->fileService->saveUploadedPrivateFile("documentation/{$folder->id}", $uploadedFile, $user->id);
  86. try {
  87. return DB::transaction(function () use ($folder, $name, $uploadedFile, $user, $file): DocumentationDocument {
  88. $document = DocumentationDocument::query()->create([
  89. 'folder_id' => $folder->id,
  90. 'name' => $name ?: $file->original_name,
  91. 'last_version_number' => 1,
  92. 'created_by' => $user->id,
  93. ]);
  94. $document->versions()->create([
  95. 'file_id' => $file->id,
  96. 'version' => 1,
  97. 'file_size' => max(0, (int) $uploadedFile->getSize()),
  98. 'created_by' => $user->id,
  99. ]);
  100. return $document;
  101. });
  102. } catch (Throwable $exception) {
  103. $this->deleteStoredFile($file);
  104. throw $exception;
  105. }
  106. }
  107. public function renameDocument(DocumentationDocument $document, string $name): void
  108. {
  109. $document->update(['name' => $name]);
  110. }
  111. public function addVersion(
  112. DocumentationDocument $document,
  113. UploadedFile $uploadedFile,
  114. User $user,
  115. ): DocumentationDocumentVersion {
  116. $file = $this->fileService->saveUploadedPrivateFile("documentation/{$document->folder_id}", $uploadedFile, $user->id);
  117. try {
  118. return DB::transaction(function () use ($document, $uploadedFile, $user, $file): DocumentationDocumentVersion {
  119. $lockedDocument = DocumentationDocument::query()->lockForUpdate()->findOrFail($document->id);
  120. $nextVersion = $lockedDocument->last_version_number + 1;
  121. $version = $lockedDocument->versions()->create([
  122. 'file_id' => $file->id,
  123. 'version' => $nextVersion,
  124. 'file_size' => max(0, (int) $uploadedFile->getSize()),
  125. 'created_by' => $user->id,
  126. ]);
  127. $lockedDocument->update(['last_version_number' => $nextVersion]);
  128. return $version;
  129. });
  130. } catch (Throwable $exception) {
  131. $this->deleteStoredFile($file);
  132. throw $exception;
  133. }
  134. }
  135. public function deleteVersion(DocumentationDocumentVersion $version): void
  136. {
  137. $version->loadMissing('file');
  138. $file = $version->file;
  139. $documentId = $version->document_id;
  140. DB::transaction(function () use ($version, $documentId): void {
  141. $version->delete();
  142. $document = DocumentationDocument::query()->lockForUpdate()->find($documentId);
  143. if ($document && ! $document->versions()->exists()) {
  144. $document->delete();
  145. }
  146. });
  147. if ($file) {
  148. $this->deleteStoredFile($file);
  149. }
  150. }
  151. public function deleteDocument(DocumentationDocument $document): void
  152. {
  153. $document->loadMissing('versions.file');
  154. $files = $document->versions->pluck('file')->filter();
  155. DB::transaction(function () use ($document): void {
  156. $document->versions()->delete();
  157. $document->delete();
  158. });
  159. foreach ($files as $file) {
  160. $this->deleteStoredFile($file);
  161. }
  162. }
  163. public function deleteFolder(DocumentationFolder $folder): void
  164. {
  165. $folderIds = $this->descendantIds($folder);
  166. $documents = DocumentationDocument::query()
  167. ->whereIn('folder_id', $folderIds)
  168. ->with('versions.file')
  169. ->get();
  170. $files = $documents->flatMap->versions->pluck('file')->filter();
  171. DB::transaction(fn (): bool => $folder->delete());
  172. foreach ($files as $file) {
  173. $this->deleteStoredFile($file);
  174. }
  175. $this->accessService->forgetCachedRules();
  176. }
  177. /**
  178. * @param array<string, array<string, mixed>> $rules
  179. */
  180. private function mergeRule(
  181. array &$rules,
  182. string $subjectType,
  183. ?int $subjectId,
  184. bool $canRead,
  185. bool $canWrite,
  186. ): void {
  187. if (! $canRead && ! $canWrite) {
  188. return;
  189. }
  190. $key = "{$subjectType}:".($subjectId ?? 'all');
  191. $rules[$key] ??= [
  192. 'subject_type' => $subjectType,
  193. 'role_id' => $subjectType === DocumentationFolderPermission::SUBJECT_ROLE ? $subjectId : null,
  194. 'user_id' => $subjectType === DocumentationFolderPermission::SUBJECT_USER ? $subjectId : null,
  195. 'can_read' => false,
  196. 'can_write' => false,
  197. ];
  198. $rules[$key]['can_read'] = (bool) ($rules[$key]['can_read'] || $canRead || $canWrite);
  199. $rules[$key]['can_write'] = (bool) ($rules[$key]['can_write'] || $canWrite);
  200. }
  201. /** @return list<int> */
  202. private function descendantIds(DocumentationFolder $folder): array
  203. {
  204. $ids = [$folder->id];
  205. $pending = [$folder->id];
  206. while ($pending !== []) {
  207. $children = DocumentationFolder::query()->whereIn('parent_id', $pending)->pluck('id')->all();
  208. $pending = array_map('intval', $children);
  209. $ids = [...$ids, ...$pending];
  210. }
  211. return $ids;
  212. }
  213. private function deleteStoredFile(File $file): void
  214. {
  215. $this->fileService->deletePrivateFile($file);
  216. $file->delete();
  217. }
  218. }