✨ feat(analytics): Implémente le suivi des visiteurs avec enregistrement des données.
This commit is contained in:
34
migrations/Version20251016075428.php
Normal file
34
migrations/Version20251016075428.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251016075428 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE analytics_visitor_log (id SERIAL NOT NULL, visitor_id VARCHAR(36) NOT NULL, session_id VARCHAR(36) NOT NULL, ip_address VARCHAR(45) NOT NULL, user_agent TEXT NOT NULL, first_access TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, last_access TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, events_logged JSON NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('COMMENT ON COLUMN analytics_visitor_log.first_access IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN analytics_visitor_log.last_access IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('DROP TABLE analytics_visitor_log');
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Service\Analytics\AnalyticsVisitor;
|
||||
use App\Service\Analytics\VisitorLogger;
|
||||
use App\Service\Mailer\Mailer;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
@@ -24,12 +26,16 @@ class HomeController extends AbstractController
|
||||
{
|
||||
|
||||
#[Route(path: '/',name: 'app_login',methods: ['GET', 'POST'])]
|
||||
public function index(Request $request,AuthenticationUtils $authenticationUtils): Response
|
||||
public function index(AnalyticsVisitor $analyticsVisitor,Request $request,AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('artemis_dashboard');
|
||||
}
|
||||
|
||||
$analyticsVisitor->recordPageView(
|
||||
$this->generateUrl('app_login'),
|
||||
'Login Page'
|
||||
);
|
||||
$websiteTitle = "Mainframe";
|
||||
if($request->getHost() == "espace-client.siteconseil.fr") {
|
||||
$websiteTitle = "SARL SITECONSEIL";
|
||||
@@ -48,8 +54,12 @@ class HomeController extends AbstractController
|
||||
|
||||
}
|
||||
#[Route(path: '/forgot-password',name: 'app_forgotpassword',methods: ['GET', 'POST'])]
|
||||
public function forgotPassword(EventDispatcherInterface $eventDispatcher,Request $request): Response
|
||||
public function forgotPassword(AnalyticsVisitor $analyticsVisitor,EventDispatcherInterface $eventDispatcher,Request $request): Response
|
||||
{
|
||||
$analyticsVisitor->recordPageView(
|
||||
$this->generateUrl('app_login'),
|
||||
'Forgot Password'
|
||||
);
|
||||
$websiteTitle = "Mainframe";
|
||||
if($request->getHost() == "espace-client.siteconseil.fr") {
|
||||
$websiteTitle = "SARL SITECONSEIL";
|
||||
@@ -68,9 +78,12 @@ class HomeController extends AbstractController
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/forgot-password/confirm/{id}/{token}',name: 'app_forgotpassword_confirm',methods: ['GET', 'POST'])]
|
||||
public function forgotPasswordConfirm(UserPasswordHasherInterface $userPasswordHasher,EventDispatcherInterface $eventDispatcher,Request $request,string $id,string $token,EntityManagerInterface $entityManager): Response
|
||||
public function forgotPasswordConfirm(AnalyticsVisitor $analyticsVisitor, UserPasswordHasherInterface $userPasswordHasher,EventDispatcherInterface $eventDispatcher,Request $request,string $id,string $token,EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
|
||||
$analyticsVisitor->recordPageView(
|
||||
$this->generateUrl('app_login'),
|
||||
'Forgot Password Confirm'
|
||||
);
|
||||
$websiteTitle = "Mainframe";
|
||||
if($request->getHost() == "espace-client.siteconseil.fr") {
|
||||
$websiteTitle = "SARL SITECONSEIL";
|
||||
|
||||
110
src/Entity/VisitorLog.php
Normal file
110
src/Entity/VisitorLog.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\VisitorLogRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: VisitorLogRepository::class)]
|
||||
#[ORM\Table(name: 'analytics_visitor_log')]
|
||||
class VisitorLog
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36)]
|
||||
private string $visitorId;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36)]
|
||||
private string $sessionId;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 45)]
|
||||
private string $ipAddress;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private string $userAgent;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $firstAccess;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $lastAccess;
|
||||
|
||||
// Nous stockons les événements comme un JSON ou un ARRAY sérialisé
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $eventsLogged = [];
|
||||
|
||||
public function setVisitorId(string $visitorId): self { $this->visitorId = $visitorId; return $this; }
|
||||
public function setSessionId(string $sessionId): self { $this->sessionId = $sessionId; return $this; }
|
||||
public function setIpAddress(string $ipAddress): self { $this->ipAddress = $ipAddress; return $this; }
|
||||
public function setUserAgent(string $userAgent): self { $this->userAgent = $userAgent; return $this; }
|
||||
public function setFirstAccess(\DateTimeImmutable $firstAccess): self { $this->firstAccess = $firstAccess; return $this; }
|
||||
public function setLastAccess(\DateTimeImmutable $lastAccess): self { $this->lastAccess = $lastAccess; return $this; }
|
||||
public function setEventsLogged(array $eventsLogged): self { $this->eventsLogged = $eventsLogged; return $this; }
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getEventsLogged(): array
|
||||
{
|
||||
return $this->eventsLogged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getFirstAccess(): \DateTimeImmutable
|
||||
{
|
||||
return $this->firstAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIpAddress(): string
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getLastAccess(): \DateTimeImmutable
|
||||
{
|
||||
return $this->lastAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSessionId(): string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent(): string
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getVisitorId(): string
|
||||
{
|
||||
return $this->visitorId;
|
||||
}
|
||||
}
|
||||
43
src/Repository/VisitorLogRepository.php
Normal file
43
src/Repository/VisitorLogRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\VisitorLog;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<VisitorLog>
|
||||
*/
|
||||
class VisitorLogRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, VisitorLog::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return VisitorLog[] Returns an array of VisitorLog objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('v')
|
||||
// ->andWhere('v.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('v.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?VisitorLog
|
||||
// {
|
||||
// return $this->createQueryBuilder('v')
|
||||
// ->andWhere('v.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
114
src/Service/Analytics/AnalyticsVisitor.php
Normal file
114
src/Service/Analytics/AnalyticsVisitor.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Analytics;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use DateTimeImmutable;
|
||||
use DateInterval;
|
||||
|
||||
class AnalyticsVisitor
|
||||
{
|
||||
// --- Core Identification ---
|
||||
public string $visitorId;
|
||||
public string $sessionId;
|
||||
public string $ipAddress;
|
||||
public string $userAgent;
|
||||
|
||||
// --- Time and Activity Tracking ---
|
||||
private DateTimeImmutable $firstAccess;
|
||||
private DateTimeImmutable $lastAccess;
|
||||
private int $pageViews = 0;
|
||||
private array $eventsLogged = [];
|
||||
|
||||
/**
|
||||
* The RequestStack is injected by Symfony to get the current Request object.
|
||||
*/
|
||||
public function __construct(RequestStack $requestStack)
|
||||
{
|
||||
$request = $requestStack->getCurrentRequest();
|
||||
|
||||
// Use the Request object for reliable data collection
|
||||
$this->ipAddress = $request ? $this->getClientIp($request) : '0.0.0.0';
|
||||
$this->userAgent = $request ? $request->headers->get('User-Agent', 'Unknown/Bot') : 'Unknown/Bot';
|
||||
|
||||
// Session and Visitor IDs would typically come from a cookie or session.
|
||||
// For simplicity here, we generate a UUID.
|
||||
$this->visitorId = Uuid::v4()->toRfc4122();
|
||||
$this->sessionId = Uuid::v4()->toRfc4122();
|
||||
|
||||
$this->firstAccess = new DateTimeImmutable();
|
||||
$this->lastAccess = $this->firstAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* A more robust way to get the client IP using the Symfony Request object.
|
||||
* * @param \Symfony\Component\HttpFoundation\Request $request
|
||||
*/
|
||||
private function getClientIp(\Symfony\Component\HttpFoundation\Request $request): string
|
||||
{
|
||||
// getClientIp() handles X-Forwarded-For and other headers securely
|
||||
// if your Symfony trusted proxies are configured correctly.
|
||||
return $request->getClientIp() ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// --- Public Methods for Tracking ---
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Records a new page view for the visitor.
|
||||
*/
|
||||
public function recordPageView(string $url, string $title): void
|
||||
{
|
||||
$this->pageViews++;
|
||||
$this->lastAccess = new DateTimeImmutable();
|
||||
$this->eventsLogged[] = [
|
||||
'type' => 'page_view',
|
||||
'timestamp' => $this->lastAccess->format(\DateTimeInterface::ISO8601),
|
||||
'url' => $url,
|
||||
'title' => $title,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a custom event.
|
||||
*/
|
||||
public function recordEvent(string $eventName, array $details = []): void
|
||||
{
|
||||
$this->lastAccess = new DateTimeImmutable();
|
||||
$this->eventsLogged[] = [
|
||||
'type' => 'custom_event',
|
||||
'timestamp' => $this->lastAccess->format(\DateTimeInterface::ISO8601),
|
||||
'event_name' => $eventName,
|
||||
'details' => $details,
|
||||
];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// --- Data Export (for Storage) ---
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns an array representation of the visitor object for storage.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
// Calculate duration in seconds
|
||||
$duration = $this->lastAccess->getTimestamp() - $this->firstAccess->getTimestamp();
|
||||
|
||||
return [
|
||||
'visitor_id' => $this->visitorId,
|
||||
'session_id' => $this->sessionId,
|
||||
'ip_address' => $this->ipAddress,
|
||||
'user_agent' => $this->userAgent,
|
||||
'first_access' => $this->firstAccess->format(\DateTimeInterface::ISO8601),
|
||||
'last_access' => $this->lastAccess->format(\DateTimeInterface::ISO8601),
|
||||
'session_duration_seconds' => $duration,
|
||||
'page_views' => $this->pageViews,
|
||||
'events_logged' => $this->eventsLogged,
|
||||
];
|
||||
}
|
||||
}
|
||||
25
src/Service/Analytics/VisitorLogSubscriber.php
Normal file
25
src/Service/Analytics/VisitorLogSubscriber.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace App\Service\Analytics;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
#[AsEventListener(event: KernelEvents::TERMINATE,method: 'onKernelTerminate')]
|
||||
class VisitorLogSubscriber
|
||||
{
|
||||
|
||||
public function __construct(private VisitorLogger $visitorLogger)
|
||||
{}
|
||||
|
||||
public function onKernelTerminate(TerminateEvent $event): void
|
||||
{
|
||||
// Ne pas enregistrer les requêtes qui ont échoué
|
||||
if (!$event->isMainRequest() || $event->getResponse()->getStatusCode() >= 500) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Appeler le service de sauvegarde
|
||||
$this->visitorLogger->logCurrentVisitor();
|
||||
}
|
||||
}
|
||||
49
src/Service/Analytics/VisitorLogger.php
Normal file
49
src/Service/Analytics/VisitorLogger.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Analytics;
|
||||
|
||||
use App\Entity\VisitorLog;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class VisitorLogger
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private AnalyticsVisitor $visitor;
|
||||
|
||||
/**
|
||||
* L'EntityManager de Doctrine et l'objet AnalyticsVisitor sont injectés par Symfony.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $entityManager, AnalyticsVisitor $visitor)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
// On récupère l'instance du visiteur qui a collecté toutes les actions
|
||||
$this->visitor = $visitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre l'ensemble des données de l'objet AnalyticsVisitor dans la BDD.
|
||||
*/
|
||||
public function logCurrentVisitor(): void
|
||||
{
|
||||
// 1. Récupérer les données structurées
|
||||
$data = $this->visitor->toArray();
|
||||
|
||||
// 2. Créer l'entité et mapper les données
|
||||
$log = new VisitorLog();
|
||||
$log->setVisitorId($data['visitor_id']);
|
||||
$log->setSessionId($data['session_id']);
|
||||
$log->setIpAddress($data['ip_address']);
|
||||
$log->setUserAgent($data['user_agent']);
|
||||
|
||||
// Convertir les chaînes de date ISO8601 en objets DateTimeImmutable
|
||||
$log->setFirstAccess(new \DateTimeImmutable($data['first_access']));
|
||||
$log->setLastAccess(new \DateTimeImmutable($data['last_access']));
|
||||
|
||||
// Sauvegarder l'historique des événements
|
||||
$log->setEventsLogged($data['events_logged']);
|
||||
|
||||
// 3. Persister et flusher
|
||||
$this->entityManager->persist($log);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user