Attainly/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php

230 lines
6.7 KiB
PHP

<?php
namespace App\Schedule\UseCases\CreateSchedule;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Schedule\CreateScheduleAssignmentDto;
use App\Schedule\CreateScheduleDto;
use App\Schedule\Schedule;
use App\Schedule\ScheduleRepository;
use App\Set\Set;
use App\Set\SetRepository;
use DateTimeImmutable;
use DateTimeZone;
class CreateSchedule
{
public function __construct(
private SetRepository $setRepository,
private ElementRepository $elementRepository,
private ScheduleRepository $scheduleRepository,
) {}
/**
* @throws BadRequestException
* @throws NotFoundException
*/
public function execute(CreateScheduleRequest $request): Schedule
{
if ($request->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->elementKind === null || $request->elementKind === '') {
throw new BadRequestException('elementKind is required');
}
$startDate = $this->parseDate($request->startDate, 'startDate');
$targetDate = $this->parseDate($request->targetDate, 'targetDate');
if ($targetDate < $startDate) {
throw new BadRequestException(
'targetDate must not be before startDate',
);
}
$elements = array_values(array_filter(
$this->orderedElements($set),
function (Element $element) use ($request): bool {
return $element->getKind() === $request->elementKind;
},
));
if ($elements === []) {
throw new BadRequestException(
'elementKind is not available for set',
);
}
$assignments = $this->assignments(
elements: $elements,
startDate: $startDate,
targetDate: $targetDate,
);
return $this->scheduleRepository->create(new CreateScheduleDto(
user: $request->user,
setName: $set->getName(),
elementKind: $request->elementKind,
startDate: $startDate,
targetDate: $targetDate,
assignments: $assignments,
));
}
/**
* @return list<Element>
*/
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<int, list<Element>> $childrenByParentId
* @param list<Element> $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<Element> $elements
* @return list<CreateScheduleAssignmentDto>
*/
private function assignments(
array $elements,
DateTimeImmutable $startDate,
DateTimeImmutable $targetDate,
): array {
$differenceInDays = $startDate->diff($targetDate)->days;
$dayCount = $differenceInDays + 1;
$elementCount = count($elements);
$assignments = [];
foreach ($elements as $index => $element) {
$dayIndex = $this->dayIndex(
elementIndex: $index,
elementCount: $elementCount,
dayCount: $dayCount,
);
$assignments[] = new CreateScheduleAssignmentDto(
name: $element->getName(),
kind: $element->getKind(),
path: $this->elementPath($element),
scheduledDate: $startDate->modify("+{$dayIndex} days"),
position: $index + 1,
);
}
return $assignments;
}
/**
* @return list<string>
*/
private function elementPath(Element $element): array
{
$path = [];
$currentElement = $element;
while ($currentElement !== null) {
array_unshift($path, $currentElement->getName());
$currentElement = $currentElement->getParentElement();
}
return $path;
}
private function dayIndex(
int $elementIndex,
int $elementCount,
int $dayCount,
): int {
if ($elementCount === 1 || $dayCount === 1) {
return 0;
}
if ($elementCount < $dayCount) {
$scaledIndex = $elementIndex * ($dayCount - 1)
/ ($elementCount - 1);
return (int) floor($scaledIndex + 0.5);
}
return intdiv($elementIndex * $dayCount, $elementCount);
}
/**
* @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;
}
}