Files
points-of-interest/server/src/Service/SignalSubmissionService.php
T
2025-10-10 16:30:14 +02:00

93 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Signal;
use App\Exception\SubmissionRateLimitedException;
use App\Message\SignalCreatedMessage;
use App\Payload\SignalPayload;
use App\Repository\SignalRepository;
use DateTimeImmutable;
use DateTimeZone;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
final class SignalSubmissionService
{
public function __construct(
private readonly SignalRepository $signals,
#[Autowire(service: 'limiter.signal_submission')] private readonly RateLimiterFactory $submissionLimiter,
private readonly PointProximityValidator $proximityValidator,
private readonly MessageBusInterface $bus,
#[Autowire(service: 'monolog.logger.signals')] private readonly LoggerInterface $logger,
) {
}
public function submit(string $clientKey, SignalPayload $payload): Signal
{
$this->enforceRateLimit($clientKey);
$signalLocation = $payload->signalLocation;
$userLocation = $payload->userLocation;
$this->proximityValidator->assertWithinRange($userLocation, $signalLocation);
$this->logger->info('Storing new signal submission.', [
'client_key' => $clientKey,
'signal_location' => [
'lat' => round($signalLocation->getLat(), 5),
'lng' => round($signalLocation->getLng(), 5),
],
'user_location' => [
'lat' => round($userLocation->getLat(), 5),
'lng' => round($userLocation->getLng(), 5),
],
]);
$signal = (new Signal())
->setUserKey($clientKey)
->setUserLocation($userLocation)
->setSignalLocation($signalLocation)
->setCreatedAt(new DateTimeImmutable('now', new DateTimeZone('UTC')));
$this->signals->save($signal);
$signalId = $signal->getId();
if ($signalId !== null) {
$this->logger->debug('Dispatching signal created message.', [
'signal_id' => $signalId,
]);
$this->bus->dispatch(new SignalCreatedMessage($signalId));
$this->logger->info('Signal stored successfully.', [
'signal_id' => $signalId,
'client_key' => $clientKey,
]);
}
return $signal;
}
private function enforceRateLimit(string $clientKey): void
{
$rateLimiter = $this->submissionLimiter->create($clientKey);
$limit = $rateLimiter->consume(1);
if (! $limit->isAccepted()) {
$retryAfter = $limit->getRetryAfter();
$this->logger->warning('Signal submission rejected due to rate limiting.', [
'client_key' => $clientKey,
'retry_after' => $retryAfter->format(DATE_ATOM),
]);
throw new SubmissionRateLimitedException($limit->getRetryAfter());
}
$this->logger->debug('Signal submission passed rate limiting.', [
'client_key' => $clientKey,
'remaining_tokens' => $limit->getRemainingTokens(),
]);
}
}