MobileCleanupCommand::tryToFix()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 1
dl 0
loc 19
ccs 0
cts 15
cp 0
crap 20
rs 9.9
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the login-cidadao project or it's bundles.
4
 *
5
 * (c) Guilherme Donato <guilhermednt on github>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LoginCidadao\CoreBundle\Command;
12
13
use Doctrine\DBAL\Types\ConversionException;
14
use Doctrine\ORM\Internal\Hydration\IterableResult;
15
use libphonenumber\PhoneNumber;
16
use libphonenumber\PhoneNumberFormat;
17
use libphonenumber\PhoneNumberUtil;
18
use LoginCidadao\CoreBundle\Entity\PersonRepository;
19
use LoginCidadao\ValidationBundle\Validator\Constraints\MobilePhoneNumberValidator;
20
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
21
use Symfony\Component\Console\Helper\ProgressIndicator;
22
use Symfony\Component\Console\Input\InputArgument;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Style\SymfonyStyle;
26
use Doctrine\ORM\EntityManager;
27
28
class MobileCleanupCommand extends ContainerAwareCommand
29
{
30
    const INVALID_RECOVERED = 'invalid_recovered';
31
    const IRRECOVERABLE = 'irrecoverable';
32
    const VALID = 'valid';
33
    const VALID_ADDED_9 = 'valid_added_9';
34
35
    const REGEX_COUNTRY = '(\+?0|\+?55)?';
36
    const REGEX_AREA_CODE = '(1[1-9]|2[12478]|3[1-5]|3[7-8]|4[1-9]|5[1345]|6[1-9]|7[134579]|8[1-9]|9[1-9])';
37
    const REGEX_SUBSCRIBER = '((?:9[6789]|[6789])\d{7})';
38
39
    /** @var PhoneNumberUtil */
40
    private $phoneUtil;
41
42
    /** @var array */
43
    private $fileHandles = [];
44
45
    /** @var array */
46
    private $counts = [];
47
48
    private $recovered = 0;
49
50
    protected function configure()
51
    {
52
        $this
53
            ->setName('lc:mobile-cleanup')
54
            ->addArgument(
55
                'file',
56
                InputArgument::REQUIRED,
57
                'File to where decisions should be logged.'
58
            )
59
            ->setDescription('Erases mobile numbers that do not comply with E.164 in any recoverable way.');
60
    }
61
62
    protected function execute(InputInterface $input, OutputInterface $output)
63
    {
64
        $file = $input->getArgument('file');
65
        $this->phoneUtil = PhoneNumberUtil::getInstance();
66
67
        $this->fileHandles['log'] = fopen($file, 'w+');
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type string[]; however, parameter $filename of fopen() 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

67
        $this->fileHandles['log'] = fopen(/** @scrutinizer ignore-type */ $file, 'w+');
Loading history...
68
69
        $this->counts = [
70
            self::INVALID_RECOVERED => 0,
71
            self::IRRECOVERABLE => 0,
72
            self::VALID => 0,
73
            self::VALID_ADDED_9 => 0,
74
        ];
75
76
        $this->processPhones($input, $output);
77
    }
78
79
    /**
80
     *
81
     * @return EntityManager
82
     */
83
    private function getManager()
84
    {
85
        /** @var EntityManager $em */
86
        $em = $this->getContainer()->get('doctrine')->getManager();
87
88
        return $em;
89
    }
90
91
    private function processPhones(InputInterface $input, OutputInterface $output)
92
    {
93
        $file = $input->getArgument('file');
94
95
        $io = new SymfonyStyle($input, $output);
96
97
        $io->title('Mobile Numbers Cleanup');
98
99
        $progress = new ProgressIndicator($output);
100
        $progress->start('Scanning phones...');
101
102
        $results = $this->getQuery()->iterate();
103
        while (true) {
104
            $progress->setMessage($this->getProgressMessage());
105
            $row = $this->getNextEntry($results);
106
            if (false === $row) {
107
                break;
108
            }
109
            if (null === $row) {
110
                continue;
111
            }
112
113
            /** @var PhoneNumber $mobile */
114
            $mobile = $row['mobile'];
115
            if (false === MobilePhoneNumberValidator::isMobile($mobile)) {
116
                $added9 = $this->tryToFix($mobile);
117
                if (false === $added9) {
118
                    $this->write(self::IRRECOVERABLE, $mobile, $row['id']);
119
                } else {
120
                    $this->write(self::VALID_ADDED_9, $mobile, $row['id'], $added9);
121
                }
122
            } else {
123
                $this->write(self::VALID, $mobile, $row['id']);
124
            }
125
        }
126
        $progress->finish($this->getProgressMessage());
127
        $io->newLine();
128
129
        foreach ($this->fileHandles as $handle) {
130
            fclose($handle);
131
        }
132
133
        $io->success("Results saved to {$file}");
134
    }
135
136
    private function getNextEntry(IterableResult $results)
137
    {
138
        try {
139
            /** @var array|false $next */
140
            $next = $results->next();
141
142
            if (false === $next) {
0 ignored issues
show
introduced by
The condition false === $next is always true.
Loading history...
143
                return false;
144
            }
145
146
            return reset($next);
147
        } catch (ConversionException $e) {
148
            preg_match('/(\+?\d+)/', $e->getMessage(), $m);
149
            $phone = $this->isRecoverable($m[0]);
150
            if ($phone instanceof PhoneNumber) {
151
                $this->write(self::INVALID_RECOVERED, $m[0], null, $phone);
152
            } else {
153
                $this->write(self::IRRECOVERABLE, $m[0]);
154
            }
155
156
            return null;
157
        }
158
    }
159
160
    private function getProgressMessage()
161
    {
162
        $counts = $this->counts;
163
164
        return sprintf(
165
            '%s valid unchanged || %s valid added 9 || %s invalid recovered || %s irrecoverable',
166
            $counts[self::VALID],
167
            $counts[self::VALID_ADDED_9],
168
            $counts[self::INVALID_RECOVERED],
169
            $counts[self::IRRECOVERABLE]
170
        );
171
    }
172
173
    private function tryToFix(PhoneNumber $phone)
174
    {
175
        $regexAreaCode = self::REGEX_AREA_CODE;
176
        $regexSubscriber = self::REGEX_SUBSCRIBER;
177
        $detectMobileRegex = "/^{$regexAreaCode}{$regexSubscriber}$/";
178
179
        if ($phone->getCountryCode() != 55 || preg_match($detectMobileRegex, $phone->getNationalNumber(), $m) !== 1) {
180
            return false;
181
        }
182
183
        $result = new PhoneNumber();
184
        $result->setCountryCode($phone->getCountryCode());
185
        $result->setNationalNumber(sprintf('%s9%s', $m[1], $m[2]));
186
187
        if (false === MobilePhoneNumberValidator::isMobile($result)) {
188
            return false;
189
        }
190
191
        return $this->phoneUtil->format($result, PhoneNumberFormat::E164);
192
    }
193
194
    private function tryToRecover($phone, $recoveredHandle)
195
    {
196
        $result = null;
197
198
        // Replace 0 by +55
199
        $regex0to55 = '/^[+]?0(1[1-9]|2[12478]|3[1-5]|3[7-8]|4[1-9]|5[1345]|6[1-9]|7[134579]|8[1-9]|9[1-9])([0-9]{8,9})$/';
200
        if (preg_match($regex0to55, $phone, $m)) {
201
            $result = "+55{$m[1]}{$m[2]}";
202
        }
203
204
        // Missing +55
205
        $missing55 = '/^(1[1-9]|2[12478]|3[1-5]|3[7-8]|4[1-9]|5[1345]|6[1-9]|7[134579]|8[1-9]|9[1-9])([0-9]{8,9})$/';
206
        if (preg_match($missing55, $phone, $m)) {
207
            $result = "+55{$m[1]}{$m[2]}";
208
        }
209
210
        if (null !== $result) {
211
            fputcsv($recoveredHandle, [$phone, $result]);
212
            $this->recovered++;
213
        }
214
215
        return $result;
216
    }
217
218
    private function isRecoverable($phone)
219
    {
220
        $phoneE164 = $this->phoneNumberToString($phone);
221
222
        $regexCountry = self::REGEX_COUNTRY;
223
        $regexAreaCode = self::REGEX_AREA_CODE;
224
        $regexSubscriber = self::REGEX_SUBSCRIBER;
225
226
        $regex = "/^{$regexCountry}{$regexAreaCode}{$regexSubscriber}$/";
227
228
        if (preg_match($regex, $phoneE164, $m) !== 1) {
229
            return false;
230
        }
231
232
        $country = $m[1];
233
        $area = $m[2];
234
        $subscriber = $m[3];
235
236
        if ($country == '0' || $country == '+0' || $country == '+55') {
237
            $country = '55';
238
        }
239
240
        if (strlen($subscriber) == 8) {
241
            $subscriber = '9'.$subscriber;
242
        }
243
244
        $phone = new PhoneNumber();
245
        $phone->setCountryCode($country);
246
        $phone->setNationalNumber($area.$subscriber);
247
248
        return $phone;
249
    }
250
251
    private function getQuery()
252
    {
253
        /** @var PersonRepository $repo */
254
        $repo = $this->getManager()->getRepository('LoginCidadaoCoreBundle:Person');
255
256
        $query = $repo->createQueryBuilder('p')
257
            ->select('p.id, p.mobile')
258
            ->where('p.mobile IS NOT NULL')
259
            ->andWhere("p.mobile != ''")
260
            ->getQuery();
261
262
        return $query;
263
    }
264
265
    /**
266
     * @param $situation
267
     * @param string $original
268
     * @param int $id
269
     * @param string $new
270
     */
271
    private function write($situation, $original, $id = null, $new = null)
272
    {
273
        $data = [
274
            $situation,
275
            $id,
276
            $this->phoneNumberToString($original),
277
            $this->phoneNumberToString($new),
278
        ];
279
        fputcsv($this->fileHandles['log'], $data);
280
        $this->counts[$situation]++;
281
    }
282
283
    private function phoneNumberToString($phone)
284
    {
285
        if ($phone instanceof PhoneNumber) {
286
            return $this->phoneUtil->format($phone, PhoneNumberFormat::E164);
287
        } else {
288
            return $phone;
289
        }
290
    }
291
}
292