TaskSchema module
This commit is contained in:
29
backend/src/Collection/TaskSchemaCollection.php
Normal file
29
backend/src/Collection/TaskSchemaCollection.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Collection;
|
||||
|
||||
use App\Entity\TaskSchema;
|
||||
|
||||
/**
|
||||
* @implements \IteratorAggregate<int, TaskSchema>
|
||||
*/
|
||||
final class TaskSchemaCollection implements \IteratorAggregate
|
||||
{
|
||||
/** @var list<TaskSchema> */
|
||||
private array $schemas = [];
|
||||
|
||||
/**
|
||||
* @param list<TaskSchema> $schemas
|
||||
*/
|
||||
public function __construct(array $schemas = [])
|
||||
{
|
||||
foreach ($schemas as $schema) {
|
||||
$this->schemas[] = $schema;
|
||||
}
|
||||
}
|
||||
|
||||
public function getIterator(): \Iterator
|
||||
{
|
||||
return new \ArrayIterator($this->schemas);
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,6 @@ class TaskController extends AbstractController
|
||||
return $this->json($task, 200, [], ['groups' => ['task:read']]);
|
||||
}
|
||||
|
||||
#[Route('', methods: ['POST'])]
|
||||
public function create(#[Payload] TaskRequest $dto): JsonResponse
|
||||
{
|
||||
$task = $this->manager->create($dto);
|
||||
|
||||
return $this->json($task, 201, [], ['groups' => ['task:read']]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
||||
public function update(Task $task, #[Payload] TaskRequest $dto): JsonResponse
|
||||
{
|
||||
|
||||
60
backend/src/Controller/Api/TaskSchemaController.php
Normal file
60
backend/src/Controller/Api/TaskSchemaController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\DTO\TaskSchemaRequest;
|
||||
use App\Entity\TaskSchema;
|
||||
use App\Repository\TaskSchemaRepository;
|
||||
use App\Service\TaskSchemaManager;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload as Payload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/task-schemas')]
|
||||
class TaskSchemaController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TaskSchemaRepository $repo,
|
||||
private readonly TaskSchemaManager $manager,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('', methods: ['GET'])]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$schemas = $this->repo->allSchemas();
|
||||
|
||||
return $this->json($schemas, 200, [], ['groups' => ['task_schema:read']]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', methods: ['GET'], requirements: ['id' => '\d+'])]
|
||||
public function show(TaskSchema $schema): JsonResponse
|
||||
{
|
||||
return $this->json($schema, 200, [], ['groups' => ['task_schema:read']]);
|
||||
}
|
||||
|
||||
#[Route('', methods: ['POST'])]
|
||||
public function create(#[Payload] TaskSchemaRequest $dto): JsonResponse
|
||||
{
|
||||
$this->manager->create($dto);
|
||||
|
||||
return new JsonResponse(null, 201);
|
||||
}
|
||||
|
||||
#[Route('/{id}', methods: ['PUT'], requirements: ['id' => '\d+'])]
|
||||
public function update(TaskSchema $schema, #[Payload] TaskSchemaRequest $dto): JsonResponse
|
||||
{
|
||||
$this->manager->update($schema, $dto);
|
||||
|
||||
return $this->json($schema, 200, [], ['groups' => ['task_schema:read']]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
||||
public function delete(TaskSchema $schema): JsonResponse
|
||||
{
|
||||
$this->manager->delete($schema);
|
||||
|
||||
return new JsonResponse(null, 204);
|
||||
}
|
||||
}
|
||||
36
backend/src/DTO/TaskSchemaRequest.php
Normal file
36
backend/src/DTO/TaskSchemaRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
use App\Enum\TaskSchemaStatus;
|
||||
use App\Enum\TaskStatus;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class TaskSchemaRequest
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly string $name,
|
||||
|
||||
#[Assert\NotNull]
|
||||
public readonly TaskSchemaStatus $status = TaskSchemaStatus::Active,
|
||||
|
||||
#[Assert\NotNull]
|
||||
public readonly TaskStatus $taskStatus = TaskStatus::Active,
|
||||
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||
public readonly ?\DateTimeImmutable $date = null,
|
||||
|
||||
public readonly ?array $repeat = null,
|
||||
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||
public readonly ?\DateTimeImmutable $start = null,
|
||||
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||
public readonly ?\DateTimeImmutable $end = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Enum\TaskStatus;
|
||||
use App\Repository\TaskRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TaskRepository::class)]
|
||||
class Task
|
||||
@@ -27,6 +28,11 @@ class Task
|
||||
#[ORM\Column(enumType: TaskStatus::class)]
|
||||
private TaskStatus $status = TaskStatus::Active;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: TaskSchema::class, inversedBy: 'tasks')]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
#[Ignore]
|
||||
private ?TaskSchema $schema = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -77,4 +83,16 @@ class Task
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSchema(): ?TaskSchema
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
public function setSchema(?TaskSchema $schema): self
|
||||
{
|
||||
$this->schema = $schema;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
158
backend/src/Entity/TaskSchema.php
Normal file
158
backend/src/Entity/TaskSchema.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enum\TaskSchemaStatus;
|
||||
use App\Enum\TaskStatus;
|
||||
use App\Repository\TaskSchemaRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TaskSchemaRepository::class)]
|
||||
class TaskSchema
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(enumType: TaskSchemaStatus::class)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private TaskSchemaStatus $status = TaskSchemaStatus::Active;
|
||||
|
||||
#[ORM\Column(enumType: TaskStatus::class)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private TaskStatus $taskStatus = TaskStatus::Active;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
#[ORM\Column(name: '`repeat`', type: 'json', nullable: true)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private ?array $repeat = null;
|
||||
|
||||
#[ORM\Column(name: '`start`', type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private ?\DateTimeImmutable $start = null;
|
||||
|
||||
#[ORM\Column(name: '`end`', type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['task_schema:read'])]
|
||||
private ?\DateTimeImmutable $end = null;
|
||||
|
||||
/** @var Collection<int, Task> */
|
||||
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'schema')]
|
||||
private Collection $tasks;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->tasks = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): TaskSchemaStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(TaskSchemaStatus $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTaskStatus(): TaskStatus
|
||||
{
|
||||
return $this->taskStatus;
|
||||
}
|
||||
|
||||
public function setTaskStatus(TaskStatus $taskStatus): self
|
||||
{
|
||||
$this->taskStatus = $taskStatus;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function setDate(?\DateTimeImmutable $date): self
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRepeat(): ?array
|
||||
{
|
||||
return $this->repeat;
|
||||
}
|
||||
|
||||
public function getRepeatType(): ?string
|
||||
{
|
||||
return $this->repeat !== null ? array_key_first($this->repeat) : null;
|
||||
}
|
||||
|
||||
public function setRepeat(?array $repeat): self
|
||||
{
|
||||
$this->repeat = $repeat;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStart(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function setStart(?\DateTimeImmutable $start): self
|
||||
{
|
||||
$this->start = $start;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnd(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
public function setEnd(?\DateTimeImmutable $end): self
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, Task> */
|
||||
public function getTasks(): Collection
|
||||
{
|
||||
return $this->tasks;
|
||||
}
|
||||
}
|
||||
9
backend/src/Enum/TaskSchemaStatus.php
Normal file
9
backend/src/Enum/TaskSchemaStatus.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum TaskSchemaStatus: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Inactive = 'inactive';
|
||||
}
|
||||
7
backend/src/Message/GenerateTasksMessage.php
Normal file
7
backend/src/Message/GenerateTasksMessage.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class GenerateTasksMessage
|
||||
{
|
||||
}
|
||||
20
backend/src/MessageHandler/GenerateTasksMessageHandler.php
Normal file
20
backend/src/MessageHandler/GenerateTasksMessageHandler.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Message\GenerateTasksMessage;
|
||||
use App\Service\TaskGenerator;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final class GenerateTasksMessageHandler
|
||||
{
|
||||
public function __construct(private TaskGenerator $generator)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(GenerateTasksMessage $message): void
|
||||
{
|
||||
$this->generator->generateNewTasks();
|
||||
}
|
||||
}
|
||||
36
backend/src/Repository/TaskSchemaRepository.php
Normal file
36
backend/src/Repository/TaskSchemaRepository.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Collection\TaskSchemaCollection;
|
||||
use App\Entity\TaskSchema;
|
||||
use App\Enum\TaskSchemaStatus;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TaskSchema>
|
||||
*/
|
||||
class TaskSchemaRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TaskSchema::class);
|
||||
}
|
||||
|
||||
public function allSchemas(): TaskSchemaCollection
|
||||
{
|
||||
return new TaskSchemaCollection(parent::findAll());
|
||||
}
|
||||
|
||||
/** @return list<TaskSchema> */
|
||||
public function findActiveWithRepeat(): array
|
||||
{
|
||||
return $this->createQueryBuilder('s')
|
||||
->andWhere('s.status = :status')
|
||||
->andWhere('s.repeat IS NOT NULL')
|
||||
->setParameter('status', TaskSchemaStatus::Active)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
27
backend/src/Schedule.php
Normal file
27
backend/src/Schedule.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Message\GenerateTasksMessage;
|
||||
use Symfony\Component\Scheduler\Attribute\AsSchedule;
|
||||
use Symfony\Component\Scheduler\RecurringMessage;
|
||||
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
|
||||
use Symfony\Component\Scheduler\ScheduleProviderInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
#[AsSchedule]
|
||||
class Schedule implements ScheduleProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSchedule(): SymfonySchedule
|
||||
{
|
||||
return (new SymfonySchedule())
|
||||
->stateful($this->cache)
|
||||
->processOnlyLastMissedRun(true)
|
||||
->add(RecurringMessage::cron('0 3 * * *', new GenerateTasksMessage()));
|
||||
}
|
||||
}
|
||||
87
backend/src/Service/TaskGenerator.php
Normal file
87
backend/src/Service/TaskGenerator.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Task;
|
||||
use App\Entity\TaskSchema;
|
||||
use App\Enum\TaskStatus;
|
||||
use App\Repository\TaskSchemaRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class TaskGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private TaskSchemaRepository $schemaRepo,
|
||||
) {
|
||||
}
|
||||
|
||||
public function generateNewTasks(): void
|
||||
{
|
||||
$schemas = $this->schemaRepo->findActiveWithRepeat();
|
||||
|
||||
foreach ($schemas as $schema) {
|
||||
$this->removeTasks($schema);
|
||||
$this->generateTasks($schema);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function removeTasks(TaskSchema $schema): void
|
||||
{
|
||||
foreach ($schema->getTasks() as $task) {
|
||||
if ($task->getStatus() === TaskStatus::Past) continue;
|
||||
$this->em->remove($task);
|
||||
}
|
||||
}
|
||||
|
||||
public function generateTasks(TaskSchema $schema): void
|
||||
{
|
||||
$dates = $this->getDates($schema);
|
||||
|
||||
foreach ($dates as $date) {
|
||||
$task = new Task();
|
||||
$task->setName($schema->getName());
|
||||
$task->setDate($date);
|
||||
$task->setStatus($schema->getTaskStatus());
|
||||
$task->setSchema($schema);
|
||||
$this->em->persist($task);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{\DateTimeImmutable, \DateTimeImmutable} */
|
||||
private function getDateRange(TaskSchema $schema): array
|
||||
{
|
||||
$today = new \DateTimeImmutable('today');
|
||||
$from = max($today, $schema->getStart() ?? $today);
|
||||
$end = min($today->modify('+14 days'), $schema->getEnd() ?? $today->modify('+14 days'));
|
||||
|
||||
return [$from, $end];
|
||||
}
|
||||
|
||||
/** @return list<\DateTimeImmutable> */
|
||||
private function getDates(TaskSchema $schema): array
|
||||
{
|
||||
[$from, $end] = $this->getDateRange($schema);
|
||||
$type = $schema->getRepeatType();
|
||||
$repeat = $schema->getRepeat();
|
||||
|
||||
$dates = [];
|
||||
for ($date = $from; $date <= $end; $date = $date->modify('+1 day')) {
|
||||
|
||||
if ($type === 'weekly') {
|
||||
$weekday = (int) $date->format('N') - 1;
|
||||
if(!$repeat['weekly'][$weekday]) continue;
|
||||
}
|
||||
if ($type === 'monthly') {
|
||||
$monthday = (int) $date->format('j') - 1;
|
||||
if(!$repeat['monthly'][$monthday]) continue;
|
||||
}
|
||||
|
||||
$dates[] = $date;
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,6 @@ class TaskManager
|
||||
{
|
||||
}
|
||||
|
||||
public function create(TaskRequest $req): Task
|
||||
{
|
||||
$task = new Task();
|
||||
$task->setName($req->name);
|
||||
$task->setDate($req->date);
|
||||
$task->setStatus($req->status);
|
||||
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
public function update(Task $task, TaskRequest $req): Task
|
||||
{
|
||||
$task->setName($req->name);
|
||||
|
||||
79
backend/src/Service/TaskSchemaManager.php
Normal file
79
backend/src/Service/TaskSchemaManager.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\DTO\TaskSchemaRequest;
|
||||
use App\Entity\Task;
|
||||
use App\Entity\TaskSchema;
|
||||
use App\Enum\TaskSchemaStatus;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class TaskSchemaManager
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private TaskGenerator $generator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(TaskSchemaRequest $req): void
|
||||
{
|
||||
if ($req->repeat === null) {
|
||||
$task = new Task();
|
||||
$task->setName($req->name);
|
||||
$task->setDate($req->date);
|
||||
$task->setStatus($req->taskStatus);
|
||||
$this->em->persist($task);
|
||||
$this->em->flush();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = new TaskSchema();
|
||||
$schema->setName($req->name);
|
||||
$schema->setStatus($req->status);
|
||||
$schema->setTaskStatus($req->taskStatus);
|
||||
$schema->setDate($req->date);
|
||||
$schema->setRepeat($req->repeat);
|
||||
$schema->setStart($req->start);
|
||||
$schema->setEnd($req->end);
|
||||
$this->em->persist($schema);
|
||||
$this->em->flush();
|
||||
|
||||
if ($schema->getStatus() === TaskSchemaStatus::Inactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->generator->generateTasks($schema);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function update(TaskSchema $schema, TaskSchemaRequest $req): void
|
||||
{
|
||||
$schema->setName($req->name);
|
||||
$schema->setStatus($req->status);
|
||||
$schema->setTaskStatus($req->taskStatus);
|
||||
$schema->setDate($req->date);
|
||||
$schema->setRepeat($req->repeat);
|
||||
$schema->setStart($req->start);
|
||||
$schema->setEnd($req->end);
|
||||
|
||||
if ($schema->getStatus() === TaskSchemaStatus::Inactive) {
|
||||
$this->em->flush();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->generator->removeTasks($schema);
|
||||
$this->generator->generateTasks($schema);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function delete(TaskSchema $schema): void
|
||||
{
|
||||
$this->generator->removeTasks($schema);
|
||||
$this->em->remove($schema);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user