Failed Conditions
Push — issue#764 ( 39afc8 )
by Guilherme
17:45 queued 08:27
created

MobileCleanupCommand   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 257
ccs 0
cts 178
cp 0
rs 9.3999
c 0
b 0
f 0
wmc 33

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getQuery() 0 12 1
A getManager() 0 3 1
A write() 0 10 1
A tryToFix() 0 19 4
A execute() 0 15 1
B tryToRecover() 0 22 4
C processPhones() 0 43 7
A phoneNumberToString() 0 6 2
A configure() 0 10 1
B isRecoverable() 0 31 6
A getProgressMessage() 0 10 1
A getNextEntry() 0 20 4
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\AbstractQuery;
15
use Doctrine\ORM\Internal\Hydration\IterableResult;
16
use libphonenumber\NumberFormat;
17
use libphonenumber\PhoneNumber;
18
use libphonenumber\PhoneNumberFormat;
19
use libphonenumber\PhoneNumberType;
20
use libphonenumber\PhoneNumberUtil;
21
use LoginCidadao\CoreBundle\Entity\Person;
22
use LoginCidadao\CoreBundle\Entity\PersonRepository;
23
use LoginCidadao\ValidationBundle\Validator\Constraints\MobilePhoneNumberValidator;
24
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
25
use Symfony\Component\Console\Helper\ProgressIndicator;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Output\OutputInterface;
28
use Symfony\Component\Console\Output\BufferedOutput;
29
use Symfony\Component\Console\Input\InputInterface;
30
use Symfony\Component\Console\Input\InputOption;
31
use Symfony\Component\Console\Input\ArrayInput;
32
use Symfony\Component\Console\Style\SymfonyStyle;
33
use Doctrine\ORM\EntityManager;
34
35
class MobileCleanupCommand extends ContainerAwareCommand
36
{
37
    const INVALID_RECOVERED = 'invalid_recovered';
38
    const IRRECOVERABLE = 'irrecoverable';
39
    const VALID = 'valid';
40
    const VALID_ADDED_9 = 'valid_added_9';
41
42
    const REGEX_COUNTRY = '(\+?0|\+?55)?';
43
    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])';
44
    const REGEX_SUBSCRIBER = '((?:9[6789]|[6789])\d{7})';
45
46
    /** @var PhoneNumberUtil */
47
    private $phoneUtil;
48
49
    /** @var array */
50
    private $fileHandles = [];
51
52
    /** @var array */
53
    private $counts = [];
54
55
    private $recovered = 0;
56
57
    protected function configure()
58
    {
59
        $this
60
            ->setName('lc:mobile-cleanup')
61
            ->addArgument(
62
                'file',
63
                InputArgument::REQUIRED,
64
                'File to where decisions should be logged.'
65
            )
66
            ->setDescription('Erases mobile numbers that do not comply with E.164 in any recoverable way.');
67
    }
68
69
    protected function execute(InputInterface $input, OutputInterface $output)
70
    {
71
        $file = $input->getArgument('file');
72
        $this->phoneUtil = PhoneNumberUtil::getInstance();
73
74
        $this->fileHandles['log'] = fopen($file, 'w+');
75
76
        $this->counts = [
77
            self::INVALID_RECOVERED => 0,
78
            self::IRRECOVERABLE => 0,
79
            self::VALID => 0,
80
            self::VALID_ADDED_9 => 0,
81
        ];
82
83
        $this->processPhones($input, $output);
84
    }
85
86
    /**
87
     *
88
     * @return EntityManager
89
     */
90
    private function getManager()
91
    {
92
        return $this->getContainer()->get('doctrine')->getManager();
93
    }
94
95
    private function processPhones(InputInterface $input, OutputInterface $output)
96
    {
97
        $file = $input->getArgument('file');
98
99
        $io = new SymfonyStyle($input, $output);
100
101
        $io->title('Mobile Numbers Cleanup');
102
103
        $progress = new ProgressIndicator($output);
104
        $progress->start('Scanning phones...');
105
106
        $results = $this->getQuery()->iterate();
107
        while (true) {
108
            $progress->setMessage($this->getProgressMessage());
109
            $row = $this->getNextEntry($results);
110
            if (false === $row) {
111
                break;
112
            }
113
            if (null === $row) {
114
                continue;
115
            }
116
117
            /** @var PhoneNumber $mobile */
118
            $mobile = $row['mobile'];
119
            if (false === MobilePhoneNumberValidator::isMobile($mobile)) {
120
                $added9 = $this->tryToFix($mobile);
121
                if (false === $added9) {
122
                    $this->write(self::IRRECOVERABLE, $mobile, $row['id']);
123
                } else {
124
                    $this->write(self::VALID_ADDED_9, $mobile, $row['id'], $added9);
125
                }
126
            } else {
127
                $this->write(self::VALID, $mobile, $row['id']);
128
            }
129
        }
130
        $progress->finish($this->getProgressMessage());
131
        $io->newLine();
132
133
        foreach ($this->fileHandles as $handle) {
134
            fclose($handle);
135
        }
136
137
        $io->success("Results saved to {$file}");
138
    }
