From fcc6ade8f3555d67153e0affe030e7ebd457557c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sat, 8 Aug 2026 22:21:25 +0300 Subject: [PATCH] add element persistence --- backend/app/Element/CreateElementDto.php | 15 +++ backend/app/Element/ElementModel.php | 47 +++++++++ backend/app/Element/ElementRepository.php | 10 ++ .../app/Element/EloquentElementRepository.php | 98 +++++++++++++++++++ backend/app/Providers/AppServiceProvider.php | 6 ++ ...026_08_08_000000_create_elements_table.php | 35 +++++++ backend/tests/Fakes/FakeElementRepository.php | 78 +++++++++++++++ 7 files changed, 289 insertions(+) create mode 100644 backend/app/Element/CreateElementDto.php create mode 100644 backend/app/Element/ElementModel.php create mode 100644 backend/app/Element/ElementRepository.php create mode 100644 backend/app/Element/EloquentElementRepository.php create mode 100644 backend/database/migrations/2026_08_08_000000_create_elements_table.php create mode 100644 backend/tests/Fakes/FakeElementRepository.php diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php new file mode 100644 index 0000000..6f7df77 --- /dev/null +++ b/backend/app/Element/CreateElementDto.php @@ -0,0 +1,15 @@ +|ElementModel newModelQuery() + * @method static Builder|ElementModel newQuery() + * @method static Builder|ElementModel query() + * + * @mixin \Eloquent + */ +#[Fillable([ + 'set_id', + 'name', + 'kind', + 'parent_element_id', + 'position', +])] +class ElementModel extends Model +{ + protected $table = 'elements'; + + public $timestamps = false; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'set_id' => 'integer', + 'parent_element_id' => 'integer', + 'position' => 'integer', + ]; + } +} diff --git a/backend/app/Element/ElementRepository.php b/backend/app/Element/ElementRepository.php new file mode 100644 index 0000000..b9ad71f --- /dev/null +++ b/backend/app/Element/ElementRepository.php @@ -0,0 +1,10 @@ +nextPosition( + $dto->set, + $dto->parentElement, + ); + $model = ElementModel::create([ + 'set_id' => $dto->set->getId(), + 'name' => $dto->name, + 'kind' => $dto->kind, + 'parent_element_id' => $dto->parentElement?->getId(), + 'position' => $position, + ]); + + return new Element( + id: $model->id, + name: $model->name, + kind: $model->kind, + set: $dto->set, + parentElement: $dto->parentElement, + position: $model->position, + ); + } + + public function find(int $id): ?Element + { + $model = ElementModel::find($id); + + return $model === null ? null : $this->toDomain($model); + } + + private function nextPosition( + Set $set, + ?Element $parentElement, + ): int { + $query = ElementModel::query() + ->where('set_id', $set->getId()); + if ($parentElement === null) { + $query->whereNull('parent_element_id'); + } else { + $query->where('parent_element_id', $parentElement->getId()); + } + + $currentMaximum = $query->max('position'); + if ($currentMaximum === null) { + return 1; + } + + return (int) $currentMaximum + 1; + } + + private function toDomain(ElementModel $model): Element + { + $set = $this->findSet($model->set_id); + + $parentElement = null; + if ($model->parent_element_id !== null) { + $parentElement = $this->find($model->parent_element_id); + if ($parentElement === null) { + throw new RuntimeException('element parent not found'); + } + } + + return new Element( + id: $model->id, + name: $model->name, + kind: $model->kind, + set: $set, + parentElement: $parentElement, + position: $model->position, + ); + } + + private function findSet(int $id): Set + { + foreach ($this->setRepository->all() as $set) { + if ($set->getId() === $id) { + return $set; + } + } + + throw new RuntimeException('element set not found'); + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index c9490ba..8d883fe 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -16,6 +16,8 @@ use App\Email\Emailer; use App\Email\EmailFactory; use App\Email\LaravelEmailer; use App\Email\LaravelEmailFactory; +use App\Element\ElementRepository; +use App\Element\EloquentElementRepository; use App\Set\EloquentSetRepository; use App\Set\SetRepository; use App\User\EloquentUserRepository; @@ -51,6 +53,10 @@ class AppServiceProvider extends ServiceProvider SetRepository::class, EloquentSetRepository::class, ); + $this->app->bind( + ElementRepository::class, + EloquentElementRepository::class, + ); $this->app->bind(PasswordHasher::class, BcryptPasswordHasher::class); $this->app->bind(TokenGenerator::class, RandomTokenGenerator::class); $this->app->bind(Clock::class, SystemClock::class); diff --git a/backend/database/migrations/2026_08_08_000000_create_elements_table.php b/backend/database/migrations/2026_08_08_000000_create_elements_table.php new file mode 100644 index 0000000..196c2c5 --- /dev/null +++ b/backend/database/migrations/2026_08_08_000000_create_elements_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('set_id') + ->constrained('sets') + ->restrictOnDelete(); + $table->string('name'); + $table->string('kind'); + $table->foreignId('parent_element_id') + ->nullable() + ->constrained('elements') + ->restrictOnDelete(); + $table->unsignedInteger('position'); + $table->index([ + 'set_id', + 'parent_element_id', + 'position', + ]); + }); + } + + public function down(): void + { + Schema::dropIfExists('elements'); + } +}; diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php new file mode 100644 index 0000000..4746bf3 --- /dev/null +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -0,0 +1,78 @@ + + */ + private array $elements = []; + + public function create(CreateElementDto $dto): Element + { + $id = count($this->elements) + 1; + $element = new Element( + id: $id, + name: $dto->name, + kind: $dto->kind, + set: $dto->set, + parentElement: $dto->parentElement, + position: $this->nextPosition($dto), + ); + $this->elements[$id] = $element; + + return $this->copy($element); + } + + public function find(int $id): ?Element + { + $element = $this->elements[$id] ?? null; + + return $element === null ? null : $this->copy($element); + } + + private function nextPosition(CreateElementDto $dto): int + { + $requestedParentId = $dto->parentElement?->getId(); + $maximumPosition = 0; + + foreach ($this->elements as $element) { + if ($element->getSet()->getId() !== $dto->set->getId()) { + continue; + } + + $elementParentId = $element->getParentElement()?->getId(); + if ($elementParentId !== $requestedParentId) { + continue; + } + + $maximumPosition = max( + $maximumPosition, + $element->getPosition(), + ); + } + + return $maximumPosition + 1; + } + + private function copy(Element $element): Element + { + $parentElement = $element->getParentElement(); + + return new Element( + id: $element->getId(), + name: $element->getName(), + kind: $element->getKind(), + set: $element->getSet(), + parentElement: $parentElement === null + ? null + : $this->copy($parentElement), + position: $element->getPosition(), + ); + } +}