add set layout api

This commit is contained in:
Yisroel Baum 2026-08-08 23:32:45 +03:00
parent eb7ac7d261
commit 4222d4c0a2
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
16 changed files with 487 additions and 6 deletions

View file

@ -14,6 +14,11 @@ interface ElementRepository
public function find(int $id): ?Element;
/**
* @return list<Element>
*/
public function findBySet(Set $set): array;
/**
* @return list<Element>
*/

View file

@ -45,6 +45,37 @@ class EloquentElementRepository implements ElementRepository
return $model === null ? null : $this->toDomain($model);
}
public function findBySet(Set $set): array
{
$models = ElementModel::query()
->where('set_id', $set->getId())
->orderBy('id')
->get();
$elements = [];
$elementsById = [];
foreach ($models as $model) {
$parentElement = null;
if ($model->parent_element_id !== null) {
$parentElement = $elementsById[$model->parent_element_id]
?? null;
if ($parentElement === null) {
throw new RuntimeException('element parent not found');
}
}
$element = $this->toDomainWithRelations(
model: $model,
set: $set,
parentElement: $parentElement,
);
$elements[] = $element;
$elementsById[$element->getId()] = $element;
}
return $elements;
}
public function findTopLevelBySet(Set $set): array
{
$models = ElementModel::query()
@ -160,10 +191,9 @@ class EloquentElementRepository implements ElementRepository
private function findSet(int $id): Set
{
foreach ($this->setRepository->all() as $set) {
if ($set->getId() === $id) {
return $set;
}
$set = $this->setRepository->find($id);
if ($set !== null) {
return $set;
}
throw new RuntimeException('element set not found');

View file

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions;
use DomainException;
class NotFoundException extends DomainException {}

View file

@ -2,7 +2,10 @@
namespace App\Http\Controllers;
use App\Exceptions\NotFoundException;
use App\Set\Set;
use App\Set\UseCases\GetSetLayout\ElementLayoutNode;
use App\Set\UseCases\GetSetLayout\GetSetLayout;
use App\Set\UseCases\ListSets\ListSets;
use Illuminate\Http\JsonResponse;
@ -10,6 +13,7 @@ class SetController extends Controller
{
public function __construct(
private ListSets $listSets,
private GetSetLayout $getSetLayout,
) {}
public function index(): JsonResponse
@ -23,4 +27,52 @@ class SetController extends Controller
return new JsonResponse(['sets' => $sets]);
}
public function show(int $setId): JsonResponse
{
try {
$layout = $this->getSetLayout->execute($setId);
} catch (NotFoundException $exception) {
return new JsonResponse(
['error' => $exception->getMessage()],
404,
);
}
$set = $layout->getSet();
return new JsonResponse([
'set' => [
'id' => $set->getId(),
'name' => $set->getName(),
],
'elements' => array_map(
$this->elementPayload(...),
$layout->getElements(),
),
]);
}
/**
* @return array{
* id: int,
* name: string,
* kind: string,
* children: list<mixed>
* }
*/
private function elementPayload(ElementLayoutNode $node): array
{
$element = $node->getElement();
return [
'id' => $element->getId(),
'name' => $element->getName(),
'kind' => $element->getKind(),
'children' => array_map(
$this->elementPayload(...),
$node->getChildren(),
),
];
}
}

View file

@ -25,6 +25,13 @@ class EloquentSetRepository implements SetRepository
);
}
public function find(int $id): ?Set
{
$model = SetModel::find($id);
return $model === null ? null : $this->toDomain($model);
}
public function all(): array
{
$models = SetModel::query()

View file

@ -6,6 +6,8 @@ interface SetRepository
{
public function create(CreateSetDto $dto): Set;
public function find(int $id): ?Set;
/**
* @return list<Set>
*/

View file

@ -0,0 +1,29 @@
<?php
namespace App\Set\UseCases\GetSetLayout;
use App\Element\Element;
final readonly class ElementLayoutNode
{
/**
* @param list<ElementLayoutNode> $children
*/
public function __construct(
private Element $element,
private array $children,
) {}
public function getElement(): Element
{
return $this->element;
}
/**
* @return list<ElementLayoutNode>
*/
public function getChildren(): array
{
return $this->children;
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace App\Set\UseCases\GetSetLayout;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\NotFoundException;
use App\Set\SetRepository;
class GetSetLayout
{
public function __construct(
private SetRepository $setRepository,
private ElementRepository $elementRepository,
) {}
/**
* @throws NotFoundException
*/
public function execute(int $setId): SetLayout
{
$set = $this->setRepository->find($setId);
if ($set === null) {
throw new NotFoundException('set not found');
}
$elementsByParentId = [];
foreach ($this->elementRepository->findBySet($set) as $element) {
$parentId = $element->getParentElement()?->getId() ?? 0;
$elementsByParentId[$parentId][] = $element;
}
foreach ($elementsByParentId as &$siblings) {
usort($siblings, function (
Element $first,
Element $second,
): int {
$positionComparison = $first->getPosition()
<=> $second->getPosition();
if ($positionComparison !== 0) {
return $positionComparison;
}
return $first->getId() <=> $second->getId();
});
}
unset($siblings);
return new SetLayout(
set: $set,
elements: $this->buildNodes(0, $elementsByParentId),
);
}
/**
* @param array<int, list<Element>> $elementsByParentId
* @return list<ElementLayoutNode>
*/
private function buildNodes(
int $parentId,
array $elementsByParentId,
): array {
$nodes = [];
foreach ($elementsByParentId[$parentId] ?? [] as $element) {
$nodes[] = new ElementLayoutNode(
element: $element,
children: $this->buildNodes(
$element->getId(),
$elementsByParentId,
),
);
}
return $nodes;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Set\UseCases\GetSetLayout;
use App\Set\Set;
final readonly class SetLayout
{
/**
* @param list<ElementLayoutNode> $elements
*/
public function __construct(
private Set $set,
private array $elements,
) {}
public function getSet(): Set
{
return $this->set;
}
/**
* @return list<ElementLayoutNode>
*/
public function getElements(): array
{
return $this->elements;
}
}