refactor(project-management) : introduce Project/Task/TaskTag/Client contracts

Tranche 1 of LST-65 (ProjectManagement module migration). Decouples the
TimeTracking module from the core-business entities before they move, with
no entity relocation yet — keeps the diff minimal and the risk isolated.

- New read contracts in Shared/Domain/Contract (minimal surface, aligned on
  the entities' real nullable signatures): ProjectInterface (id/code/name),
  TaskInterface (id/number/title), TaskTagInterface (id/label/color),
  ClientInterface (id/name).
- Project/Task/TaskTag/Client implement their contract (entities stay in
  src/Entity for now). Project.client typed as ClientInterface.
- TimeEntry (TimeTracking) now references ProjectInterface/TaskInterface/
  TaskTagInterface instead of the concrete entities; repository + DQL
  untouched in behaviour.
- resolve_target_entities maps the 4 contracts to the legacy entities (will
  be repointed to the module in tranche 2).
- Adds the migration plan doc.

159 tests green, mapping valid, cs-fixer clean.
This commit is contained in:
Matthieu
2026-06-20 16:34:15 +02:00
parent 1b652ef680
commit f119ec30ca
13 changed files with 189 additions and 32 deletions
+2 -1
View File
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\ClientRepository;
use App\Shared\Domain\Contract\ClientInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@@ -29,7 +30,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
order: ['name' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: ClientRepository::class)]
class Client
class Client implements ClientInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
+7 -5
View File
@@ -15,6 +15,8 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\ApiResource\SwitchWorkflowOutput;
use App\Repository\ProjectRepository;
use App\Shared\Domain\Contract\ClientInterface;
use App\Shared\Domain\Contract\ProjectInterface;
use App\State\SwitchProjectWorkflowProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@@ -54,7 +56,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiFilter(BooleanFilter::class, properties: ['archived'])]
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
#[UniqueEntity(fields: ['code'], message: 'Ce code de projet est déjà utilisé.')]
class Project
class Project implements ProjectInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
@@ -80,10 +82,10 @@ class Project
#[Groups(['project:read', 'project:write', 'time_entry:read', 'task:read'])]
private ?string $color = '#222783';
#[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'projects')]
#[ORM\ManyToOne(targetEntity: ClientInterface::class, inversedBy: 'projects')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['project:read', 'project:write'])]
private ?Client $client = null;
private ?ClientInterface $client = null;
#[ORM\ManyToOne(targetEntity: Workflow::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]
@@ -173,12 +175,12 @@ class Project
return $this;
}
public function getClient(): ?Client
public function getClient(): ?ClientInterface
{
return $this->client;
}
public function setClient(?Client $client): static
public function setClient(?ClientInterface $client): static
{
$this->client = $client;
+2 -1
View File
@@ -16,6 +16,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskRepository;
use App\Shared\Domain\Contract\TaskInterface;
use App\Shared\Domain\Contract\UserInterface;
use App\State\TaskCalendarProcessor;
use App\State\TaskNumberProcessor;
@@ -46,7 +47,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Entity(repositoryClass: TaskRepository::class)]
#[ORM\Table(name: 'task')]
#[ORM\UniqueConstraint(name: 'uniq_task_project_number', columns: ['project_id', 'number'])]
class Task
class Task implements TaskInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
+2 -1
View File
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\TaskTagRepository;
use App\Shared\Domain\Contract\TaskTagInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
@@ -28,7 +29,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
#[ORM\Entity(repositoryClass: TaskTagRepository::class)]
#[ORM\Table(name: 'task_type')]
class TaskTag
class TaskTag implements TaskTagInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]