feat(time-tracking) : migrate TimeEntry into TimeTracking module (back)
First business module of Phase 2 (LST-64, rodage). Strangler-style, additive move — no behavioural change to the public API or MCP tools. - New module App\Module\TimeTracking (TimeTrackingModule, id "time-tracking", declares time-tracking.entries.view/export permissions in the RBAC catalog; operation security left on ROLE_USER, not re-wired here). - Move TimeEntry entity, repository (now interface + Doctrine impl bound in services.yaml), ActiveTimeEntryProvider, export service/controller and the 4 MCP TimeEntry tools into the module. #[ApiResource] (operations, security, uriTemplates /time_entries/*), filters and serialization groups preserved. - Doctrine mapping "TimeTracking" added; table time_entry unchanged. - Sidebar item gated with module "time-tracking" (SidebarFilter disables the route when the module is inactive). - Timestampable/Blamable adopted (first adopter): additive migration adds created_at/updated_at/created_by/updated_by (nullable, FK SET NULL) + COMMENT ON COLUMN. Functional test confirms created_at on persist and updated_at refresh on update — the suspected preUpdate recompute issue does not occur (Doctrine ORM 3.6.2 recomputes change sets after preUpdate). 159 tests green, schema mapping valid, php-cs-fixer clean.
This commit is contained in:
@@ -17,7 +17,6 @@ use App\Entity\TaskPriority;
|
||||
use App\Entity\TaskRecurrence;
|
||||
use App\Entity\TaskStatus;
|
||||
use App\Entity\TaskTag;
|
||||
use App\Entity\TimeEntry;
|
||||
use App\Entity\Workflow;
|
||||
use App\Entity\ZimbraConfiguration;
|
||||
use App\Enum\AbsenceStatus;
|
||||
@@ -27,6 +26,7 @@ use App\Enum\RecurrenceType;
|
||||
use App\Enum\StatusCategory;
|
||||
use App\Module\Core\Application\Rbac\RbacSeeder;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
|
||||
@@ -16,8 +16,8 @@ use App\Entity\TaskGroup;
|
||||
use App\Entity\TaskPriority;
|
||||
use App\Entity\TaskStatus;
|
||||
use App\Entity\TaskTag;
|
||||
use App\Entity\TimeEntry;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
namespace App\Module\TimeTracking\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
@@ -13,9 +13,15 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\TimeEntryRepository;
|
||||
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\TimestampableInterface;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use App\State\ActiveTimeEntryProvider;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -52,10 +58,12 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
)]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['user' => 'exact', 'project' => 'exact', 'tags' => 'exact'])]
|
||||
#[ApiFilter(DateFilter::class, properties: ['startedAt'])]
|
||||
#[ORM\Entity(repositoryClass: TimeEntryRepository::class)]
|
||||
#[ORM\Entity(repositoryClass: DoctrineTimeEntryRepository::class)]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_active_timer', columns: ['user_id'], options: ['where' => '(stopped_at IS NULL)'])]
|
||||
class TimeEntry
|
||||
class TimeEntry implements TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
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\UserInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
interface TimeEntryRepositoryInterface
|
||||
{
|
||||
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder;
|
||||
|
||||
public function findById(int $id): ?TimeEntry;
|
||||
|
||||
public function findActiveByUser(UserInterface $user): ?TimeEntry;
|
||||
|
||||
/**
|
||||
* @param null|UserInterface[] $users
|
||||
* @param null|Project[] $projects
|
||||
* @param null|int[] $tagIds
|
||||
*
|
||||
* @return TimeEntry[]
|
||||
*/
|
||||
public function findForExport(
|
||||
DateTimeImmutable $after,
|
||||
DateTimeImmutable $before,
|
||||
?array $users = null,
|
||||
?array $projects = null,
|
||||
?array $tagIds = null,
|
||||
): array;
|
||||
}
|
||||
+4
-4
@@ -2,12 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
namespace App\Module\TimeTracking\Infrastructure\ApiPlatform\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\TimeEntry;
|
||||
use App\Repository\TimeEntryRepository;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/**
|
||||
@@ -17,7 +17,7 @@ final readonly class ActiveTimeEntryProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private TimeEntryRepository $timeEntryRepository,
|
||||
private TimeEntryRepositoryInterface $timeEntryRepository,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
+4
-4
@@ -2,12 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Controller;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Repository\TimeEntryRepository;
|
||||
use App\Service\TimeEntryExportService;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use App\Module\TimeTracking\Infrastructure\Export\TimeEntryExportService;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
@@ -23,7 +23,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
class TimeEntryExportController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TimeEntryRepository $timeEntryRepository,
|
||||
private readonly TimeEntryRepositoryInterface $timeEntryRepository,
|
||||
private readonly TimeEntryExportService $exportService,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
+9
-3
@@ -2,10 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Doctrine;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\TimeEntry;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use App\Shared\Domain\Contract\UserInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -14,13 +15,18 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
/**
|
||||
* @extends ServiceEntityRepository<TimeEntry>
|
||||
*/
|
||||
class TimeEntryRepository extends ServiceEntityRepository
|
||||
class DoctrineTimeEntryRepository extends ServiceEntityRepository implements TimeEntryRepositoryInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, TimeEntry::class);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?TimeEntry
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
public function findActiveByUser(UserInterface $user): ?TimeEntry
|
||||
{
|
||||
return $this->findOneBy([
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Export;
|
||||
|
||||
use App\Entity\TimeEntry;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use DateTimeImmutable;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
+4
-4
@@ -2,15 +2,15 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\TimeEntry;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Entity\TimeEntry;
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
|
||||
use App\Module\TimeTracking\Domain\Entity\TimeEntry;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TaskRepository;
|
||||
use App\Repository\TaskTagRepository;
|
||||
use App\Repository\TimeEntryRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
@@ -29,7 +29,7 @@ class CreateTimeEntryTool
|
||||
private readonly ProjectRepository $projectRepository,
|
||||
private readonly TaskRepository $taskRepository,
|
||||
private readonly TaskTagRepository $taskTagRepository,
|
||||
private readonly TimeEntryRepository $timeEntryRepository,
|
||||
private readonly TimeEntryRepositoryInterface $timeEntryRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
+4
-4
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\TimeEntry;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Repository\TimeEntryRepository;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
@@ -17,7 +17,7 @@ use function sprintf;
|
||||
class DeleteTimeEntryTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TimeEntryRepository $timeEntryRepository,
|
||||
private readonly TimeEntryRepositoryInterface $timeEntryRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
@@ -28,7 +28,7 @@ class DeleteTimeEntryTool
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$entry = $this->timeEntryRepository->find($id);
|
||||
$entry = $this->timeEntryRepository->findById($id);
|
||||
|
||||
if (null === $entry) {
|
||||
throw new InvalidArgumentException(sprintf('TimeEntry with ID %d not found.', $id));
|
||||
+3
-3
@@ -2,10 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\TimeEntry;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Repository\TimeEntryRepository;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use DateTimeImmutable;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
class ListTimeEntriesTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TimeEntryRepository $timeEntryRepository,
|
||||
private readonly TimeEntryRepositoryInterface $timeEntryRepository,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
+4
-4
@@ -2,13 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\TimeEntry;
|
||||
namespace App\Module\TimeTracking\Infrastructure\Mcp\Tool;
|
||||
|
||||
use App\Mcp\Tool\Serializer;
|
||||
use App\Module\TimeTracking\Domain\Repository\TimeEntryRepositoryInterface;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TaskRepository;
|
||||
use App\Repository\TaskTagRepository;
|
||||
use App\Repository\TimeEntryRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
@@ -22,7 +22,7 @@ use function sprintf;
|
||||
class UpdateTimeEntryTool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TimeEntryRepository $timeEntryRepository,
|
||||
private readonly TimeEntryRepositoryInterface $timeEntryRepository,
|
||||
private readonly ProjectRepository $projectRepository,
|
||||
private readonly TaskRepository $taskRepository,
|
||||
private readonly TaskTagRepository $taskTagRepository,
|
||||
@@ -47,7 +47,7 @@ class UpdateTimeEntryTool
|
||||
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
||||
}
|
||||
|
||||
$entry = $this->timeEntryRepository->find($id);
|
||||
$entry = $this->timeEntryRepository->findById($id);
|
||||
|
||||
if (null === $entry) {
|
||||
throw new InvalidArgumentException(sprintf('TimeEntry with ID %d not found.', $id));
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\TimeTracking;
|
||||
|
||||
use App\Shared\Domain\Module\ModuleInterface;
|
||||
|
||||
final class TimeTrackingModule implements ModuleInterface
|
||||
{
|
||||
public static function id(): string
|
||||
{
|
||||
return 'time-tracking';
|
||||
}
|
||||
|
||||
public static function label(): string
|
||||
{
|
||||
return 'Suivi des temps';
|
||||
}
|
||||
|
||||
public static function isRequired(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions RBAC fin du Module TimeTracking (2.1).
|
||||
*
|
||||
* Additif : alimente le catalogue RBAC. La sécurité des opérations API
|
||||
* reste en ROLE_USER (non recâblée ici).
|
||||
*
|
||||
* @return list<array{code: string, label: string}>
|
||||
*/
|
||||
public static function permissions(): array
|
||||
{
|
||||
return [
|
||||
['code' => 'time-tracking.entries.view', 'label' => 'Voir les saisies de temps'],
|
||||
['code' => 'time-tracking.entries.export', 'label' => 'Exporter les saisies de temps'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user