diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php
index 6c34f19c..6a6e4122 100644
--- a/module/CLI/config/cli.config.php
+++ b/module/CLI/config/cli.config.php
@@ -9,6 +9,7 @@ return [
Command\ResolveUrlCommand::class,
Command\ListShortcodesCommand::class,
Command\GetVisitsCommand::class,
+ Command\ProcessVisitsCommand::class,
]
],
diff --git a/module/CLI/config/services.config.php b/module/CLI/config/services.config.php
index 839e92f8..5c5931a4 100644
--- a/module/CLI/config/services.config.php
+++ b/module/CLI/config/services.config.php
@@ -13,6 +13,7 @@ return [
CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class,
CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class,
CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class,
+ CLI\Command\ProcessVisitsCommand::class => AnnotatedFactory::class,
],
],
diff --git a/module/CLI/src/Command/ProcessVisitsCommand.php b/module/CLI/src/Command/ProcessVisitsCommand.php
new file mode 100644
index 00000000..19692e85
--- /dev/null
+++ b/module/CLI/src/Command/ProcessVisitsCommand.php
@@ -0,0 +1,74 @@
+visitService = $visitService;
+ $this->ipLocationResolver = $ipLocationResolver;
+ }
+
+ public function configure()
+ {
+ $this->setName('visit:process')
+ ->setDescription('Processes visits where location is not set already');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $visits = $this->visitService->getUnlocatedVisits();
+
+ foreach ($visits as $visit) {
+ $ipAddr = $visit->getRemoteAddr();
+ $output->write(sprintf('Processing IP %s', $ipAddr));
+ if ($ipAddr === self::LOCALHOST) {
+ $output->writeln(' (Ignored localhost address)');
+ continue;
+ }
+
+ try {
+ $result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
+ $location = new VisitLocation();
+ $location->exchangeArray($result);
+ $visit->setVisitLocation($location);
+ $this->visitService->saveVisit($visit);
+ $output->writeln(sprintf(' (Address located at "%s")', $location->getCityName()));
+ } catch (WrongIpException $e) {
+ continue;
+ }
+ }
+
+ $output->writeln('Finished processing all IPs');
+ }
+}
diff --git a/module/Core/config/services.config.php b/module/Core/config/services.config.php
index cab7ca41..00de6a67 100644
--- a/module/Core/config/services.config.php
+++ b/module/Core/config/services.config.php
@@ -11,6 +11,7 @@ return [
Service\UrlShortener::class => AnnotatedFactory::class,
Service\VisitsTracker::class => AnnotatedFactory::class,
Service\ShortUrlService::class => AnnotatedFactory::class,
+ Service\VisitService::class => AnnotatedFactory::class,
// Middleware
RedirectMiddleware::class => AnnotatedFactory::class,
diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php
index 0775804c..a95c61b0 100644
--- a/module/Core/src/Entity/Visit.php
+++ b/module/Core/src/Entity/Visit.php
@@ -9,7 +9,7 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
* @author
* @link
*
- * @ORM\Entity
+ * @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\VisitRepository")
* @ORM\Table(name="visits")
*/
class Visit extends AbstractEntity implements \JsonSerializable
@@ -42,7 +42,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
protected $shortUrl;
/**
* @var VisitLocation
- * @ORM\ManyToOne(targetEntity=VisitLocation::class)
+ * @ORM\ManyToOne(targetEntity=VisitLocation::class, cascade={"persist"})
* @ORM\JoinColumn(name="visit_location_id", referencedColumnName="id", nullable=true)
*/
protected $visitLocation;
diff --git a/module/Core/src/Entity/VisitLocation.php b/module/Core/src/Entity/VisitLocation.php
index 0ca3685c..3b9851ac 100644
--- a/module/Core/src/Entity/VisitLocation.php
+++ b/module/Core/src/Entity/VisitLocation.php
@@ -17,42 +17,37 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
{
/**
* @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $countryCode;
/**
* @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $countryName;
/**
* @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $regionName;
/**
* @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $cityName;
/**
* @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $latitude;
/**
* @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $longitude;
/**
* @var string
- * @ORM\Column()
- */
- protected $areaCode;
- /**
- * @var string
- * @ORM\Column()
+ * @ORM\Column(nullable=true)
*/
protected $timezone;
@@ -164,24 +159,6 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
return $this;
}
- /**
- * @return string
- */
- public function getAreaCode()
- {
- return $this->areaCode;
- }
-
- /**
- * @param string $areaCode
- * @return $this
- */
- public function setAreaCode($areaCode)
- {
- $this->areaCode = $areaCode;
- return $this;
- }
-
/**
* @return string
*/
@@ -208,17 +185,17 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
*/
public function exchangeArray(array $array)
{
- if (array_key_exists('countryCode', $array)) {
- $this->setCountryCode($array['countryCode']);
+ if (array_key_exists('country_code', $array)) {
+ $this->setCountryCode($array['country_code']);
}
- if (array_key_exists('countryName', $array)) {
- $this->setCountryName($array['countryName']);
+ if (array_key_exists('country_name', $array)) {
+ $this->setCountryName($array['country_name']);
}
- if (array_key_exists('regionName', $array)) {
- $this->setRegionName($array['regionName']);
+ if (array_key_exists('region_name', $array)) {
+ $this->setRegionName($array['region_name']);
}
- if (array_key_exists('cityName', $array)) {
- $this->setCityName($array['cityName']);
+ if (array_key_exists('city', $array)) {
+ $this->setCityName($array['city']);
}
if (array_key_exists('latitude', $array)) {
$this->setLatitude($array['latitude']);
@@ -226,11 +203,8 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
if (array_key_exists('longitude', $array)) {
$this->setLongitude($array['longitude']);
}
- if (array_key_exists('areaCode', $array)) {
- $this->setAreaCode($array['areaCode']);
- }
- if (array_key_exists('timezone', $array)) {
- $this->setTimezone($array['timezone']);
+ if (array_key_exists('time_zone', $array)) {
+ $this->setTimezone($array['time_zone']);
}
}
@@ -248,7 +222,6 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
'cityName' => $this->cityName,
'latitude' => $this->latitude,
'longitude' => $this->longitude,
- 'areaCode' => $this->areaCode,
'timezone' => $this->timezone,
];
}
diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php
new file mode 100644
index 00000000..ef7d7935
--- /dev/null
+++ b/module/Core/src/Repository/VisitRepository.php
@@ -0,0 +1,19 @@
+createQueryBuilder('v');
+ $qb->where($qb->expr()->isNull('v.visitLocation'));
+
+ return $qb->getQuery()->getResult();
+ }
+}
diff --git a/module/Core/src/Repository/VisitRepositoryInterface.php b/module/Core/src/Repository/VisitRepositoryInterface.php
new file mode 100644
index 00000000..6534d7ea
--- /dev/null
+++ b/module/Core/src/Repository/VisitRepositoryInterface.php
@@ -0,0 +1,13 @@
+em = $em;
+ }
+
+ /**
+ * @return Visit[]
+ */
+ public function getUnlocatedVisits()
+ {
+ /** @var VisitRepository $repo */
+ $repo = $this->em->getRepository(Visit::class);
+ return $repo->findUnlocatedVisits();
+ }
+
+ /**
+ * @param Visit $visit
+ */
+ public function saveVisit(Visit $visit)
+ {
+ $this->em->persist($visit);
+ $this->em->flush();
+ }
+}
diff --git a/module/Core/src/Service/VisitServiceInterface.php b/module/Core/src/Service/VisitServiceInterface.php
new file mode 100644
index 00000000..8347fdb3
--- /dev/null
+++ b/module/Core/src/Service/VisitServiceInterface.php
@@ -0,0 +1,17 @@
+