diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php new file mode 100644 index 0000000..db77dd7 --- /dev/null +++ b/backend/app/Element/CreateElementDto.php @@ -0,0 +1,14 @@ +id; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getSet(): Set + { + return $this->set; + } + + public function getParentElement(): ?Element + { + return $this->parentElement; + } +} diff --git a/backend/app/Element/ElementModel.php b/backend/app/Element/ElementModel.php new file mode 100644 index 0000000..119d287 --- /dev/null +++ b/backend/app/Element/ElementModel.php @@ -0,0 +1,40 @@ +|ElementModel newModelQuery() + * @method static Builder|ElementModel newQuery() + * @method static Builder|ElementModel query() + * @method static Builder|ElementModel whereId($value) + * @method static Builder|ElementModel whereParentElementId($value) + * @method static Builder|ElementModel whereSetId($value) + * @method static Builder|ElementModel whereTitle($value) + * + * @mixin \Eloquent + */ +class ElementModel extends Model +{ + protected $table = 'elements'; + + public $timestamps = false; + + protected $fillable = [ + 'set_id', + 'title', + 'parent_element_id', + ]; + + protected $casts = [ + 'set_id' => 'integer', + 'parent_element_id' => 'integer', + ]; +} diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php new file mode 100644 index 0000000..7c91492 --- /dev/null +++ b/backend/app/Element/ElementRepository.php @@ -0,0 +1,15 @@ + $dto->set->getId(), + 'title' => $dto->title, + 'parent_element_id' => $dto->parentElement?->getId(), + ]); + + return new Element( + id: $model->id, + title: $dto->title, + set: $dto->set, + parentElement: $dto->parentElement, + ); + } + + public function find(int $id): ?Element + { + $model = ElementModel::find($id); + + return $model === null ? null : $this->toDomain($model); + } + + /** + * @return Element[] + */ + public function findBySetId(int $id): array + { + $models = ElementModel::where('set_id', $id)->orderBy('id')->get(); + $elements = []; + foreach ($models as $model) { + $elements[] = $this->toDomain($model); + } + + return $elements; + } + + private function toDomain(ElementModel $model): Element + { + $set = $this->setRepo->find($model->set_id); + if ($set === null) { + throw new DomainException( + "Set with id: {$model->set_id} doesnt exist" + ); + } + + $parentElement = null; + if ($model->parent_element_id !== null) { + $parentElement = $this->find($model->parent_element_id); + if ($parentElement === null) { + throw new DomainException( + "Element with id: {$model->parent_element_id} doesnt exist" + ); + } + } + + return new Element( + id: $model->id, + title: $model->title, + set: $set, + parentElement: $parentElement, + ); + } +} diff --git a/backend/app/Element/UseCases/CreateElement/CreateElement.php b/backend/app/Element/UseCases/CreateElement/CreateElement.php new file mode 100644 index 0000000..881bcea --- /dev/null +++ b/backend/app/Element/UseCases/CreateElement/CreateElement.php @@ -0,0 +1,84 @@ +setId === null) { + throw new BadRequestException('setId is required'); + } + if ($request->title === null || $request->title === '') { + throw new BadRequestException('title is required'); + } + + $set = $this->setRepo->find($request->setId); + if ($set === null) { + throw new DomainException( + "Set with id: {$request->setId} doesnt exist" + ); + } + + if ($request->parentElementId === null) { + $this->validateNoRootElementExists($request->setId); + + return $this->elementRepo->create(new CreateElementDto( + set: $set, + title: $request->title, + parentElement: null, + )); + } + + $parentElement = $this->elementRepo->find( + $request->parentElementId + ); + if ($parentElement === null) { + throw new DomainException( + "Element with id: {$request->parentElementId} doesnt exist" + ); + } + if ($parentElement->getSet()->getId() !== $set->getId()) { + throw new DomainException( + 'Parent element must belong to the same set' + ); + } + + return $this->elementRepo->create(new CreateElementDto( + set: $set, + title: $request->title, + parentElement: $parentElement, + )); + } + + /** + * @throws DomainException + */ + private function validateNoRootElementExists(int $setId): void + { + $elements = $this->elementRepo->findBySetId($setId); + foreach ($elements as $element) { + if ($element->getParentElement() === null) { + throw new DomainException( + 'A root element already exists for this set' + ); + } + } + } +} diff --git a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php new file mode 100644 index 0000000..b9f0dbd --- /dev/null +++ b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php @@ -0,0 +1,12 @@ +app->bind( + SetRepository::class, + EloquentSetRepository::class + ); + $this->app->bind( + ElementRepository::class, + EloquentElementRepository::class + ); } } diff --git a/backend/app/Set/CreateSetDto.php b/backend/app/Set/CreateSetDto.php new file mode 100644 index 0000000..2999a88 --- /dev/null +++ b/backend/app/Set/CreateSetDto.php @@ -0,0 +1,10 @@ + $dto->name, + ]); + + return $this->toDomain($model); + } + + public function find(int $id): ?Set + { + $model = SetModel::find($id); + + return $model === null ? null : $this->toDomain($model); + } + + public function getAll(): array + { + $models = SetModel::orderBy('id')->get(); + $sets = []; + foreach ($models as $model) { + $sets[] = $this->toDomain($model); + } + + return $sets; + } + + private function toDomain(SetModel $model): Set + { + return new Set( + id: $model->id, + name: $model->name, + ); + } +} diff --git a/backend/app/Set/Set.php b/backend/app/Set/Set.php new file mode 100644 index 0000000..d045e70 --- /dev/null +++ b/backend/app/Set/Set.php @@ -0,0 +1,21 @@ +id; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/backend/app/Set/SetModel.php b/backend/app/Set/SetModel.php new file mode 100644 index 0000000..c30e2c9 --- /dev/null +++ b/backend/app/Set/SetModel.php @@ -0,0 +1,27 @@ +|SetModel newModelQuery() + * @method static Builder|SetModel newQuery() + * @method static Builder|SetModel query() + * @method static Builder|SetModel whereId($value) + * @method static Builder|SetModel whereName($value) + * + * @mixin \Eloquent + */ +class SetModel extends Model +{ + protected $table = 'sets'; + + public $timestamps = false; + + protected $fillable = ['name']; +} diff --git a/backend/app/Set/SetRepository.php b/backend/app/Set/SetRepository.php new file mode 100644 index 0000000..7f1efcd --- /dev/null +++ b/backend/app/Set/SetRepository.php @@ -0,0 +1,15 @@ +name === null || $request->name === '') { + throw new BadRequestException('name is required'); + } + + $set = $this->setRepo->create(new CreateSetDto( + name: $request->name, + )); + + $this->elementRepo->create(new CreateElementDto( + set: $set, + title: $set->getName(), + parentElement: null, + )); + + return $set; + } +} diff --git a/backend/app/Set/UseCases/CreateSet/CreateSetRequest.php b/backend/app/Set/UseCases/CreateSet/CreateSetRequest.php new file mode 100644 index 0000000..066d8ec --- /dev/null +++ b/backend/app/Set/UseCases/CreateSet/CreateSetRequest.php @@ -0,0 +1,10 @@ +id(); + $table->string('name'); + }); + } + + public function down(): void + { + Schema::dropIfExists('sets'); + } +}; diff --git a/backend/database/migrations/2026_05_24_000001_elements_table.php b/backend/database/migrations/2026_05_24_000001_elements_table.php new file mode 100644 index 0000000..377ba38 --- /dev/null +++ b/backend/database/migrations/2026_05_24_000001_elements_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('set_id')->constrained('sets'); + $table->string('title'); + $table->foreignId('parent_element_id') + ->nullable() + ->constrained('elements'); + }); + } + + public function down(): void + { + Schema::dropIfExists('elements'); + } +};