Completed
Push — master ( 0ec7e8...22630c )
by Alejandro
23s queued 10s
created

LocateVisitsCommand   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Test Coverage

Coverage 93.94%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 17
eloc 74
c 1
b 0
f 0
dl 0
loc 133
ccs 62
cts 66
cp 0.9394
rs 10

5 Methods

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