Completed
Push — master ( c70077...3d32a9 )
by Alejandro
16s queued 10s
created

ProcessVisitsCommand   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 87
Duplicated Lines 0 %

Test Coverage

Coverage 95.24%

Importance

Changes 0
Metric Value
wmc 10
eloc 46
dl 0
loc 87
rs 10
c 0
b 0
f 0
ccs 40
cts 42
cp 0.9524

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A configure() 0 5 1
A execute() 0 27 3
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\Util\ExitCodes;
7
use Shlinkio\Shlink\Common\Exception\WrongIpException;
8
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
9
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
10
use Shlinkio\Shlink\Common\Util\IpAddress;
11
use Shlinkio\Shlink\Core\Entity\Visit;
12
use Shlinkio\Shlink\Core\Entity\VisitLocation;
13
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
14
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Console\Style\SymfonyStyle;
19
use Symfony\Component\Lock\Factory as Locker;
20
use function sprintf;
21
22
class ProcessVisitsCommand extends Command
23
{
24
    public const NAME = 'visit:process';
25
26
    /** @var VisitServiceInterface */
27
    private $visitService;
28
    /** @var IpLocationResolverInterface */
29
    private $ipLocationResolver;
30
    /** @var Locker */
31
    private $locker;
32
    /** @var OutputInterface */
33
    private $output;
34
35 6
    public function __construct(
36
        VisitServiceInterface $visitService,
37
        IpLocationResolverInterface $ipLocationResolver,
38
        Locker $locker
39
    ) {
40 6
        parent::__construct();
41 6
        $this->visitService = $visitService;
42 6
        $this->ipLocationResolver = $ipLocationResolver;
43 6
        $this->locker = $locker;
44
    }
45
46 6
    protected function configure(): void
47
    {
48
        $this
49 6
            ->setName(self::NAME)
50 6
            ->setDescription('Processes visits where location is not set yet');
51
    }
52
53 6
    protected function execute(InputInterface $input, OutputInterface $output): ?int
54
    {
55 6
        $this->output = $output;
56 6
        $io = new SymfonyStyle($input, $output);
57
58 6
        $lock = $this->locker->createLock(self::NAME);
59 6
        if (! $lock->acquire()) {
60 1
            $io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
61 1
            return ExitCodes::EXIT_WARNING;
62
        }
63
64
        try {
65 5
            $this->visitService->locateUnlocatedVisits(
66 5
                [$this, 'getGeolocationDataForVisit'],
67
                function (VisitLocation $location) use ($output) {
68 1
                    if (! $location->isEmpty()) {
69
                        $output->writeln(
70
                            sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
71
                        );
72
                    }
73 5
                }
74
            );
75
76 1
            $io->success('Finished processing all IPs');
77 1
        } finally {
78 5
            $lock->release();
79 5
            return ExitCodes::EXIT_SUCCESS;
80
        }
81
    }
82
83 5
    public function getGeolocationDataForVisit(Visit $visit): Location
84
    {
85 5
        if (! $visit->hasRemoteAddr()) {
86 2
            $this->output->writeln(
87 2
                '<comment>Ignored visit with no IP address</comment>',
88 2
                OutputInterface::VERBOSITY_VERBOSE
89
            );
90 2
            throw IpCannotBeLocatedException::forEmptyAddress();
91
        }
92
93 3
        $ipAddr = $visit->getRemoteAddr();
94 3
        $this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
95 3
        if ($ipAddr === IpAddress::LOCALHOST) {
96 1
            $this->output->writeln(' [<comment>Ignored localhost address</comment>]');
97 1
            throw IpCannotBeLocatedException::forLocalhost();
98
        }
99
100
        try {
101 2
            return $this->ipLocationResolver->resolveIpLocation($ipAddr);
102 1
        } catch (WrongIpException $e) {
103 1
            $this->output->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
104 1
            if ($this->output->isVerbose()) {
105 1
                $this->getApplication()->renderException($e, $this->output);
106
            }
107
108 1
            throw IpCannotBeLocatedException::forError($e);
109
        }
110
    }
111
}
112