createStub(UserInterface::class); $subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user)); $entity = new FullAuditableFixture(); $subscriber->prePersist($this->prePersistArgs($entity)); // Les 4 colonnes sont remplies : dates posees, blame = user courant. self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt()); self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt()); self::assertSame($entity->getCreatedAt(), $entity->getUpdatedAt()); self::assertSame($user, $entity->getCreatedBy()); self::assertSame($user, $entity->getUpdatedBy()); } public function testPrePersistWithoutUser(): void { $subscriber = new TimestampableBlamableSubscriber($this->securityReturning(null)); $entity = new FullAuditableFixture(); $subscriber->prePersist($this->prePersistArgs($entity)); // Hors contexte HTTP (CLI / cron) : dates remplies, blame laisse a null. self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt()); self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt()); self::assertNull($entity->getCreatedBy()); self::assertNull($entity->getUpdatedBy()); } public function testPreUpdate(): void { $user = $this->createStub(UserInterface::class); $subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user)); // On simule une entite deja persistee : createdAt fige dans le passe, // createdBy positionne par une creation anterieure. $createdAt = new DateTimeImmutable('2020-01-01 10:00:00'); $entity = new FullAuditableFixture(); $entity->setCreatedAt($createdAt); $entity->setUpdatedAt($createdAt); $subscriber->preUpdate($this->preUpdateArgs($entity)); // updatedAt avance, createdAt reste fige, updatedBy = user courant. self::assertSame($createdAt, $entity->getCreatedAt()); self::assertGreaterThan($createdAt, $entity->getUpdatedAt()); self::assertSame($user, $entity->getUpdatedBy()); } public function testPartialEntityTimestampableOnly(): void { $user = $this->createStub(UserInterface::class); $subscriber = new TimestampableBlamableSubscriber($this->securityReturning($user)); $entity = new TimestampableOnlyFixture(); // Entite Timestampable mais NON Blamable : seules les dates sont posees, // aucun appel de blame (et aucune erreur). $subscriber->prePersist($this->prePersistArgs($entity)); self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt()); self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt()); } /** * Security stubbee renvoyant l'utilisateur fourni (ou null). */ private function securityReturning(?UserInterface $user): Security { $security = $this->createStub(Security::class); $security->method('getUser')->willReturn($user); return $security; } private function prePersistArgs(object $entity): PrePersistEventArgs { return new PrePersistEventArgs($entity, $this->createStub(EntityManagerInterface::class)); } private function preUpdateArgs(object $entity): PreUpdateEventArgs { $changeSet = []; return new PreUpdateEventArgs($entity, $this->createStub(EntityManagerInterface::class), $changeSet); } } /** * Fixture interne : entite metier complete (Timestampable + Blamable) via le * Trait reel teste. * * @internal */ final class FullAuditableFixture implements TimestampableInterface, BlamableInterface { use TimestampableBlamableTrait; } /** * Fixture interne : entite Timestampable seule (sans Blamable), pour verifier * la dissociation des deux contrats par le Subscriber. * * @internal */ final class TimestampableOnlyFixture implements TimestampableInterface { private ?DateTimeImmutable $createdAt = null; private ?DateTimeImmutable $updatedAt = null; public function getCreatedAt(): ?DateTimeImmutable { return $this->createdAt; } public function setCreatedAt(DateTimeImmutable $createdAt): void { $this->createdAt = $createdAt; } public function getUpdatedAt(): ?DateTimeImmutable { return $this->updatedAt; } public function setUpdatedAt(DateTimeImmutable $updatedAt): void { $this->updatedAt = $updatedAt; } }