139
140
    private function getNextEntry(IterableResult $results)
141
    {
142
        try {
143
            $next = $results->next();
144
145
            if (false === $next) {
0 ignored issues
show
introduced by
The condition false === $next is always false.
Loading history...
146
                return false;
147
            }
148
149
            return reset($next);
150
        } catch (ConversionException $e) {
151
            preg_match('/(\+?\d+)/', $e->getMessage(), $m);
152
            $phone = $this->isRecoverable($m[0]);
153
            if ($phone instanceof PhoneNumber) {
154
                $this->write(self::INVALID_RECOVERED, $m[0], null, $phone);
155
            } else {
156
                $this->write(self::IRRECOVERABLE, $m[0]);
157
            }
158
159
            return null;
160
        }
161
    }
162
163
    private function getProgressMessage()
164
    {
165
        $counts = $this->counts;
166
167
        return sprintf(
168
            '%s valid unchanged || %s valid added 9 || %s invalid recovered || %s irrecoverable',
169
            $counts[self::VALID],
170
            $counts[self::VALID_ADDED_9],
171
            $counts[self::INVALID_RECOVERED],
172
            $counts[self::IRRECOVERABLE]
173
        );
174
    }
175
176
    private function tryToFix(PhoneNumber $phone)
177
    {
178
        $regexAreaCode = self::REGEX_AREA_CODE;
179
        $regexSubscriber = self::REGEX_SUBSCRIBER;
180
        $detectMobileRegex = "/^{$regexAreaCode}{$regexSubscriber}$/";
181
182
        if ($phone->getCountryCode() != 55 || preg_match($detectMobileRegex, $phone->getNationalNumber(), $m) !== 1) {
183
            return false;
184
        }
185
186
        $result = new PhoneNumber();
187
        $result->setCountryCode($phone->getCountryCode());
188
        $result->setNationalNumber(sprintf('%s9%s', $m[1], $m[2]));
189
190
        if (false === MobilePhoneNumberValidator::isMobile($result)) {
191
            return false;
192
        }
193
194
        return $this->phoneUtil->format($result, PhoneNumberFormat::E164);
195
    }
196
197
    private function tryToRecover($phone, $recoveredHandle)
198
    {
199
        $result = null;
200
201
        // Replace 0 by +55
202
        $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})$/';
203
        if (preg_match($regex0to55, $phone, $m)) {
204
            $result = "+55{$m[1]}{$m[2]}";
205
        }
206
207
        // Missing +55
208
        $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})$/';
209
        if (preg_match($missing55, $phone, $m)) {
210
            $result = "+55{$m[1]}{$m[2]}";
211
        }
212
213
        if (null !== $result) {
214
            fputcsv($recoveredHandle, [$phone, $result]);
215
            $this->recovered++;
216
        }
217
218
        return $result;
219
    }
220
221
    private function isRecoverable($phone)
222
    {
223
        $phoneE164 = $this->phoneNumberToString($phone);
224
225
        $regexCountry = self::REGEX_COUNTRY;
226
        $regexAreaCode = self::REGEX_AREA_CODE;
227
        $regexSubscriber = self::REGEX_SUBSCRIBER;
228
229
        $regex = "/^{$regexCountry}{$regexAreaCode}{$regexSubscriber}$/";
230
231
        if (preg_match($regex, $phoneE164, $m) !== 1) {
232
            return false;
233
        }
234
235
        $country = $m[1];
236
        $area = $m[2];
237
        $subscriber = $m[3];
238
239
        if ($country == '0' || $country == '+0' || $country == '+55') {
240
            $country = '55';
241
        }
242
243
        if (strlen($subscriber) == 8) {
244
            $subscriber = '9'.$subscriber;
245
        }
246
247
        $phone = new PhoneNumber();
248
        $phone->setCountryCode($country);
249
        $phone->setNationalNumber($area.$subscriber);
250
251
        return $phone;
252
    }
253
254
    private function getQuery()
255
    {
256
        /** @var PersonRepository $repo */
257
        $repo = $this->getManager()->getRepository('LoginCidadaoCoreBundle:Person');
258
259
        $query = $repo->createQueryBuilder('p')
260
            ->select('p.id, p.mobile')
261
            ->where('p.mobile IS NOT NULL')
262
            ->andWhere("p.mobile != ''")
263
            ->getQuery();
264
265
        return $query;
266
    }
267
268
    /**
269
     * @param $situation
270
     * @param string $original
271
     * @param int $id
272
     * @param string $new
273
     */
274
    private function write($situation, $original, $id = null, $new = null)
275
    {
276
        $data = [
277
            $situation,
278
            $id,
279
            $this->phoneNumberToString($original),
280
            $this->phoneNumberToString($new),
281
        ];
282
        fputcsv($this->fileHandles['log'], $data);
283
        $this->counts[$situation]++;
284
    }
285
286
    private function phoneNumberToString($phone)
287
    {
288
        if ($phone instanceof PhoneNumber) {
289
            return $this->phoneUtil->format($phone, PhoneNumberFormat::E164);
290
        } else {
291
            return $phone;
292
        }
293
    }
294
}
295