$criteria * @return array */ public static function fromModels(iterable $criteria): array { $models = ($criteria instanceof EloquentCollection ? $criteria : collect($criteria)) ->sort(function (AcceptanceCriterion $left, AcceptanceCriterion $right): int { return [ $left->parent_acceptance_criterion_id ?? 0, $left->position, $left->id, ] <=> [ $right->parent_acceptance_criterion_id ?? 0, $right->position, $right->id, ]; }) ->values(); if ($models->isEmpty()) { return []; } /** @var array> $grouped */ $grouped = $models->groupBy('parent_acceptance_criterion_id')->all(); $items = []; self::flattenItems($items, $grouped, null, 0); return $items; } /** * @return array */ public static function normalize(mixed $value): array { if (is_array($value)) { return self::normalizeFromArray($value); } return []; } /** * @param Ticket $ticket * @param array $items */ public static function syncTicket(Ticket $ticket, array $items): void { $normalized = self::normalize($items); $ticket->acceptanceCriteria()->delete(); if ($normalized === []) { return; } $parentStack = []; $positionsByParent = []; foreach ($normalized as $item) { $indent = min($item['indent'], count($parentStack)); $parentId = $indent === 0 ? null : $parentStack[$indent - 1]; $positionKey = $parentId ?? 'root'; $position = $positionsByParent[$positionKey] ?? 0; $criterion = $ticket->acceptanceCriteria()->create([ 'parent_acceptance_criterion_id' => $parentId, 'position' => $position, 'text' => $item['text'], 'is_checked' => $item['checked'], ]); $positionsByParent[$positionKey] = $position + 1; $parentStack[$indent] = $criterion->id; $parentStack = array_slice($parentStack, 0, $indent + 1); } $ticket->unsetRelation('acceptanceCriteria'); } /** * @param array $items * @param array> $grouped */ private static function flattenItems(array &$items, array $grouped, ?int $parentId, int $indent): void { foreach ($grouped[$parentId] ?? [] as $criterion) { $items[] = [ 'checked' => $criterion->is_checked, 'indent' => $indent, 'text' => $criterion->text, ]; self::flattenItems($items, $grouped, $criterion->id, $indent + 1); } } /** * @param array $value * @return array */ private static function normalizeFromArray(array $value): array { $items = []; foreach ($value as $rawItem) { if (! is_array($rawItem)) { continue; } $text = trim((string) Arr::get($rawItem, 'text', '')); if ($text === '') { continue; } $items[] = [ 'checked' => filter_var(Arr::get($rawItem, 'checked', false), FILTER_VALIDATE_BOOLEAN), 'indent' => self::clampIndent((int) Arr::get($rawItem, 'indent', 0)), 'text' => $text, ]; } return $items; } private static function clampIndent(int $indent): int { return max(0, min(self::MAX_INDENT_LEVEL, $indent)); } }