diff --git a/backend/app/Controllers/SetController.php b/backend/app/Controllers/SetController.php index 6fcbb8c..37b846f 100644 --- a/backend/app/Controllers/SetController.php +++ b/backend/app/Controllers/SetController.php @@ -3,15 +3,23 @@ namespace App\Controllers; use App\Element\ElementRepository; +use App\Exceptions\BadRequestException; use App\Set\Set as DomainSet; use App\Set\SetRepository; +use App\Set\UseCases\CreateSetWithRoot\CreateSetWithRoot; +use App\Set\UseCases\CreateSetWithRoot\CreateSetWithRootRequest; +use App\Shared\Files\Filesystem; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Http\UploadedFile; class SetController { public function __construct( private SetRepository $setRepository, private ElementRepository $elementRepository, + private CreateSetWithRoot $createSetWithRoot, + private Filesystem $filesystem, ) { } @@ -27,6 +35,32 @@ class SetController ], 200); } + public function create(Request $request): JsonResponse + { + $iconImage = $this->uploadedFileInput($request, 'iconImage'); + + try { + $set = $this->createSetWithRoot->execute( + new CreateSetWithRootRequest( + name: $this->stringInput($request, 'name'), + description: $this->stringInput($request, 'description'), + iconImageContents: $iconImage['contents'], + iconImageOriginalName: $iconImage['originalName'], + iconImageMimeType: $iconImage['mimeType'], + iconImageSizeBytes: $iconImage['sizeBytes'], + ) + ); + } catch (BadRequestException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 400); + } + + return new JsonResponse([ + 'set' => $this->buildSetPayload($set), + ], 201); + } + /** * @return array{ * id: int, @@ -44,8 +78,77 @@ class SetController 'id' => $set->getId(), 'name' => $set->getName(), 'description' => $set->getDescription(), - 'iconImageUrl' => $set->getIconImageUrl(), + 'iconImageUrl' => $this->storageUrl( + $set->getIconImageUrl(), + 'set-icons/', + ), 'rootElementId' => $rootElement?->getId(), ]; } + + private function stringInput(Request $request, string $key): ?string + { + if (! $request->exists($key)) { + return null; + } + + $value = $request->input($key); + if ($value === null) { + return ''; + } + + if (! is_string($value)) { + return null; + } + + return $value; + } + + /** + * @return array{ + * contents: string|null, + * originalName: string|null, + * mimeType: string|null, + * sizeBytes: int|null + * } + */ + private function uploadedFileInput(Request $request, string $key): array + { + $emptyFileInput = [ + 'contents' => null, + 'originalName' => null, + 'mimeType' => null, + 'sizeBytes' => null, + ]; + + if (! $request->hasFile($key)) { + return $emptyFileInput; + } + + $file = $request->file($key); + if (! $file instanceof UploadedFile) { + return $emptyFileInput; + } + + $realPath = $file->getRealPath(); + if ($realPath === false) { + return $emptyFileInput; + } + + return [ + 'contents' => (string) file_get_contents($realPath), + 'originalName' => $file->getClientOriginalName(), + 'mimeType' => (string) $file->getMimeType(), + 'sizeBytes' => (int) $file->getSize(), + ]; + } + + private function storageUrl(string $path, string $folderPrefix): string + { + if (str_starts_with($path, $folderPrefix)) { + return $this->filesystem->url($path); + } + + return $path; + } } diff --git a/backend/app/Set/UseCases/CreateSetWithRoot/CreateSetWithRoot.php b/backend/app/Set/UseCases/CreateSetWithRoot/CreateSetWithRoot.php new file mode 100644 index 0000000..9d9c98d --- /dev/null +++ b/backend/app/Set/UseCases/CreateSetWithRoot/CreateSetWithRoot.php @@ -0,0 +1,106 @@ +name === null || $request->name === '') { + throw new BadRequestException('name is required'); + } + if ($request->description === null || $request->description === '') { + throw new BadRequestException('description is required'); + } + + $iconPath = $this->uploadIconImage($request); + $set = $this->setRepository->create(new CreateSetDto( + name: $request->name, + description: $request->description, + iconImageUrl: $iconPath, + )); + + $this->elementRepository->create(new CreateElementDto( + set: $set, + title: $set->getName(), + description: $set->getDescription(), + iconImageUrl: $iconPath, + richText: '', + shortPdfPath: null, + longPdfPath: null, + youtubeUrl: null, + parentElement: null, + )); + + return $set; + } + + /** + * @throws BadRequestException + */ + private function uploadIconImage(CreateSetWithRootRequest $request): string + { + if ( + $request->iconImageContents === null + || $request->iconImageOriginalName === null + || $request->iconImageMimeType === null + || $request->iconImageSizeBytes === null + ) { + throw new BadRequestException('icon image is required'); + } + + if ( + ! in_array( + $request->iconImageMimeType, + self::ALLOWED_MIME_TYPES, + true, + ) + ) { + throw new BadRequestException( + 'icon image must be a jpeg, png or webp image', + ); + } + + if ($request->iconImageSizeBytes > self::MAX_SIZE_BYTES) { + throw new BadRequestException( + 'icon image must be 5MB or smaller', + ); + } + + $iconImage = new FileToUpload( + contents: $request->iconImageContents, + originalName: $request->iconImageOriginalName, + mimeType: $request->iconImageMimeType, + sizeBytes: $request->iconImageSizeBytes, + ); + + return $this->filesystem->upload($iconImage, 'set-icons'); + } +} diff --git a/backend/app/Set/UseCases/CreateSetWithRoot/CreateSetWithRootRequest.php b/backend/app/Set/UseCases/CreateSetWithRoot/CreateSetWithRootRequest.php new file mode 100644 index 0000000..166a901 --- /dev/null +++ b/backend/app/Set/UseCases/CreateSetWithRoot/CreateSetWithRootRequest.php @@ -0,0 +1,16 @@ +middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); +Route::post('/sets', [SetController::class, 'create']) + ->middleware(AuthMiddleware::class); Route::get('/elements/{id}', [ElementController::class, 'show']); Route::get('/element-pdfs/{folder}/{fileName}', [ ElementController::class,