Refactor point value object and add observability

This commit is contained in:
Bernard Ngandu
2025-10-10 14:55:36 +02:00
parent 8a43d3967c
commit 68eb54995f
46 changed files with 3691 additions and 229 deletions
+25 -84
View File
@@ -4,14 +4,13 @@ declare(strict_types=1);
namespace App\Controller;
use App\Dto\SignalPayload;
use App\Entity\Signal;
use App\Repository\SignalRepository;
use App\Service\SignalSnapshotBuilder;
use DateTimeImmutable;
use DateTimeZone;
use App\Payload\SignalPayload;
use App\Service\ClientKeyProvider;
use App\Service\SignalSnapshotService;
use App\Service\SignalSubmissionService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Annotation\Route;
@@ -20,104 +19,46 @@ use Symfony\Component\Routing\Annotation\Route;
class SignalController
{
public function __construct(
private readonly SignalRepository $signals,
private readonly SignalSnapshotBuilder $snapshotBuilder,
private readonly SignalSnapshotService $snapshotService,
private readonly SignalSubmissionService $submissionService,
private readonly ClientKeyProvider $clientKeyProvider,
) {
}
#[Route(path: '', name: 'api_signals_index', methods: ['GET'])]
public function index(Request $request, #[MapQueryParameter('limit', flags: FILTER_NULL_ON_FAILURE)] ?int $limit = null): JsonResponse
{
$limit = $this->normalizeLimit($limit);
$clientKey = $this->clientKeyProvider->fromRequest($request);
$snapshot = $this->snapshotService->buildSnapshot($limit, $clientKey);
$clientKey = $this->hashIp($this->extractClientIp($request));
$signals = $this->signals->findRecent($limit);
$snapshot = $this->snapshotBuilder->build($signals);
$payload = [
'clientKey' => $clientKey,
'points' => $snapshot['points'],
'density' => $snapshot['density'],
'latestByUser' => $snapshot['latestByUser'],
'totals' => $snapshot['totals'],
'updatedAt' => new DateTimeImmutable('now', new DateTimeZone('UTC'))->format(DATE_ATOM),
];
return new JsonResponse($payload);
return new JsonResponse($snapshot);
}
#[Route(path: '', name: 'api_signals_store', methods: ['POST'])]
public function store(#[MapRequestPayload(validationFailedStatusCode: JsonResponse::HTTP_UNPROCESSABLE_ENTITY)] SignalPayload $payload, Request $request): JsonResponse
public function store(Request $request, #[MapRequestPayload] SignalPayload $payload): JsonResponse
{
$lat = $payload->lat;
$lng = $payload->lng;
$clientKey = $this->clientKeyProvider->fromRequest($request);
$signal = $this->submissionService->submit($clientKey, $payload);
if (! is_finite($lat) || ! is_finite($lng)) {
return $this->errorResponse('invalid_coordinates', 'Latitude and longitude must be numbers.', JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
}
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
return $this->errorResponse('out_of_bounds', 'Latitude or longitude out of range.', JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
}
$clientIp = $this->extractClientIp($request);
$clientKey = $this->hashIp($clientIp);
$signal = new Signal()
->setUserKey($clientKey)
->setLat($lat)
->setLng($lng)
->setCreatedAt(new DateTimeImmutable('now', new DateTimeZone('UTC')));
$this->signals->save($signal);
$signalLocation = $signal->getSignalLocation();
$userLocation = $signal->getUserLocation();
return new JsonResponse([
'status' => 'stored',
'clientKey' => $clientKey,
'point' => [
'id' => $signal->getId(),
'lat' => $signal->getLat(),
'lng' => $signal->getLng(),
'signalLocation' => [
'lat' => $signalLocation->getLat(),
'lng' => $signalLocation->getLng(),
],
'userLocation' => [
'lat' => $userLocation->getLat(),
'lng' => $userLocation->getLng(),
],
'createdAt' => $signal->getCreatedAt()->format(DATE_ATOM),
'userKey' => $signal->getUserKey(),
],
], JsonResponse::HTTP_CREATED);
}
private function errorResponse(string $error, string $message, int $statusCode): JsonResponse
{
return new JsonResponse([
'error' => $error,
'message' => $message,
], $statusCode);
}
private function normalizeLimit(?int $limit): int
{
$limit ??= 750;
if ($limit < 1 || $limit > 5000) {
return 750;
}
return $limit;
}
private function extractClientIp(Request $request): string
{
$forwarded = $request->headers->get('X-Forwarded-For');
if ($forwarded !== null && $forwarded !== '') {
$parts = array_filter(array_map('trim', explode(',', $forwarded)));
if ($parts !== []) {
return $parts[0];
}
}
return $request->getClientIp() ?? '0.0.0.0';
}
private function hashIp(string $ip): string
{
return substr(hash('sha256', $ip), 0, 12);
], Response::HTTP_CREATED);
}
}