Completed
Push — master ( 94e1e6...1341d4 )
by Alejandro
16s queued 11s
created

LocateVisitsCommand::getGeolocationDataForVisit()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0073

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 26
ccs 14
cts 15
cp 0.9333
rs 9.3888
cc 5
nc 5
nop 1
crap 5.0073
1
<?php
2
declare(strict_types=1);
3
4
namespace Shlinkio\Shlink\CLI\Command\Visit;
5
6
use Exception;
7
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
8
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
9
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
10
use Shlinkio\Shlink\CLI\Util\ExitCodes;
11
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
12
use Shlinkio\Shlink\Common\Exception\WrongIpException;
13
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
14
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
15
use Shlinkio\Shlink\Common\Util\IpAddress;
16
use Shlinkio\Shlink\Core\Entity\Visit;
17
use Shlinkio\Shlink\Core\Entity\VisitLocation;
18
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
19
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
20
use Symfony\Component\Console\Helper\ProgressBar;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use Symfony\Component\Console\Style\SymfonyStyle;
24
use Symfony\Component\Lock\Factory as Locker;
25
use Throwable;
26
27
use function sprintf;
28
29
class LocateVisitsCommand extends AbstractLockedCommand
30
{
31
    public const NAME = 'visit:locate';
32
    public const ALIASES = ['visit:process'];
33
34
    /** @var VisitServiceInterface */
35
    private $visitService;
36
    /** @var IpLocationResolverInterface */
37
    private $ipLocationResolver;
38
    /** @var GeolocationDbUpdaterInterface */
39
    private $dbUpdater;
40
41
    /** @var SymfonyStyle */
42
    private $io;
43
    /** @var ProgressBar */
44
    private $progressBar;
45
46 8
    public function __construct(
47
        VisitServiceInterface $visitService,
48
        IpLocationResolverInterface $ipLocationResolver,
49
        Locker $locker,
50
        GeolocationDbUpdaterInterface $dbUpdater
51
    ) {
52 8
        parent::__construct($locker);
53 8
        $this->visitService = $visitService;
54 8
        $this->ipLocationResolver = $ipLocationResolver;
55 8
        $this->dbUpdater = $dbUpdater;
56 8
    }
57
58
    protected function configure(): void
59 8
    {
60
        $this
61
            ->setName(self::NAME)
62 8
            ->setAliases(self::ALIASES)
63 8
            ->setDescription('Resolves visits origin locations.');
64 8
    }
65
66
    protected function lockedExecute(InputInterface $input, OutputInterface $output): int
67 8
    {
68
        $this->io = new SymfonyStyle($input, $output);
69 8
70
        try {
71 8
            $this->checkDbUpdate();
72 8
73 1
            $this->visitService->locateUnlocatedVisits(
74 1
                [$this, 'getGeolocationDataForVisit'],
75
                static function (VisitLocation $location) use ($output) {
76
                    if (!$location->isEmpty()) {
77
                        $output->writeln(
78 7
                            sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
79
                        );
80 6
                    }
81 6
                }
82
            );
83 1
84
            $this->io->success('Finished processing all IPs');
85
            return ExitCodes::EXIT_SUCCESS;
86
        } catch (Throwable $e) {
87
            $this->io->error($e->getMessage());
88 6
            if ($e instanceof Exception && $this->io->isVerbose()) {
89
                $this->getApplication()->renderException($e, $this->io);
90
            }
91 2
92 2
            return ExitCodes::EXIT_FAILURE;
93 5
        }
94 5
    }
95 5
96 4
    public function getGeolocationDataForVisit(Visit $visit): Location
97
    {
98
        if (! $visit->hasRemoteAddr()) {
99 5
            $this->io->writeln(
100
                '<comment>Ignored visit with no IP address</comment>',
101 7
                OutputInterface::VERBOSITY_VERBOSE
102
            );
103
            throw IpCannotBeLocatedException::forEmptyAddress();
104
        }
105 5
106
        $ipAddr = $visit->getRemoteAddr();
107 5
        $this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
108 2
        if ($ipAddr === IpAddress::LOCALHOST) {
109 2
            $this->io->writeln(' [<comment>Ignored localhost address</comment>]');
110 2
            throw IpCannotBeLocatedException::forLocalhost();
111
        }
112 2
113
        try {
114
            return $this->ipLocationResolver->resolveIpLocation($ipAddr);
115 3
        } catch (WrongIpException $e) {
116 3
            $this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
117 3
            if ($this->io->isVerbose()) {
118 1
                $this->getApplication()->renderException($e, $this->io);
119 1
            }
120
121
            throw IpCannotBeLocatedException::forError($e);
122
        }
123 2
    }
124 1
125 1
    private function checkDbUpdate(): void
126 1
    {
127 1
        try {
128
            $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) {
129
                $this->io->writeln(
130 1
                    sprintf('<fg=blue>%s GeoLite2 database...</>', $olderDbExists ? 'Updating' : 'Downloading')
131
                );
132
                $this->progressBar = new ProgressBar($this->io);
133
            }, function (int $total, int $downloaded) {
134 7
                $this->progressBar->setMaxSteps($total);
135
                $this->progressBar->setProgress($downloaded);
136
            });
137
138 2
            if ($this->progressBar !== null) {
139 2
                $this->progressBar->finish();
140
                $this->io->newLine();
141 2
            }
142
        } catch (GeolocationDbUpdateFailedException $e) {
143 2
            if (! $e->olderDbExists()) {
144 2
                $this->io->error('GeoLite2 database download failed. It is not possible to locate visits.');
145 7
                throw $e;
146
            }
147 5
148
            $this->io->newLine();
149 5
            $this->io->writeln(
150
                '<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>'
151 2
            );
152 2
        }
153 1
    }
154 1
155
    protected function getLockConfig(): LockedCommandConfig
156
    {
157 1
        return new LockedCommandConfig($this->getName());
158 1
    }
159
}
160