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:
@@ -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]
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -13,12 +13,12 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Task;
|
||||
use App\Entity\TaskTag;
|
||||
use App\Module\TimeTracking\Infrastructure\ApiPlatform\State\ActiveTimeEntryProvider;
|
||||
use App\Module\TimeTracking\Infrastructure\Doctrine\DoctrineTimeEntryRepository;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\ProjectInterface;
|
||||
use App\Shared\Domain\Contract\TaskInterface;
|
||||
use App\Shared\Domain\Contract\TaskTagInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
@@ -91,18 +91,18 @@ class TimeEntry implements TimestampableInterface, BlamableInterface
|
||||
#[Groups(['time_entry:read', 'time_entry:write'])]
|
||||
private ?UserInterface $user = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\ManyToOne(targetEntity: ProjectInterface::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['time_entry:read', 'time_entry:write'])]
|
||||
private ?Project $project = null;
|
||||
private ?ProjectInterface $project = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Task::class)]
|
||||
#[ORM\ManyToOne(targetEntity: TaskInterface::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['time_entry:read', 'time_entry:write'])]
|
||||
private ?Task $task = null;
|
||||
private ?TaskInterface $task = null;
|
||||
|
||||
/** @var Collection<int, TaskTag> */
|
||||
#[ORM\ManyToMany(targetEntity: TaskTag::class)]
|
||||
/** @var Collection<int, TaskTagInterface> */
|
||||
#[ORM\ManyToMany(targetEntity: TaskTagInterface::class)]
|
||||
#[ORM\JoinTable(
|
||||
name: 'time_entry_task_type',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'time_entry_id', referencedColumnName: 'id')],
|
||||
@@ -181,37 +181,37 @@ class TimeEntry implements TimestampableInterface, BlamableInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
public function getProject(): ?ProjectInterface
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(?Project $project): static
|
||||
public function setProject(?ProjectInterface $project): static
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTask(): ?Task
|
||||
public function getTask(): ?TaskInterface
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
public function setTask(?Task $task): static
|
||||
public function setTask(?TaskInterface $task): static
|
||||
{
|
||||
$this->task = $task;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, TaskTag> */
|
||||
/** @return Collection<int, TaskTagInterface> */
|
||||
public function getTags(): Collection
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function addTag(TaskTag $tag): static
|
||||
public function addTag(TaskTagInterface $tag): static
|
||||
{
|
||||
if (!$this->tags->contains($tag)) {
|
||||
$this->tags->add($tag);
|
||||
@@ -220,7 +220,7 @@ class TimeEntry implements TimestampableInterface, BlamableInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTag(TaskTag $tag): static
|
||||
public function removeTag(TaskTagInterface $tag): static
|
||||
{
|
||||
$this->tags->removeElement($tag);
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Module\TimeTracking\Domain\Repository;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use App\Shared\Domain\Contract\ProjectInterface;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -19,9 +19,9 @@ interface TimeEntryRepositoryInterface
|
||||
public function findActiveByUser(UserInterface $user): ?TimeEntry;
|
||||
|
||||
/**
|
||||
* @param null|UserInterface[] $users
|
||||
* @param null|Project[] $projects
|
||||
* @param null|int[] $tagIds
|
||||
* @param null|UserInterface[] $users
|
||||
* @param null|ProjectInterface[] $projects
|
||||
* @param null|int[] $tagIds
|
||||
*
|
||||
* @return TimeEntry[]
|
||||
*/
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Module\TimeTracking\Infrastructure\Doctrine;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use App\Shared\Domain\Contract\ProjectInterface;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -36,9 +36,9 @@ class DoctrineTimeEntryRepository extends ServiceEntityRepository implements Tim
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|UserInterface[] $users
|
||||
* @param null|Project[] $projects
|
||||
* @param null|int[] $tagIds
|
||||
* @param null|UserInterface[] $users
|
||||
* @param null|ProjectInterface[] $projects
|
||||
* @param null|int[] $tagIds
|
||||
*
|
||||
* @return TimeEntry[]
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat de LECTURE d'un client, consommé hors du module qui le possède.
|
||||
*/
|
||||
interface ClientInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getName(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat de LECTURE d'un projet, consommé hors du module qui le possède.
|
||||
*/
|
||||
interface ProjectInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getCode(): ?string;
|
||||
|
||||
public function getName(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat de LECTURE d'une tâche, consommé hors du module qui la possède.
|
||||
*/
|
||||
interface TaskInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getNumber(): ?int;
|
||||
|
||||
public function getTitle(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat de LECTURE d'un tag de tâche, consommé hors du module qui le possède.
|
||||
*/
|
||||
interface TaskTagInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getLabel(): ?string;
|
||||
|
||||
public function getColor(): ?string;
|
||||
}
|
||||
Reference in New Issue
Block a user