Completed
Pull Request — master (#402)
by Alejandro
05:51
created

LocateVisitsCommand   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 126
Duplicated Lines 0 %

Test Coverage

Coverage 95.08%

Importance

Changes 0
Metric Value
wmc 15
eloc 69
dl 0
loc 126
rs 10
c 0
b 0
f 0
ccs 58
cts 61
cp 0.9508

5 Methods

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