9c311cb58b
Auto Tag Develop / tag (push) Successful in 11s
## Probleme (ERP-98) Suite PHPUnit flaky ~1 run sur 2 -> hook pre-commit qui plante, recours au `--no-verify` sur des commits sains. ## Cause racine Une seule cause commune : l'horloge `CLOCK_REALTIME` du conteneur n'est pas monotone sous WSL2/Docker (saut arriere sous charge), alors que le code et les tests supposaient une horloge stable. - **401 « Invalid JWT Token »** : lexik validait `iat`/`nbf`/`exp` avec `clock_skew: 0` (`LooseValidAt(.., PT0S)` cote lcobucci). Un recul d'horloge apres `/login_check` rend le token « dans le futur » -> rejet. - **Horodatages « meme seconde »** (`1780402904 > 1780402904`) : colonnes `TIMESTAMP(0)` + `sleep(1)` reel. L'ecart floor-seconde n'est nul que si l'horloge recule. ## Correctifs | Fichier | Modif | |---|---| | `config/packages/lexik_jwt_authentication.yaml` | `clock_skew: 15` -> tolere la derive (benefice prod aussi) | | `TimestampableBlamableSubscriber` | injection `ClockInterface` (prod inchange via NativeClock) | | `CategoryTimestampableBlamableTest` | `ClockSensitiveTrait` + MockClock fige/avance, suppression des `sleep(1)` | | `TimestampableBlamableSubscriberTest` | MockClock injecte dans les 4 instanciations | **Subtilite** : `mockTime()` cree un MockClock en UTC ; les colonnes `TIMESTAMP WITHOUT TIME ZONE` round-trippent via le fuseau PHP (Europe/Paris) -> decalage 2h. Le mock est seede dans le fuseau par defaut (comme le NativeClock prod). ## Verifications - `make test` : **464 tests verts**, 0 echec / 0 erreur - Test timestamp cible : **5/5 deterministe** (et plus rapide, sleeps reels supprimes) - `make php-cs-fixer-allow-risky` : 0 fichier a corriger - Deprecations/notices PHPUnit preexistantes (hors perimetre) Pas de migration, pas de changement front, RBAC intact. --------- Co-authored-by: admin malio <malio@yuno.malio.fr> Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #47 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
73 lines
2.4 KiB
PHP
73 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Shared\Infrastructure\Doctrine;
|
|
|
|
use App\Shared\Domain\Contract\BlamableInterface;
|
|
use App\Shared\Domain\Contract\TimestampableInterface;
|
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
|
use Doctrine\ORM\Event\PrePersistEventArgs;
|
|
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
|
use Doctrine\ORM\Events;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\Clock\ClockInterface;
|
|
use Symfony\Component\Security\Core\User\UserInterface;
|
|
|
|
/**
|
|
* Listener Doctrine global qui remplit automatiquement les colonnes
|
|
* Timestampable + Blamable.
|
|
*
|
|
* Pattern aligne sur AuditListener (cf.
|
|
* src/Module/Core/Infrastructure/Doctrine/AuditListener.php) : declare via
|
|
* #[AsDoctrineListener], auto-wire par le DoctrineBundle.
|
|
*
|
|
* Regle Blamable : si aucun utilisateur n'est authentifie (CLI, cron,
|
|
* migration), les FK `created_by` / `updated_by` restent a null. L'affichage
|
|
* front gere le libelle « Systeme » pour null.
|
|
*/
|
|
#[AsDoctrineListener(event: Events::prePersist)]
|
|
#[AsDoctrineListener(event: Events::preUpdate)]
|
|
final class TimestampableBlamableSubscriber
|
|
{
|
|
// L'horloge est injectee (et non un `new DateTimeImmutable()` direct) pour
|
|
// que les tests puissent figer/avancer le temps de facon deterministe via
|
|
// ClockSensitiveTrait (cf. ERP-98). En prod, le service `clock` delegue a
|
|
// l'horloge systeme reelle.
|
|
public function __construct(
|
|
private readonly Security $security,
|
|
private readonly ClockInterface $clock,
|
|
) {}
|
|
|
|
public function prePersist(PrePersistEventArgs $args): void
|
|
{
|
|
$entity = $args->getObject();
|
|
$now = $this->clock->now();
|
|
$user = $this->security->getUser();
|
|
|
|
if ($entity instanceof TimestampableInterface) {
|
|
$entity->setCreatedAt($now);
|
|
$entity->setUpdatedAt($now);
|
|
}
|
|
|
|
if ($entity instanceof BlamableInterface && $user instanceof UserInterface) {
|
|
$entity->setCreatedBy($user);
|
|
$entity->setUpdatedBy($user);
|
|
}
|
|
}
|
|
|
|
public function preUpdate(PreUpdateEventArgs $args): void
|
|
{
|
|
$entity = $args->getObject();
|
|
$user = $this->security->getUser();
|
|
|
|
if ($entity instanceof TimestampableInterface) {
|
|
$entity->setUpdatedAt($this->clock->now());
|
|
}
|
|
|
|
if ($entity instanceof BlamableInterface && $user instanceof UserInterface) {
|
|
$entity->setUpdatedBy($user);
|
|
}
|
|
}
|
|
}
|