Extracted logic to geolocate a visit, handling possible domain errors

This commit is contained in:
Alejandro Celaya
2022-09-18 18:44:01 +02:00
parent fe41e9d573
commit 83b7d5a5f1
11 changed files with 214 additions and 31 deletions

View File

@@ -4,22 +4,20 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\EventDispatcher;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class LocateUnlocatedVisits implements VisitGeolocationHelperInterface
{
public function __construct(
private readonly VisitLocatorInterface $locator,
private readonly IpLocationResolverInterface $ipLocationResolver,
private readonly VisitToLocationHelperInterface $visitToLocation,
) {
}
@@ -33,25 +31,10 @@ class LocateUnlocatedVisits implements VisitGeolocationHelperInterface
*/
public function geolocateVisit(Visit $visit): Location
{
// TODO This method duplicates code from LocateVisitsCommand. Move to a common place.
if (! $visit->hasRemoteAddr()) {
throw IpCannotBeLocatedException::forEmptyAddress();
}
$ipAddr = $visit->getRemoteAddr() ?? '';
if ($ipAddr === IpAddress::LOCALHOST) {
throw IpCannotBeLocatedException::forLocalhost();
}
try {
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
} catch (WrongIpException $e) {
throw IpCannotBeLocatedException::forError($e);
}
return $this->visitToLocation->resolveVisitLocation($visit);
}
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
{
// Do nothing
}
}

View File

@@ -4,35 +4,40 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
use Throwable;
class IpCannotBeLocatedException extends RuntimeException
{
private bool $isNonLocatableAddress = true;
private function __construct(
string $message,
public readonly UnlocatableIpType $type,
int $code = 0,
?Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
public static function forEmptyAddress(): self
{
return new self('Ignored visit with no IP address');
return new self('Ignored visit with no IP address', UnlocatableIpType::EMPTY_ADDRESS);
}
public static function forLocalhost(): self
{
return new self('Ignored localhost address');
return new self('Ignored localhost address', UnlocatableIpType::LOCALHOST);
}
public static function forError(Throwable $e): self
{
$e = new self('An error occurred while locating IP', $e->getCode(), $e);
$e->isNonLocatableAddress = false;
return $e;
return new self('An error occurred while locating IP', UnlocatableIpType::ERROR, $e->getCode(), $e);
}
/**
* Tells if this error belongs to an address that will never be possible locate
* Tells if this belongs to an address that will never be possible to locate
*/
public function isNonLocatableAddress(): bool
{
return $this->isNonLocatableAddress;
return $this->type !== UnlocatableIpType::ERROR;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\Core\Visit\Model;
enum UnlocatableIpType
{
case EMPTY_ADDRESS;
case LOCALHOST;
case ERROR;
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class VisitToLocationHelper implements VisitToLocationHelperInterface
{
public function __construct(private readonly IpLocationResolverInterface $ipLocationResolver)
{
}
/**
* @throws IpCannotBeLocatedException
*/
public function resolveVisitLocation(Visit $visit): Location
{
if (! $visit->hasRemoteAddr()) {
throw IpCannotBeLocatedException::forEmptyAddress();
}
$ipAddr = $visit->getRemoteAddr() ?? '';
if ($ipAddr === IpAddress::LOCALHOST) {
throw IpCannotBeLocatedException::forLocalhost();
}
try {
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
} catch (WrongIpException $e) {
throw IpCannotBeLocatedException::forError($e);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
interface VisitToLocationHelperInterface
{
/**
* @throws IpCannotBeLocatedException
*/
public function resolveVisitLocation(Visit $visit): Location;
}