From c290bed3549f550bbe683739119f34270e6b45da Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Jul 2016 09:35:46 +0200 Subject: [PATCH 1/5] Created VisitLocation entity --- module/Core/src/Entity/Visit.php | 25 +++ module/Core/src/Entity/VisitLocation.php | 267 +++++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 module/Core/src/Entity/VisitLocation.php diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index 04cfbe68..0775804c 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -40,6 +40,12 @@ class Visit extends AbstractEntity implements \JsonSerializable * @ORM\JoinColumn(name="short_url_id", referencedColumnName="id") */ protected $shortUrl; + /** + * @var VisitLocation + * @ORM\ManyToOne(targetEntity=VisitLocation::class) + * @ORM\JoinColumn(name="visit_location_id", referencedColumnName="id", nullable=true) + */ + protected $visitLocation; public function __construct() { @@ -136,6 +142,24 @@ class Visit extends AbstractEntity implements \JsonSerializable return $this; } + /** + * @return VisitLocation + */ + public function getVisitLocation() + { + return $this->visitLocation; + } + + /** + * @param VisitLocation $visitLocation + * @return $this + */ + public function setVisitLocation($visitLocation) + { + $this->visitLocation = $visitLocation; + return $this; + } + /** * Specify data which should be serialized to JSON * @link http://php.net/manual/en/jsonserializable.jsonserialize.php @@ -150,6 +174,7 @@ class Visit extends AbstractEntity implements \JsonSerializable 'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null, 'remoteAddr' => $this->remoteAddr, 'userAgent' => $this->userAgent, + 'visitLocation' => $this->visitLocation, ]; } } diff --git a/module/Core/src/Entity/VisitLocation.php b/module/Core/src/Entity/VisitLocation.php new file mode 100644 index 00000000..0ca3685c --- /dev/null +++ b/module/Core/src/Entity/VisitLocation.php @@ -0,0 +1,267 @@ +countryCode; + } + + /** + * @param string $countryCode + * @return $this + */ + public function setCountryCode($countryCode) + { + $this->countryCode = $countryCode; + return $this; + } + + /** + * @return string + */ + public function getCountryName() + { + return $this->countryName; + } + + /** + * @param string $countryName + * @return $this + */ + public function setCountryName($countryName) + { + $this->countryName = $countryName; + return $this; + } + + /** + * @return string + */ + public function getRegionName() + { + return $this->regionName; + } + + /** + * @param string $regionName + * @return $this + */ + public function setRegionName($regionName) + { + $this->regionName = $regionName; + return $this; + } + + /** + * @return string + */ + public function getCityName() + { + return $this->cityName; + } + + /** + * @param string $cityName + * @return $this + */ + public function setCityName($cityName) + { + $this->cityName = $cityName; + return $this; + } + + /** + * @return string + */ + public function getLatitude() + { + return $this->latitude; + } + + /** + * @param string $latitude + * @return $this + */ + public function setLatitude($latitude) + { + $this->latitude = $latitude; + return $this; + } + + /** + * @return string + */ + public function getLongitude() + { + return $this->longitude; + } + + /** + * @param string $longitude + * @return $this + */ + public function setLongitude($longitude) + { + $this->longitude = $longitude; + 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 + */ + public function getTimezone() + { + return $this->timezone; + } + + /** + * @param string $timezone + * @return $this + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone; + return $this; + } + + /** + * Exchange internal values from provided array + * + * @param array $array + * @return void + */ + public function exchangeArray(array $array) + { + if (array_key_exists('countryCode', $array)) { + $this->setCountryCode($array['countryCode']); + } + if (array_key_exists('countryName', $array)) { + $this->setCountryName($array['countryName']); + } + if (array_key_exists('regionName', $array)) { + $this->setRegionName($array['regionName']); + } + if (array_key_exists('cityName', $array)) { + $this->setCityName($array['cityName']); + } + if (array_key_exists('latitude', $array)) { + $this->setLatitude($array['latitude']); + } + 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']); + } + } + + /** + * Return an array representation of the object + * + * @return array + */ + public function getArrayCopy() + { + return [ + 'countryCode' => $this->countryCode, + 'countryName' => $this->countryName, + 'regionName' => $this->regionName, + 'cityName' => $this->cityName, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + 'areaCode' => $this->areaCode, + 'timezone' => $this->timezone, + ]; + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + public function jsonSerialize() + { + return $this->getArrayCopy(); + } +} From 06fa33877bc53f898cbe23d050086d3f30273b65 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 20 Jul 2016 10:13:53 +0200 Subject: [PATCH 2/5] Moved some exceptions from core to common --- module/Common/config/services.config.php | 2 ++ module/Common/src/Exception/ExceptionInterface.php | 6 ++++++ .../src/Exception/InvalidArgumentException.php | 2 +- module/{Core => Common}/src/Exception/RuntimeException.php | 2 +- module/Core/src/Exception/ExceptionInterface.php | 6 ------ module/Core/src/Exception/InvalidShortCodeException.php | 2 ++ module/Core/src/Exception/InvalidUrlException.php | 2 ++ module/Core/src/Service/UrlShortener.php | 2 +- module/Core/src/Service/UrlShortenerInterface.php | 2 +- module/Core/src/Service/VisitsTracker.php | 2 +- module/Rest/src/Action/GetVisitsMiddleware.php | 2 +- module/Rest/src/Exception/AuthenticationException.php | 2 +- .../Rest/src/Middleware/CheckAuthenticationMiddleware.php | 2 +- module/Rest/src/Service/RestTokenService.php | 2 +- module/Rest/src/Service/RestTokenServiceInterface.php | 2 +- module/Rest/src/Util/RestUtils.php | 3 ++- 16 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 module/Common/src/Exception/ExceptionInterface.php rename module/{Core => Common}/src/Exception/InvalidArgumentException.php (70%) rename module/{Core => Common}/src/Exception/RuntimeException.php (67%) delete mode 100644 module/Core/src/Exception/ExceptionInterface.php diff --git a/module/Common/config/services.config.php b/module/Common/config/services.config.php index b3c4e14d..838b7896 100644 --- a/module/Common/config/services.config.php +++ b/module/Common/config/services.config.php @@ -4,6 +4,7 @@ use Doctrine\Common\Cache\Cache; use Doctrine\ORM\EntityManager; use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; +use Shlinkio\Shlink\Common\Service\IpLocationResolver; use Zend\ServiceManager\Factory\InvokableFactory; return [ @@ -13,6 +14,7 @@ return [ EntityManager::class => EntityManagerFactory::class, GuzzleHttp\Client::class => InvokableFactory::class, Cache::class => CacheFactory::class, + IpLocationResolver::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/module/Common/src/Exception/ExceptionInterface.php b/module/Common/src/Exception/ExceptionInterface.php new file mode 100644 index 00000000..81255f6a --- /dev/null +++ b/module/Common/src/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ + Date: Wed, 20 Jul 2016 12:37:48 +0200 Subject: [PATCH 3/5] Created service to resolve IP locations --- .../Common/src/Exception/WrongIpException.php | 10 +++++ .../Common/src/Service/IpLocationResolver.php | 42 +++++++++++++++++++ .../Service/IpLocationResolverInterface.php | 11 +++++ 3 files changed, 63 insertions(+) create mode 100644 module/Common/src/Exception/WrongIpException.php create mode 100644 module/Common/src/Service/IpLocationResolver.php create mode 100644 module/Common/src/Service/IpLocationResolverInterface.php diff --git a/module/Common/src/Exception/WrongIpException.php b/module/Common/src/Exception/WrongIpException.php new file mode 100644 index 00000000..8aa3a7bf --- /dev/null +++ b/module/Common/src/Exception/WrongIpException.php @@ -0,0 +1,10 @@ +httpClient = $httpClient; + } + + /** + * @param $ipAddress + * @return array + */ + public function resolveIpLocation($ipAddress) + { + try { + $response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress)); + return json_decode($response->getBody(), true); + } catch (GuzzleException $e) { + throw WrongIpException::fromIpAddress($ipAddress, $e); + } + } +} diff --git a/module/Common/src/Service/IpLocationResolverInterface.php b/module/Common/src/Service/IpLocationResolverInterface.php new file mode 100644 index 00000000..350c2b9c --- /dev/null +++ b/module/Common/src/Service/IpLocationResolverInterface.php @@ -0,0 +1,11 @@ + Date: Wed, 20 Jul 2016 19:00:23 +0200 Subject: [PATCH 4/5] Created services and command to process visits --- module/CLI/config/cli.config.php | 1 + module/CLI/config/services.config.php | 1 + .../CLI/src/Command/ProcessVisitsCommand.php | 74 +++++++++++++++++++ module/Core/config/services.config.php | 1 + module/Core/src/Entity/Visit.php | 4 +- module/Core/src/Entity/VisitLocation.php | 61 +++++---------- .../Core/src/Repository/VisitRepository.php | 19 +++++ .../Repository/VisitRepositoryInterface.php | 13 ++++ module/Core/src/Service/VisitService.php | 45 +++++++++++ .../src/Service/VisitServiceInterface.php | 17 +++++ 10 files changed, 190 insertions(+), 46 deletions(-) create mode 100644 module/CLI/src/Command/ProcessVisitsCommand.php create mode 100644 module/Core/src/Repository/VisitRepository.php create mode 100644 module/Core/src/Repository/VisitRepositoryInterface.php create mode 100644 module/Core/src/Service/VisitService.php create mode 100644 module/Core/src/Service/VisitServiceInterface.php 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 @@ + Date: Wed, 20 Jul 2016 19:04:38 +0200 Subject: [PATCH 5/5] Fixed wrong exception name --- module/Core/test/Service/UrlShortenerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index f67086b5..8298a8cc 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -65,7 +65,7 @@ class UrlShortenerTest extends TestCase /** * @test - * @expectedException \Shlinkio\Shlink\Core\Exception\RuntimeException + * @expectedException \Shlinkio\Shlink\Common\Exception\RuntimeException */ public function exceptionIsThrownWhenOrmThrowsException() {