From 73b0befc97dad304ed98bc068ff40a3bbfa8d05c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Thu, 28 May 2026 20:08:36 +0300 Subject: [PATCH] add element updates --- backend/app/Controllers/ElementController.php | 86 ++++++++++++++++--- backend/app/Element/ElementRepository.php | 2 + .../app/Element/EloquentElementRepository.php | 30 +++++++ backend/app/Element/UpdateElementDto.php | 16 ++++ .../UseCases/UpdateElement/UpdateElement.php | 57 ++++++++++++ .../UpdateElement/UpdateElementRequest.php | 17 ++++ backend/routes/api.php | 2 + backend/tests/Fakes/FakeElementRepository.php | 19 ++++ .../tests/Feature/ElementsEndpointTest.php | 3 +- .../Controllers/ElementControllerTest.php | 4 +- 10 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 backend/app/Element/UpdateElementDto.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateElement.php create mode 100644 backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 1c346c6..6a63a85 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -2,16 +2,22 @@ namespace App\Controllers; +use App\Element\Element; use App\Element\UseCases\GetElement\GetElement; use App\Element\UseCases\GetElement\GetElementRequest; +use App\Element\UseCases\UpdateElement\UpdateElement; +use App\Element\UseCases\UpdateElement\UpdateElementRequest; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; class ElementController { - public function __construct(private GetElement $getElement) - { + public function __construct( + private GetElement $getElement, + private UpdateElement $updateElement, + ) { } public function show(?int $id): JsonResponse @@ -42,15 +48,73 @@ class ElementController return new JsonResponse([ 'childElements' => $childElements, - 'element' => [ - 'id' => $element->getId(), - 'title' => $element->getTitle(), - 'description' => $element->getDescription(), - 'iconImageUrl' => $element->getIconImageUrl(), - 'richText' => $element->getRichText(), - 'pdfPath' => $element->getPdfPath(), - 'youtubeUrl' => $element->getYoutubeUrl(), - ], + 'element' => $this->buildElementPayload($element), ], 200); } + + public function update(?int $id, Request $request): JsonResponse + { + try { + $element = $this->updateElement->execute( + new UpdateElementRequest( + id: $id, + title: $this->stringInput($request, 'title'), + description: $this->stringInput($request, 'description'), + iconImageUrl: $this->stringInput( + $request, + 'iconImageUrl', + ), + richText: $this->stringInput($request, 'richText'), + pdfPath: $this->stringInput($request, 'pdfPath'), + youtubeUrl: $this->stringInput($request, 'youtubeUrl'), + ) + ); + } catch (BadRequestException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 400); + } catch (NotFoundException $exception) { + return new JsonResponse([ + 'error' => $exception->getMessage(), + ], 404); + } + + return new JsonResponse([ + 'element' => $this->buildElementPayload($element), + ], 200); + } + + private function stringInput(Request $request, string $key): ?string + { + $value = $request->input($key); + if (! is_string($value)) { + return null; + } + + return $value; + } + + /** + * @return array{ + * id: int, + * title: string, + * description: string, + * iconImageUrl: string|null, + * richText: string, + * pdfPath: string|null, + * youtubeUrl: string|null, + * } + */ + private function buildElementPayload(Element $element): array + { + return [ + 'id' => $element->getId(), + 'title' => $element->getTitle(), + 'description' => $element->getDescription(), + 'iconImageUrl' => $element->getIconImageUrl(), + 'richText' => $element->getRichText(), + 'pdfPath' => $element->getPdfPath(), + 'youtubeUrl' => $element->getYoutubeUrl(), + ]; + } } diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php index 3e89628..b733611 100644 --- a/backend/app/Element/ElementRepository.php +++ b/backend/app/Element/ElementRepository.php @@ -8,6 +8,8 @@ interface ElementRepository { public function create(CreateElementDto $dto): Element; + public function update(Element $element, UpdateElementDto $dto): Element; + public function find(int $id): ?Element; public function findRootBySet(DomainSet $set): ?Element; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index b8c06d7..65dc061 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -38,6 +38,36 @@ class EloquentElementRepository implements ElementRepository ); } + public function update(Element $element, UpdateElementDto $dto): Element + { + $model = ElementModel::find($element->getId()); + if ($model === null) { + throw new DomainException( + "Element with id: {$element->getId()} doesnt exist" + ); + } + + $model->title = $dto->title; + $model->description = $dto->description; + $model->icon_image_url = $dto->iconImageUrl; + $model->rich_text = $dto->richText; + $model->pdf_path = $dto->pdfPath; + $model->youtube_url = $dto->youtubeUrl; + $model->save(); + + return new Element( + id: $element->getId(), + title: $dto->title, + description: $dto->description, + iconImageUrl: $dto->iconImageUrl, + richText: $dto->richText, + pdfPath: $dto->pdfPath, + youtubeUrl: $dto->youtubeUrl, + set: $element->getSet(), + parentElement: $element->getParentElement(), + ); + } + public function find(int $id): ?Element { $model = ElementModel::find($id); diff --git a/backend/app/Element/UpdateElementDto.php b/backend/app/Element/UpdateElementDto.php new file mode 100644 index 0000000..5c4b0b7 --- /dev/null +++ b/backend/app/Element/UpdateElementDto.php @@ -0,0 +1,16 @@ +id === null) { + throw new BadRequestException('id is required'); + } + + if ($request->title === null || trim($request->title) === '') { + throw new BadRequestException('title is required'); + } + + $element = $this->elementRepository->find($request->id); + if ($element === null) { + throw new NotFoundException('Element not found'); + } + + return $this->elementRepository->update( + $element, + new UpdateElementDto( + title: $request->title, + description: $request->description ?? '', + iconImageUrl: $this->nullableString($request->iconImageUrl), + richText: $request->richText ?? '', + pdfPath: $this->nullableString($request->pdfPath), + youtubeUrl: $this->nullableString($request->youtubeUrl), + ), + ); + } + + private function nullableString(?string $value): ?string + { + if ($value === null || $value === '') { + return null; + } + + return $value; + } +} diff --git a/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php new file mode 100644 index 0000000..0b5e766 --- /dev/null +++ b/backend/app/Element/UseCases/UpdateElement/UpdateElementRequest.php @@ -0,0 +1,17 @@ +middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); Route::get('/elements/{id}', [ElementController::class, 'show']); +Route::patch('/elements/{id}', [ElementController::class, 'update']) + ->middleware(AuthMiddleware::class); diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index 132a526..037fae7 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -5,6 +5,7 @@ namespace Tests\Fakes; use App\Element\CreateElementDto; use App\Element\Element; use App\Element\ElementRepository; +use App\Element\UpdateElementDto; use App\Set\Set as DomainSet; class FakeElementRepository implements ElementRepository @@ -33,6 +34,24 @@ class FakeElementRepository implements ElementRepository return $element; } + public function update(Element $element, UpdateElementDto $dto): Element + { + $updatedElement = new Element( + id: $element->getId(), + title: $dto->title, + description: $dto->description, + iconImageUrl: $dto->iconImageUrl, + richText: $dto->richText, + pdfPath: $dto->pdfPath, + youtubeUrl: $dto->youtubeUrl, + set: $element->getSet(), + parentElement: $element->getParentElement(), + ); + $this->elementsById[$element->getId()] = $updatedElement; + + return $this->cloneElement($updatedElement); + } + public function find(int $id): ?Element { if (! isset($this->elementsById[$id])) { diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 251fddf..2ff9089 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -158,7 +158,8 @@ class ElementsEndpointTest extends TestCase )); $this->createSession('valid-token'); - $response = $this->withCookie('auth_token', 'valid-token') + $response = $this->withCredentials() + ->withUnencryptedCookie('auth_token', 'valid-token') ->patchJson("/api/elements/{$element->getId()}", [ 'title' => 'Updated title', 'description' => 'Updated description', diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index d06c0f7..64fcfb5 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -6,6 +6,7 @@ use App\Controllers\ElementController; use App\Element\CreateElementDto; use App\Element\Element; use App\Element\UseCases\GetElement\GetElement; +use App\Element\UseCases\UpdateElement\UpdateElement; use App\Set\Set as DomainSet; use Tests\Fakes\FakeElementRepository; use Tests\TestCase; @@ -20,7 +21,8 @@ class ElementControllerTest extends TestCase { $this->elementRepo = new FakeElementRepository(); $getElement = new GetElement($this->elementRepo); - $this->controller = new ElementController($getElement); + $updateElement = new UpdateElement($this->elementRepo); + $this->controller = new ElementController($getElement, $updateElement); } public function testShowReturnsElementPayload(): void