setId === null || $request->setId < 1) { throw new BadRequestException('setId is required'); } $set = $this->setRepository->find($request->setId); if ($set === null) { throw new NotFoundException('set not found'); } if ($request->levelId === null || $request->levelId < 1) { throw new BadRequestException('levelId is required'); } $level = $this->setLevelRepository->find($request->levelId); if ( $level === null || $level->getSet()->getId() !== $set->getId() ) { throw new BadRequestException( 'level is not available for set', ); } $startDate = $this->parseDate($request->startDate, 'startDate'); $targetDate = $this->parseDate($request->targetDate, 'targetDate'); if ($targetDate < $startDate) { throw new BadRequestException( 'targetDate must not be before startDate', ); } $workloadPlacement = $this->workloadPlacement( $request->workloadPlacement, ); $elements = array_values(array_filter( $this->orderedElements($set), function (Element $element) use ($level): bool { return $element->getLevel()->getId() === $level->getId(); }, )); if ($elements === []) { throw new BadRequestException('level has no elements'); } $scheduledDates = $this->evenDistributionScheduler->scheduledDates( assignmentCount: count($elements), startDate: $startDate, targetDate: $targetDate, workloadPlacement: $workloadPlacement, ); $assignments = $this->assignments( elements: $elements, scheduledDates: $scheduledDates, ); return $this->scheduleRepository->create(new CreateScheduleDto( user: $request->user, setName: $set->getName(), elementKind: $level->getKind(), startDate: $startDate, targetDate: $targetDate, assignments: $assignments, )); } /** * @return list */ private function orderedElements(Set $set): array { $childrenByParentId = []; foreach ($this->elementRepository->findBySet($set) as $element) { $parentId = $element->getParentElement()?->getId() ?? 0; $childrenByParentId[$parentId][] = $element; } foreach ($childrenByParentId as &$children) { usort($children, function (Element $first, Element $second): int { $positionComparison = $first->getPosition() <=> $second->getPosition(); if ($positionComparison !== 0) { return $positionComparison; } return $first->getId() <=> $second->getId(); }); } unset($children); $orderedElements = []; $this->appendChildren( parentId: 0, childrenByParentId: $childrenByParentId, orderedElements: $orderedElements, ); return $orderedElements; } /** * @param array> $childrenByParentId * @param list $orderedElements */ private function appendChildren( int $parentId, array $childrenByParentId, array &$orderedElements, ): void { foreach ($childrenByParentId[$parentId] ?? [] as $element) { $orderedElements[] = $element; $this->appendChildren( parentId: $element->getId(), childrenByParentId: $childrenByParentId, orderedElements: $orderedElements, ); } } /** * @param list $elements * @param list $scheduledDates * @return list */ private function assignments( array $elements, array $scheduledDates, ): array { $assignments = []; foreach ($elements as $index => $element) { $assignments[] = new CreateScheduleAssignmentDto( name: $element->getName(), kind: $element->getKind(), path: $this->elementPath($element), scheduledDate: $scheduledDates[$index], position: $index + 1, ); } return $assignments; } /** * @return list */ private function elementPath(Element $element): array { $path = []; $currentElement = $element; while ($currentElement !== null) { array_unshift($path, $currentElement->getName()); $currentElement = $currentElement->getParentElement(); } return $path; } /** * @throws BadRequestException */ private function parseDate(?string $value, string $field): DateTimeImmutable { if ($value === null || $value === '') { throw new BadRequestException("{$field} is required"); } $date = DateTimeImmutable::createFromFormat( '!Y-m-d', $value, new DateTimeZone('UTC'), ); $errors = DateTimeImmutable::getLastErrors(); if ( $date === false || $date->format('Y-m-d') !== $value || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) ) { throw new BadRequestException( "{$field} must be a valid date in YYYY-MM-DD format", ); } return $date; } /** * @throws BadRequestException */ private function workloadPlacement(?string $value): WorkloadPlacement { if ($value === null) { return WorkloadPlacement::Middle; } $workloadPlacement = WorkloadPlacement::tryFrom($value); if ($workloadPlacement === null) { throw new BadRequestException( 'workloadPlacement must be start, middle, or end', ); } return $workloadPlacement; } }