Completed
Pull Request — master (#440)
by Alejandro
17:40 queued 14:38
created

LocateVisitsCommand::execute()   A

Complexity

Conditions 5
Paths 39

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 5.0634

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 35
ccs 19
cts 22
cp 0.8636
rs 9.2568
cc 5
nc 39
nop 2
crap 5.0634
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
    }
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 7
    protected function lockedExecute(InputInterface $input, OutputInterface $output): int
67
    {
68 7
        $this->io = new SymfonyStyle($input, $output);
69
70
        try {
71 7
            $this->checkDbUpdate();
72
73 6
            $this->visitService->locateUnlocatedVisits(
74 6
                [$this, 'getGeolocationDataForVisit'],
75
                static function (VisitLocation $location) use ($output) {
76 1
                    if (!$location->isEmpty()) {
77
                        $output->writeln(
78
                            sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
79
                        );
80
                    }
81 6
                }
82
            );
83
84 2
            $this->io->success('Finished processing all IPs');
85 2
            return ExitCodes::EXIT_SUCCESS;
86 5
        } catch (Throwable $e) {
87 5
            $this->io->error($e->getMessage());
88 5
            if ($e instanceof Exception && $this->io->isVerbose()) {
89 4
                $this->getApplication()->renderException($e, $this->io);
90
            }
91
92 5
            return ExitCodes::EXIT_FAILURE;
93
        }
94
    }
95
96 5
    public function getGeolocationDataForVisit(Visit $visit): Location
97
    {
98 5
        if (! $visit->hasRemoteAddr()) {
99 2
            $this->io->writeln(
100 2
                '<comment>Ignored visit with no IP address</comment>',
101 2
                OutputInterface::VERBOSITY_VERBOSE
102
            );
103 2
            throw IpCannotBeLocatedException::forEmptyAddress();
104
        }
105
106 3
        $ipAddr = $visit->getRemoteAddr();
107 3
        $this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
108 3
        if ($ipAddr === IpAddress::LOCALHOST) {
109 1
            $this->io->writeln(' [<comment>Ignored localhost address</comment>]');
110 1
            throw IpCannotBeLocatedException::forLocalhost();
111
        }
112
113
        try {
114 2
            return $this->ipLocationResolver->resolveIpLocation($ipAddr);
115 1
        } catch (WrongIpException $e) {
116 1
            $this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
117 1
            if ($this->io->isVerbose()) {
118 1
                $this->getApplication()->renderException($e, $this->io);
119
            }
120
121 1
            throw IpCannotBeLocatedException::forError($e);
122
        }
123
    }
124
125 7
    private function checkDbUpdate(): void
126
    {
127
        try {
128
            $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) {
129 2
                $this->io->writeln(
130 2
                    sprintf('<fg=blue>%s GeoLite2 database...</>', $olderDbExists ? 'Updating' : 'Downloading')
131
                );
132 2
                $this->progressBar = new ProgressBar($this->io);
133
            }, function (int $total, int $downloaded) {
134 2
                $this->progressBar->setMaxSteps($total);
135 2
                $this->progressBar->setProgress($downloaded);
136 7
            });
137
138 5
            if ($this->progressBar !== null) {
139
                $this->progressBar->finish();
140 5
                $this->io->newLine();
141
            }
142 2
        } catch (GeolocationDbUpdateFailedException $e) {
143 2
            if (! $e->olderDbExists()) {
144 1
                $this->io->error('GeoLite2 database download failed. It is not possible to locate visits.');
145 1
                throw $e;
146
            }
147
148 1
            $this->io->newLine();
149 1
            $this->io->writeln(
150 1
                '<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>'
151
            );
152
        }
153
    }
154
155 8
    protected function getLockConfig(): LockedCommandConfig
156
    {
157 8
        return new LockedCommandConfig($this->getName());
158
    }
159
}
160