-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add History feature with entity Trait/Interface and dedicated History…
…Logger service
- Loading branch information
1 parent
695771c
commit 09e1f93
Showing
13 changed files
with
534 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Nécéssaire pour faire marcher le make cc sinon les dépendances aux services security ne sont pas chargés | ||
security: | ||
firewalls: | ||
dev: | ||
security: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?php | ||
|
||
namespace Smart\CoreBundle\Entity\Log; | ||
|
||
/** | ||
* @author Mathieu Ducrot <[email protected]> | ||
*/ | ||
interface HistorizableInterface | ||
{ | ||
public function getId(): ?int; | ||
|
||
public function getHistory(): ?array; | ||
|
||
public function setHistory(?array $history): self; | ||
|
||
public function addHistory(array $history): self; | ||
|
||
/** | ||
* Permet d'activer ou non l'historique sur les events doctrine prePersist/preUpdate pour les diffs | ||
*/ | ||
public function isDoctrineListenerEnable(): bool; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
namespace Smart\CoreBundle\Entity\Log; | ||
|
||
/** | ||
* Permet d'avoir un affichage spécifique sur les transitions de status dans l'historique | ||
* @author Mathieu Ducrot <[email protected]> | ||
*/ | ||
interface HistorizableStatusInterface | ||
{ | ||
public function getStatus(): mixed; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
namespace Smart\CoreBundle\Entity\Log; | ||
|
||
use Doctrine\DBAL\Types\Types; | ||
use Doctrine\ORM\Mapping as ORM; | ||
|
||
/** | ||
* @author Mathieu Ducrot <[email protected]> | ||
*/ | ||
trait HistorizableTrait | ||
{ | ||
#[ORM\Column(type: Types::JSON, nullable: true)] | ||
protected ?array $history = null; | ||
|
||
public function getHistory(): ?array | ||
{ | ||
return $this->history; | ||
} | ||
|
||
public function setHistory(?array $history): self | ||
{ | ||
$this->history = $history; | ||
|
||
return $this; | ||
} | ||
|
||
public function addHistory(array $history): self | ||
{ | ||
if ($this->history == null) { | ||
$this->history = []; | ||
} | ||
array_unshift($this->history, $history); | ||
|
||
return $this; | ||
} | ||
|
||
public function isDoctrineListenerEnable(): bool | ||
{ | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?php | ||
|
||
namespace Smart\CoreBundle\Entity\User; | ||
|
||
interface UserProfileInterface extends \Stringable | ||
{ | ||
public function getProfile(): string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
<?php | ||
|
||
namespace Smart\CoreBundle\EventListener; | ||
|
||
use Doctrine\ORM\Event\PreUpdateEventArgs; | ||
use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
use Smart\CoreBundle\Entity\Log\HistorizableInterface; | ||
use Smart\CoreBundle\Entity\Log\HistorizableStatusInterface; | ||
use Smart\CoreBundle\Logger\HistoryLogger; | ||
|
||
/** | ||
* @author Mathieu Ducrot <[email protected]> | ||
*/ | ||
class HistoryDoctrineListener | ||
{ | ||
private bool $enabled = true; | ||
private string $prePersistCode = HistoryLogger::CREATED_CODE; | ||
private ?array $historyExtraData = null; | ||
|
||
public function __construct(private HistoryLogger $historyLogger) | ||
{ | ||
} | ||
|
||
// @param typé avec LifecycleEventArgs sinon le load des fixtures ne fonctionne pas | ||
// devrait pouvoir passer à Doctrine\ORM\Event\PrePersistEventArgs après update de doctrine/orm 2.14 | ||
public function prePersist(LifecycleEventArgs $args): void | ||
{ | ||
$this->handleHistory($args, $this->prePersistCode); | ||
} | ||
|
||
public function preUpdate(PreUpdateEventArgs $args): void | ||
{ | ||
$this->handleHistory($args, HistoryLogger::UPDATED_CODE); | ||
} | ||
|
||
private function handleHistory(LifecycleEventArgs $args, string $code): void | ||
{ | ||
$entity = $args->getObject(); | ||
if (!$entity instanceof HistorizableInterface || !$this->enabled || !$entity->isDoctrineListenerEnable()) { | ||
return; | ||
} | ||
|
||
$historyData = []; | ||
if ($code === HistoryLogger::UPDATED_CODE) { | ||
/** @var PreUpdateEventArgs $args */ | ||
$entityData = $args->getEntityChangeSet(); | ||
|
||
// Si update avec uniquement history on skip pour ne pas créer de doublon d'history (cas email) | ||
if (count($entityData) === 1 && isset($entityData['history'])) { | ||
return; | ||
} | ||
|
||
$statusDiff = null; | ||
$isHistorizableStatus = $entity instanceof HistorizableStatusInterface; | ||
foreach ($entityData as $field => $change) { | ||
if ($field === 'history' || $field === 'updatedAt' || $field === 'updatedAtMonth' || $field === 'updatedAtYear') { | ||
unset($entityData[$field]); | ||
continue; | ||
} | ||
if ($field === 'password') { | ||
$changes = [ | ||
'f' => '**********', | ||
't' => '**********', | ||
]; | ||
} else { | ||
$changes = [ | ||
'f' => $this->serializeDiffValue($change[0]), | ||
't' => $this->serializeDiffValue($change[1]), | ||
]; | ||
} | ||
if ($isHistorizableStatus && $field === HistoryLogger::STATUS_PROPERTY) { | ||
$statusDiff = $changes; | ||
unset($entityData[$field]); | ||
continue; | ||
} | ||
$entityData[$field] = $changes; | ||
} | ||
if ($isHistorizableStatus) { | ||
$historyData[HistoryLogger::STATUS_PROPERTY] = $statusDiff; | ||
} | ||
|
||
// MDT diff des collections ManyToMany, actuellement pas possible d'avoir l'état de la collection avant preUpdate | ||
$uow = $args->getObjectManager()->getUnitOfWork(); | ||
$entityClass = get_class($entity); | ||
foreach ($uow->getScheduledCollectionUpdates() as $collection) { | ||
if ($entityClass !== get_class($collection->getOwner())) { | ||
continue; | ||
} | ||
// MDT c_u clé raccourci pour collection_update | ||
$entityData[$collection->getMapping()['fieldName']]['c_u'] = $collection->map(function ($item) { | ||
return $this->serializeDiffValue($item); | ||
})->toArray(); | ||
} | ||
// MDT l'event preUpdate n'est pas trigger si la seul modif de l'event concerne la suppression de collection | ||
// cf. https://github.com/doctrine/orm/issues/9960 | ||
foreach ($uow->getScheduledCollectionDeletions() as $collection) { | ||
$entityData[$collection->getMapping()['fieldName']] = 'label.empty'; | ||
} | ||
$historyData[HistoryLogger::DIFF_PROPERTY] = $entityData; | ||
} | ||
if (isset($historyData[HistoryLogger::DIFF_PROPERTY]['archivedAt'])) { | ||
$code = HistoryLogger::ARCHIVED_CODE; | ||
unset($historyData[HistoryLogger::DIFF_PROPERTY]['archivedAt']); | ||
} | ||
|
||
if ($this->historyExtraData !== null) { | ||
$historyData = array_merge($historyData, $this->historyExtraData); | ||
} | ||
|
||
// Pas besoin de setFlushLog car il a déjà lieu après l'event doctrine prePersist/preUpdate | ||
$this->historyLogger->log($entity, $code, $historyData); | ||
} | ||
|
||
private function serializeDiffValue(mixed $value): mixed | ||
{ | ||
if ($value instanceof \DateTimeInterface) { | ||
if ($value->format('d/m/Y') === '01/01/1970') { | ||
return $value->format('H\hi'); | ||
} | ||
|
||
return $value->format('Y-m-d\TH:i:sP'); | ||
} elseif ($value instanceof \Stringable) { | ||
return (string) $value; | ||
} | ||
|
||
return $value; | ||
} | ||
|
||
public function disable(): void | ||
{ | ||
$this->enabled = false; | ||
} | ||
|
||
public function setPrePersistCode(string $prePersistCode): void | ||
{ | ||
$this->prePersistCode = $prePersistCode; | ||
} | ||
|
||
public function addHistoryExtraData(?array $historyExtraData): void | ||
{ | ||
$this->historyExtraData = $historyExtraData; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
namespace Smart\CoreBundle\EventListener; | ||
|
||
use Smart\CoreBundle\Entity\User\UserProfileInterface; | ||
use Smart\CoreBundle\Logger\HistoryLogger; | ||
use Smart\CoreBundle\Utils\RequestUtils; | ||
use Sonata\AdminBundle\Controller\CRUDController; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
use Symfony\Component\HttpFoundation\RequestStack; | ||
use Symfony\Component\HttpKernel\Event\ControllerEvent; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | ||
|
||
/** | ||
* Controller to init internal variables of the HistoryLogger | ||
* | ||
* @author Mathieu Ducrot <[email protected]> | ||
*/ | ||
class HistoryLoggerListener implements EventSubscriberInterface | ||
{ | ||
private string $context; | ||
|
||
public function __construct( | ||
protected RequestStack $requestStack, | ||
protected TokenStorageInterface $tokenStorage, | ||
protected HistoryLogger $historyLogger, | ||
protected string $domain, | ||
) { | ||
} | ||
|
||
public static function getSubscribedEvents(): array | ||
{ | ||
return [ | ||
KernelEvents::CONTROLLER => 'onKernelController', | ||
]; | ||
} | ||
|
||
public function onKernelController(ControllerEvent $event): void | ||
{ | ||
// Set the context base on the domain | ||
$request = $this->requestStack->getCurrentRequest(); | ||
$this->context = RequestUtils::getContextFromHost($request->getHost(), $this->domain); | ||
$this->historyLogger->setContext($this->context); | ||
|
||
// Set the origin based on the action controller | ||
if (is_array($event->getController())) { | ||
/** @var string $controllerClass */ | ||
$controllerClass = get_class($event->getController()[0]); | ||
$controllerAction = $event->getController()[1]; | ||
} else { | ||
$controllerClass = null; | ||
$controllerAction = null; | ||
} | ||
$isCrudController = str_ends_with($controllerClass, '\CRUDController'); | ||
if ($isCrudController && $controllerAction === 'createAction') { | ||
$this->historyLogger->setOrigin('h.crt_f'); | ||
} elseif ($isCrudController && $controllerAction === 'editAction') { | ||
$this->historyLogger->setOrigin('h.upd_f'); | ||
} elseif ($isCrudController && $controllerAction === 'importAction') { | ||
$this->historyLogger->setOrigin('h.imp_f'); | ||
} elseif ($isCrudController && $controllerAction === 'archiveAction') { | ||
$this->historyLogger->setOrigin('h.arc_a'); | ||
} elseif ($controllerClass !== null && str_ends_with($controllerClass, '\SecurityController') && $controllerAction === 'profile') { | ||
$this->historyLogger->setOrigin('h.prf_f'); | ||
} | ||
|
||
// Set the user | ||
$userToken = $this->tokenStorage->getToken(); | ||
$user = $userToken?->getUser(); | ||
if ($user instanceof UserProfileInterface) { | ||
$this->historyLogger->setUser((string) $user); | ||
$this->historyLogger->setUserProfile($user->getProfile()); | ||
} | ||
} | ||
} |
Oops, something went wrong.