Completed
Push — develop ( 9d3653...f17214 )
by Alejandro
16s queued 13s
created

LocateVisitsCommand::checkDbUpdate()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 26
rs 9.3554
c 0
b 0
f 0
ccs 16
cts 17
cp 0.9412
cc 5
nc 8
nop 0
crap 5.005
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Shlinkio\Shlink\CLI\Command\Visit;
6
7
use Exception;
8
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
9
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
10
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
11
use Shlinkio\Shlink\CLI\Util\ExitCodes;
12
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
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 Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
19
use Shlinkio\Shlink\IpGeolocation\Model\Location;
20
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
21
use Symfony\Component\Console\Helper\ProgressBar;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use Symfony\Component\Lock\LockFactory;
26
use Throwable;
27
28
use function sprintf;
29
30
class LocateVisitsCommand extends AbstractLockedCommand
31
{
32
    public const NAME = 'visit:locate';
33
34
    private VisitServiceInterface $visitService;
35
    private IpLocationResolverInterface $ipLocationResolver;
36
    private GeolocationDbUpdaterInterface $dbUpdater;
37
38
    private SymfonyStyle $io;
39
    private ?ProgressBar $progressBar = null;
40
41 8
    public function __construct(
42
        VisitServiceInterface $visitService,
43
        IpLocationResolverInterface $ipLocationResolver,
44
        LockFactory $locker,
45
        GeolocationDbUpdaterInterface $dbUpdater
46
    ) {
47 8
        parent::__construct($locker);
48 8
        $this->visitService = $visitService;
49 8
        $this->ipLocationResolver = $ipLocationResolver;
50 8
        $this->dbUpdater = $dbUpdater;
51
    }
52
53 8
    protected function configure(): void
54
    {
55
        $this
56 8
            ->setName(self::NAME)
57 8
            ->setDescription('Resolves visits origin locations.');
58
    }
59
60 7
    protected function lockedExecute(InputInterface $input, OutputInterface $output): int
61
    {
62 7
        $this->io = new SymfonyStyle($input, $output);
63
64
        try {
65 7
            $this->checkDbUpdate();
66
67 6
            $this->visitService->locateUnlocatedVisits(
68 6
                [$this, 'getGeolocationDataForVisit'],
69
                static function (VisitLocation $location) use ($output) {
70 1
                    if (!$location->isEmpty()) {
71
                        $output->writeln(
72
                            sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
73
                        );
74
                    }
75 6
                }
76
            );
77
78 2
            $this->io->success('Finished processing all IPs');
79 2
            return ExitCodes::EXIT_SUCCESS;
80 5
        } catch (Throwable $e) {
81 5
            $this->io->error($e->getMessage());
82 5
            if ($e instanceof Exception && $this->io->isVerbose()) {
83 4
                $this->getApplication()->renderThrowable($e, $this->io);
84
            }
85
86 5
            return ExitCodes::EXIT_FAILURE;
87
        }
88
    }
89
90 5
    public function getGeolocationDataForVisit(Visit $visit): Location
91
    {
92 5
        if (! $visit->hasRemoteAddr()) {
93 2
            $this->io->writeln(
94 2
                '<comment>Ignored visit with no IP address</comment>',
95 2
                OutputInterface::VERBOSITY_VERBOSE
96
            );
97 2
            throw IpCannotBeLocatedException::forEmptyAddress();
98
        }
99
100 3
        $ipAddr = $visit->getRemoteAddr();
101 3
        $this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
102 3
        if ($ipAddr === IpAddress::LOCALHOST) {
103 1
            $this->io->writeln(' [<comment>Ignored localhost address</comment>]');
104 1
            throw IpCannotBeLocatedException::forLocalhost();
105
        }
106
107
        try {
108 2
            return $this->ipLocationResolver->resolveIpLocation($ipAddr);
109 1
        } catch (WrongIpException $e) {
110 1
            $this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
111 1
            if ($this->io->isVerbose()) {
112 1
                $this->getApplication()->renderThrowable($e, $this->io);
113
            }
114
115 1
            throw IpCannotBeLocatedException::forError($e);
116
        }
117
    }
118
119 7
    private function checkDbUpdate(): void
120
    {
121
        try {
122
            $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) {
123 2
                $this->io->writeln(
124 2
                    sprintf('<fg=blue>%s GeoLite2 database...</>', $olderDbExists ? 'Updating' : 'Downloading')
125
                );
126 2
                $this->progressBar = new ProgressBar($this->io);
127
            }, function (int $total, int $downloaded) {
128 2
                $this->progressBar->setMaxSteps($total);
0 ignored issues
show
Bug introduced by
The method setMaxSteps() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
                $this->progressBar->/** @scrutinizer ignore-call */ 
129
                                    setMaxSteps($total);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
129 2
                $this->progressBar->setProgress($downloaded);
130 7
            });
131
132 5
            if ($this->progressBar !== null) {
133
                $this->progressBar->finish();
134 5
                $this->io->newLine();
135
            }
136 2
        } catch (GeolocationDbUpdateFailedException $e) {
137 2
            if (! $e->olderDbExists()) {
138 1
                $this->io->error('GeoLite2 database download failed. It is not possible to locate visits.');
139 1
                throw $e;
140
            }
141
142 1
            $this->io->newLine();
143 1
            $this->io->writeln(
144 1
                '<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>'
145
            );
146
        }
147
    }
148
149 8
    protected function getLockConfig(): LockedCommandConfig
150
    {
151 8
        return new LockedCommandConfig($this->getName());
0 ignored issues
show
Bug introduced by
It seems like $this->getName() can also be of type null; however, parameter $lockName of Shlinkio\Shlink\CLI\Comm...ndConfig::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

151
        return new LockedCommandConfig(/** @scrutinizer ignore-type */ $this->getName());
Loading history...
152
    }
153
}
154