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 @@ +