diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 6c57caf..303521a 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -13,6 +13,8 @@ doctrine: identity_generation_preferences: Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity auto_mapping: true + resolve_target_entities: + App\Shared\Domain\Contract\UserInterface: App\Entity\User mappings: App: type: attribute diff --git a/src/Entity/User.php b/src/Entity/User.php index 2bc49c9..38e789e 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -13,6 +13,7 @@ use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Enum\ContractType; use App\Repository\UserRepository; +use App\Shared\Domain\Contract\UserInterface as SharedUserInterface; use App\State\MeProvider; use App\State\UserPasswordHasherProcessor; use DateTimeImmutable; @@ -44,7 +45,7 @@ use Symfony\Component\Serializer\Attribute\Groups; )] #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Table(name: '`user`')] -class User implements UserInterface, PasswordAuthenticatedUserInterface +class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Shared/Application/CurrentUserProviderInterface.php b/src/Shared/Application/CurrentUserProviderInterface.php new file mode 100644 index 0000000..da670e2 --- /dev/null +++ b/src/Shared/Application/CurrentUserProviderInterface.php @@ -0,0 +1,12 @@ +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; + } + + public function getCreatedBy(): ?UserInterface + { + return $this->createdBy; + } + + public function setCreatedBy(?UserInterface $user): void + { + $this->createdBy = $user; + } + + public function getUpdatedBy(): ?UserInterface + { + return $this->updatedBy; + } + + public function setUpdatedBy(?UserInterface $user): void + { + $this->updatedBy = $user; + } +} diff --git a/src/Shared/Infrastructure/Doctrine/TimestampableBlamableSubscriber.php b/src/Shared/Infrastructure/Doctrine/TimestampableBlamableSubscriber.php new file mode 100644 index 0000000..2aa832a --- /dev/null +++ b/src/Shared/Infrastructure/Doctrine/TimestampableBlamableSubscriber.php @@ -0,0 +1,64 @@ +applyOnCreate($args->getObject()); + } + + public function preUpdate(PreUpdateEventArgs $args): void + { + $this->applyOnUpdate($args->getObject()); + } + + public function applyOnCreate(object $entity): void + { + $now = new DateTimeImmutable(); + + if ($entity instanceof TimestampableInterface) { + if (null === $entity->getCreatedAt()) { + $entity->setCreatedAt($now); + } + $entity->setUpdatedAt($now); + } + + if ($entity instanceof BlamableInterface) { + $user = $this->currentUserProvider->getCurrentUser(); + if (null === $entity->getCreatedBy()) { + $entity->setCreatedBy($user); + } + $entity->setUpdatedBy($user); + } + } + + public function applyOnUpdate(object $entity): void + { + if ($entity instanceof TimestampableInterface) { + $entity->setUpdatedAt(new DateTimeImmutable()); + } + + if ($entity instanceof BlamableInterface) { + $entity->setUpdatedBy($this->currentUserProvider->getCurrentUser()); + } + } +} diff --git a/src/Shared/Infrastructure/Security/SecurityCurrentUserProvider.php b/src/Shared/Infrastructure/Security/SecurityCurrentUserProvider.php new file mode 100644 index 0000000..2d9a5b3 --- /dev/null +++ b/src/Shared/Infrastructure/Security/SecurityCurrentUserProvider.php @@ -0,0 +1,23 @@ +security->getUser(); + + return $user instanceof UserInterface ? $user : null; + } +} diff --git a/tests/Unit/Shared/Doctrine/TimestampableBlamableSubscriberTest.php b/tests/Unit/Shared/Doctrine/TimestampableBlamableSubscriberTest.php new file mode 100644 index 0000000..669acf5 --- /dev/null +++ b/tests/Unit/Shared/Doctrine/TimestampableBlamableSubscriberTest.php @@ -0,0 +1,91 @@ +makeUser(7); + $subscriber = new TimestampableBlamableSubscriber($this->providerReturning($user)); + $entity = $this->makeEntity(); + + $subscriber->applyOnCreate($entity); + + self::assertInstanceOf(DateTimeImmutable::class, $entity->getCreatedAt()); + self::assertInstanceOf(DateTimeImmutable::class, $entity->getUpdatedAt()); + self::assertSame($user, $entity->getCreatedBy()); + self::assertSame($user, $entity->getUpdatedBy()); + } + + public function testApplyOnUpdateLeavesCreatedUntouched(): void + { + $creator = $this->makeUser(1); + $editor = $this->makeUser(2); + $entity = $this->makeEntity(); + + new TimestampableBlamableSubscriber($this->providerReturning($creator))->applyOnCreate($entity); + $createdAt = $entity->getCreatedAt(); + + new TimestampableBlamableSubscriber($this->providerReturning($editor))->applyOnUpdate($entity); + + self::assertSame($createdAt, $entity->getCreatedAt()); + self::assertSame($creator, $entity->getCreatedBy()); + self::assertSame($editor, $entity->getUpdatedBy()); + } + + public function testApplyOnCreateIgnoresNonTimestampableEntities(): void + { + $subscriber = new TimestampableBlamableSubscriber($this->providerReturning(null)); + + // Must not throw. + $subscriber->applyOnCreate(new stdClass()); + $this->addToAssertionCount(1); + } + + private function providerReturning(?UserInterface $user): CurrentUserProviderInterface + { + return new class($user) implements CurrentUserProviderInterface { + public function __construct(private ?UserInterface $user) {} + + public function getCurrentUser(): ?UserInterface + { + return $this->user; + } + }; + } + + private function makeUser(int $id): UserInterface + { + return new class($id) implements UserInterface { + public function __construct(private int $id) {} + + public function getId(): ?int + { + return $this->id; + } + }; + } + + private function makeEntity(): object + { + return new class implements TimestampableInterface, BlamableInterface { + use TimestampableBlamableTrait; + }; + } +}