Task module
This commit is contained in:
63
backend/src/Collection/TaskCollection.php
Normal file
63
backend/src/Collection/TaskCollection.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Collection;
|
||||
|
||||
use App\Entity\Task;
|
||||
use App\Enum\TaskStatus;
|
||||
|
||||
/**
|
||||
* @implements \IteratorAggregate<int, Task>
|
||||
*/
|
||||
final class TaskCollection implements \IteratorAggregate
|
||||
{
|
||||
/** @var list<Task> */
|
||||
private array $tasks = [];
|
||||
|
||||
/**
|
||||
* @param list<Task> $tasks
|
||||
*/
|
||||
public function __construct(array $tasks = [])
|
||||
{
|
||||
foreach ($tasks as $task) {
|
||||
$this->add($task);
|
||||
}
|
||||
}
|
||||
|
||||
private function add(Task $task): void
|
||||
{
|
||||
$this->tasks[] = $task;
|
||||
}
|
||||
|
||||
public function getIterator(): \Iterator
|
||||
{
|
||||
return new \ArrayIterator($this->tasks);
|
||||
}
|
||||
|
||||
public function filterInactive(): self
|
||||
{
|
||||
$this->tasks = [...array_filter(
|
||||
$this->tasks,
|
||||
fn (Task $t) => $t->getStatus() !== TaskStatus::Inactive
|
||||
)];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sortByDueDate(): self
|
||||
{
|
||||
usort($this->tasks, fn (Task $a, Task $b) => $a->getDate() <=> $b->getDate());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sortByDueDateDesc(): self
|
||||
{
|
||||
usort($this->tasks, fn (Task $a, Task $b) =>
|
||||
$a->getDate() === null || $b->getDate() === null
|
||||
? $a->getDate() <=> $b->getDate()
|
||||
: $b->getDate() <=> $a->getDate()
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
79
backend/src/Controller/Api/TaskController.php
Normal file
79
backend/src/Controller/Api/TaskController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\DTO\TaskRequest;
|
||||
use App\Entity\Task;
|
||||
use App\Enum\TaskStatus;
|
||||
use App\Repository\TaskRepository;
|
||||
use App\Service\TaskManager;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload as Payload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/tasks')]
|
||||
class TaskController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TaskRepository $repo,
|
||||
private readonly TaskManager $manager,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('', methods: ['GET'])]
|
||||
public function index(Request $req): JsonResponse
|
||||
{
|
||||
$tasks = match ($req->query->get('filter')) {
|
||||
'current' => $this->repo->currentTasks()->filterInactive()->sortByDueDate(),
|
||||
default => $this->repo->allTasks()->sortByDueDateDesc(),
|
||||
};
|
||||
|
||||
return $this->json($tasks, 200, [], ['groups' => ['task:read']]);
|
||||
}
|
||||
|
||||
#[Route('/statuses', methods: ['GET'])]
|
||||
public function statuses(): JsonResponse
|
||||
{
|
||||
return $this->json(TaskStatus::userSelectableValues());
|
||||
}
|
||||
|
||||
#[Route('/{id}', methods: ['GET'], requirements: ['id' => '\d+'])]
|
||||
public function show(Task $task): JsonResponse
|
||||
{
|
||||
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
|
||||
{
|
||||
$task = $this->manager->update($task, $dto);
|
||||
|
||||
return $this->json($task, 200, [], ['groups' => ['task:read']]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', methods: ['DELETE'], requirements: ['id' => '\d+'])]
|
||||
public function delete(Task $task): JsonResponse
|
||||
{
|
||||
$this->manager->delete($task);
|
||||
|
||||
return new JsonResponse(null, 204);
|
||||
}
|
||||
|
||||
#[Route('/{id}/toggle', methods: ['PATCH'], requirements: ['id' => '\d+'])]
|
||||
public function toggle(Task $task): JsonResponse
|
||||
{
|
||||
$task = $this->manager->toggle($task);
|
||||
|
||||
return $this->json($task, 200, [], ['groups' => ['task:read']]);
|
||||
}
|
||||
}
|
||||
24
backend/src/DTO/TaskRequest.php
Normal file
24
backend/src/DTO/TaskRequest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
use App\Enum\TaskStatus;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class TaskRequest
|
||||
{
|
||||
public function __construct(
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly string $name,
|
||||
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => '!Y-m-d'])]
|
||||
public readonly ?\DateTimeImmutable $date = null,
|
||||
|
||||
#[Assert\NotNull]
|
||||
public readonly TaskStatus $status = TaskStatus::Active,
|
||||
) {
|
||||
}
|
||||
}
|
||||
80
backend/src/Entity/Task.php
Normal file
80
backend/src/Entity/Task.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enum\TaskStatus;
|
||||
use App\Repository\TaskRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TaskRepository::class)]
|
||||
class Task
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['task:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['task:read'])]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['task:read'])]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
#[ORM\Column(enumType: TaskStatus::class)]
|
||||
private TaskStatus $status = TaskStatus::Active;
|
||||
|
||||
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 getDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function setDate(?\DateTimeImmutable $date): self
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['task:read'])]
|
||||
public function getStatus(): TaskStatus
|
||||
{
|
||||
if ($this->date !== null && $this->date < new \DateTimeImmutable('today')) {
|
||||
return TaskStatus::Past;
|
||||
}
|
||||
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getRawStatus(): TaskStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(TaskStatus $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
20
backend/src/Enum/TaskStatus.php
Normal file
20
backend/src/Enum/TaskStatus.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum TaskStatus: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Done = 'done';
|
||||
case Inactive = 'inactive';
|
||||
case Past = 'past';
|
||||
|
||||
/** @return list<string> */
|
||||
public static function userSelectableValues(): array
|
||||
{
|
||||
return array_values(array_map(
|
||||
fn (self $s) => $s->value,
|
||||
array_filter(self::cases(), fn (self $s) => $s !== self::Past)
|
||||
));
|
||||
}
|
||||
}
|
||||
39
backend/src/Repository/TaskRepository.php
Normal file
39
backend/src/Repository/TaskRepository.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Collection\TaskCollection;
|
||||
use App\Entity\Task;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Task>
|
||||
*/
|
||||
class TaskRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Task::class);
|
||||
}
|
||||
|
||||
public function currentTasks(): TaskCollection
|
||||
{
|
||||
$from = new \DateTimeImmutable('today');
|
||||
$to = $from->modify('+14 days');
|
||||
|
||||
$tasks = $this->createQueryBuilder('t')
|
||||
->andWhere('t.date IS NULL OR (t.date >= :from AND t.date <= :to)')
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
return new TaskCollection($tasks);
|
||||
}
|
||||
|
||||
public function allTasks(): TaskCollection
|
||||
{
|
||||
return new TaskCollection(parent::findAll());
|
||||
}
|
||||
}
|
||||
61
backend/src/Service/TaskManager.php
Normal file
61
backend/src/Service/TaskManager.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\DTO\TaskRequest;
|
||||
use App\Entity\Task;
|
||||
use App\Enum\TaskStatus;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class TaskManager
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
{
|
||||
}
|
||||
|
||||
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);
|
||||
$task->setDate($req->date);
|
||||
$task->setStatus($req->status);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
public function delete(Task $task): void
|
||||
{
|
||||
$this->em->remove($task);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function toggle(Task $task): Task
|
||||
{
|
||||
$new = match ($task->getRawStatus()) {
|
||||
TaskStatus::Active => TaskStatus::Done,
|
||||
TaskStatus::Done => TaskStatus::Active,
|
||||
TaskStatus::Inactive => TaskStatus::Inactive,
|
||||
TaskStatus::Past => TaskStatus::Past,
|
||||
};
|
||||
|
||||
$task->setStatus($new);
|
||||
$this->em->flush();
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user