feat : add Task entities, repositories and migration

Add Task, TaskStatus, TaskEffort, TaskPriority, TaskType, TaskGroup
entities with Doctrine mappings and API Platform CRUD operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 23:38:39 +01:00
parent b240dc6fc4
commit 1d50e5dcb3
13 changed files with 792 additions and 0 deletions

214
src/Entity/Task.php Normal file
View File

@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task:read']],
denormalizationContext: ['groups' => ['task:write']],
order: ['id' => 'DESC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact', 'group' => 'exact'])]
#[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', 'task:write'])]
private ?string $title = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['task:read', 'task:write'])]
private ?string $description = null;
#[ORM\ManyToOne(targetEntity: TaskStatus::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskStatus $status = null;
#[ORM\ManyToOne(targetEntity: TaskEffort::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskEffort $effort = null;
#[ORM\ManyToOne(targetEntity: TaskPriority::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskPriority $priority = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?User $assignee = null;
#[ORM\ManyToOne(targetEntity: TaskGroup::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskGroup $group = null;
#[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task:read', 'task:write'])]
private ?Project $project = null;
/** @var Collection<int, TaskType> */
#[ORM\ManyToMany(targetEntity: TaskType::class)]
#[ORM\JoinTable(name: 'task_task_type')]
#[Groups(['task:read', 'task:write'])]
private Collection $types;
public function __construct()
{
$this->types = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getStatus(): ?TaskStatus
{
return $this->status;
}
public function setStatus(?TaskStatus $status): static
{
$this->status = $status;
return $this;
}
public function getEffort(): ?TaskEffort
{
return $this->effort;
}
public function setEffort(?TaskEffort $effort): static
{
$this->effort = $effort;
return $this;
}
public function getPriority(): ?TaskPriority
{
return $this->priority;
}
public function setPriority(?TaskPriority $priority): static
{
$this->priority = $priority;
return $this;
}
public function getAssignee(): ?User
{
return $this->assignee;
}
public function setAssignee(?User $assignee): static
{
$this->assignee = $assignee;
return $this;
}
public function getGroup(): ?TaskGroup
{
return $this->group;
}
public function setGroup(?TaskGroup $group): static
{
$this->group = $group;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
/** @return Collection<int, TaskType> */
public function getTypes(): Collection
{
return $this->types;
}
public function addType(TaskType $type): static
{
if (!$this->types->contains($type)) {
$this->types->add($type);
}
return $this;
}
public function removeType(TaskType $type): static
{
$this->types->removeElement($type);
return $this;
}
}

58
src/Entity/TaskEffort.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskEffortRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_effort:read']],
denormalizationContext: ['groups' => ['task_effort:write']],
order: ['label' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskEffortRepository::class)]
class TaskEffort
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_effort:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 50)]
#[Groups(['task_effort:read', 'task_effort:write', 'task:read'])]
private ?string $label = null;
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
}

110
src/Entity/TaskGroup.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskGroupRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_group:read']],
denormalizationContext: ['groups' => ['task_group:write']],
order: ['title' => 'ASC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact'])]
#[ORM\Entity(repositoryClass: TaskGroupRepository::class)]
class TaskGroup
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_group:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_group:read', 'task_group:write', 'task:read'])]
private ?string $title = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['task_group:read', 'task_group:write'])]
private ?string $description = null;
#[ORM\Column(length: 7)]
#[Groups(['task_group:read', 'task_group:write', 'task:read'])]
private ?string $color = '#222783';
#[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task_group:read', 'task_group:write'])]
private ?Project $project = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskPriorityRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_priority:read']],
denormalizationContext: ['groups' => ['task_priority:write']],
order: ['label' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskPriorityRepository::class)]
class TaskPriority
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_priority:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_priority:read', 'task_priority:write', 'task:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_priority:read', 'task_priority:write', 'task:read'])]
private ?string $color = '#222783';
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
}

90
src/Entity/TaskStatus.php Normal file
View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskStatusRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_status:read']],
denormalizationContext: ['groups' => ['task_status:write']],
order: ['position' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskStatusRepository::class)]
class TaskStatus
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_status:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
private ?string $color = '#222783';
#[ORM\Column]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
private ?int $position = 0;
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
public function getPosition(): ?int
{
return $this->position;
}
public function setPosition(int $position): static
{
$this->position = $position;
return $this;
}
}

74
src/Entity/TaskType.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskTypeRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_type:read']],
denormalizationContext: ['groups' => ['task_type:write']],
order: ['label' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskTypeRepository::class)]
class TaskType
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_type:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_type:read', 'task_type:write', 'task:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_type:read', 'task_type:write', 'task:read'])]
private ?string $color = '#222783';
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TaskEffort;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TaskEffortRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TaskEffort::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TaskGroup;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TaskGroupRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TaskGroup::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TaskPriority;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TaskPriorityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TaskPriority::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Task;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TaskRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Task::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TaskStatus;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TaskStatusRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TaskStatus::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\TaskType;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TaskTypeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TaskType::class);
}
}