feat(shared) : add timestampable/blamable trait and doctrine subscriber

This commit is contained in:
Matthieu
2026-06-19 14:37:28 +02:00
parent 52399b35d9
commit 3053c09522
10 changed files with 309 additions and 1 deletions
+2 -1
View File
@@ -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]
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Shared\Application;
use App\Shared\Domain\Contract\UserInterface;
interface CurrentUserProviderInterface
{
public function getCurrentUser(): ?UserInterface;
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Contract;
interface BlamableInterface
{
public function getCreatedBy(): ?UserInterface;
public function setCreatedBy(?UserInterface $user): void;
public function getUpdatedBy(): ?UserInterface;
public function setUpdatedBy(?UserInterface $user): void;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Contract;
use DateTimeImmutable;
interface TimestampableInterface
{
public function getCreatedAt(): ?DateTimeImmutable;
public function setCreatedAt(DateTimeImmutable $createdAt): void;
public function getUpdatedAt(): ?DateTimeImmutable;
public function setUpdatedAt(DateTimeImmutable $updatedAt): void;
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Contract;
interface UserInterface
{
public function getId(): ?int;
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Shared\Domain\Trait;
use App\Shared\Domain\Contract\UserInterface;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
trait TimestampableBlamableTrait
{
#[ORM\Column(name: 'created_at', type: 'datetime_immutable', nullable: true)]
#[Groups(['timestampable:read'])]
private ?DateTimeImmutable $createdAt = null;
#[ORM\Column(name: 'updated_at', type: 'datetime_immutable', nullable: true)]
#[Groups(['timestampable:read'])]
private ?DateTimeImmutable $updatedAt = null;
#[ORM\ManyToOne(targetEntity: UserInterface::class)]
#[ORM\JoinColumn(name: 'created_by', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
#[Groups(['blamable:read'])]
private ?UserInterface $createdBy = null;
#[ORM\ManyToOne(targetEntity: UserInterface::class)]
#[ORM\JoinColumn(name: 'updated_by', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
#[Groups(['blamable:read'])]
private ?UserInterface $updatedBy = 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;
}
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;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Shared\Infrastructure\Doctrine;
use App\Shared\Application\CurrentUserProviderInterface;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use DateTimeImmutable;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
#[AsDoctrineListener(event: Events::prePersist)]
#[AsDoctrineListener(event: Events::preUpdate)]
final readonly class TimestampableBlamableSubscriber
{
public function __construct(
private CurrentUserProviderInterface $currentUserProvider,
) {}
public function prePersist(PrePersistEventArgs $args): void
{
$this->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());
}
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Shared\Infrastructure\Security;
use App\Shared\Application\CurrentUserProviderInterface;
use App\Shared\Domain\Contract\UserInterface;
use Symfony\Bundle\SecurityBundle\Security;
final readonly class SecurityCurrentUserProvider implements CurrentUserProviderInterface
{
public function __construct(
private Security $security,
) {}
public function getCurrentUser(): ?UserInterface
{
$user = $this->security->getUser();
return $user instanceof UserInterface ? $user : null;
}
}