Failed Conditions
Push — issue#702 ( 91bd46...0b5bf0 )
by Guilherme
19:37 queued 12:15
created

MobileCleanupCommand::write()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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