Completed
Push — master ( 886ffb...8e4bc0 )
by Louis
107:54 queued 52:50
created

PhotoUpdateCommand::compareStr()   B

Complexity

Conditions 10
Paths 15

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 7.6666
c 0
b 0
f 0
cc 10
nc 15
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace KI\UserBundle\Command;
4
5
use KI\UserBundle\Entity\User;
6
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7
use Symfony\Component\Console\Input\InputArgument;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Output\OutputInterface;
11
use Symfony\Component\Console\Question\ConfirmationQuestion;
12
use Symfony\Component\Serializer\Serializer;
13
use Symfony\Component\Serializer\Encoder\CsvEncoder;
14
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
15
16
class PhotoUpdateCommand extends ContainerAwareCommand
17
{
18
    protected $FACEBOOK_API_URL = 'https://graph.facebook.com/v2.10';
19
20
    protected function configure()
21
    {
22
        $this
23
            ->setName('upont:update:photo')
24
            ->setDescription('Import missing photos from Facebook for the given promo')
25
            ->addArgument('promo', InputArgument::REQUIRED, 'The promo whose photos are to be updated.')
26
            ->addArgument('file', InputArgument::REQUIRED, 'Absolute path to a csv containing facebook_name,facebook_id')
27
            ->addOption('preview', 'p', InputOption::VALUE_NONE, 'Make a preview of the photos to be imported without importing them')
28
            ->addOption('all', 'a', InputOption::VALUE_NONE, 'Treat the users regardless whether they already have a photo on uPont')
29
            ->addOption('interactive', 'i', InputOption::VALUE_NONE, 'For each match, ask interactively whether the photo should be updated')
30
            ->addOption('similarity-threshold', 's', InputOption::VALUE_REQUIRED, 'Similarity threshold with Fb profiles above which photos are imported in non-preview and non-interactive mode (arbitrary unit, 200 by default)', 200);
31
    }
32
33
    protected function execute(InputInterface $input, OutputInterface $output)
34
    {
35
        $em = $this->getContainer()->get('doctrine')->getManager();
36
        $usersRepo = $this->getContainer()->get('doctrine')->getRepository(User::class);
37
        $curlService = $this->getContainer()->get('ki_core.service.curl');
38
        $imageService = $this->getContainer()->get('ki_core.service.image');
39
        $questionHelper = $this->getHelper('question');
40
        $isPreview = $input->getOption('preview');
41
        $users = $usersRepo->findByPromo($input->getArgument('promo'));
42
        $question = new ConfirmationQuestion('Update? ', false, '/^y/i');
43
        $serializer = new Serializer([new ObjectNormalizer()], [new CsvEncoder()]);
44
        $csvData = $serializer->decode(file_get_contents($input->getArgument('file')), 'csv');
45
        $noPhotoCount = 0;
46
        $notFoundCount = 0;
47
        $updatedNoPhotoCount = 0;
48
        $updatedExistingPhotoCount = 0;
49
        $similarityThreshold = $input->getOption("similarity-threshold");
50
        $output->writeln('Importing facebook photos for users (> ' . $similarityThreshold . ' similarity score) :');
51
        foreach ($users as $user) {
52
            $noPhoto = $user->imageUrl() === 'uploads/others/default-user.png';
53
            if (!$noPhoto) {
54
                $noPhotoCount++;
55
            }
56
            if ($noPhoto || $input->getOption('all')) {
57
                // Find best match
58
                $bestMatch = null;
59
                $bestPercent = -1;
60 View Code Duplication
                foreach ($csvData as $member) {
61
                    $percent = $this->isSimilar($user, $member);
62
                    if ($percent > $bestPercent) {
63
                        $bestPercent = $percent;
64
                        $bestMatch = $member;
65
                    }
66
                }
67
                if ($bestPercent > $similarityThreshold) {
68
                    $userFullName = $user->getFirstName() . ' ' . $user->getLastName();
69
                    $pictureInfo = $noPhoto ? '[Picture MISSING]' : '[Picture exists]';
70
                    $output->writeln($userFullName . ' <- ' . $bestMatch['name'] . ' (' . $bestPercent . '% similar) ' . $pictureInfo);
71
                    $updateConfirmation = $input->getOption('interactive') ? $questionHelper->ask($input, $output, $question) : true;
72
                    if ($updateConfirmation) {
73
                        if (!$noPhoto) {
74
                            $updatedExistingPhotoCount++;
75
                        } else {
76
                            $updatedNoPhotoCount++;
77
                        }
78
                        if (!$isPreview) {
79
                            $url = '/' . $bestMatch['id'] . '/picture?width=9999&redirect=false';
80
                            $dataImage = json_decode($curlService->curl($this->FACEBOOK_API_URL . $url), true);
81
                            $image = $imageService->upload($dataImage['data']['url'], true);
82
                            $user->setImage($image);
83
                        }
84
                    }
85
                } else {
86
                    $notFoundCount++;
87
                }
88
                $em->flush();
89
            }
90
        }
91
        $output->writeln([
92
            'End of list',
93
            '',
94
            'Students in promo ' . $input->getArgument('promo') . ' : ' . count($users)
95
        ]);
96
        if ($input->getOption('all')) {
97
            $output->writeln([
98
                'Imported missing photos: ' . $updatedNoPhotoCount,
99
                'Not found photos: ' . $notFoundCount,
100
                'Replaced photos: ' . $updatedExistingPhotoCount,
101
            ]);
102
        } else {
103
            $output->writeln([
104
                'Missing photos in promo : ' . $noPhotoCount,
105
                'Imported missing photos: ' . $updatedNoPhotoCount,
106
                'Not found photos: ' . $notFoundCount,
107
                'Remaining missing photos: ' . ($noPhotoCount - $updatedNoPhotoCount),
108
            ]);
109
        }
110
    }
111
    // Compare un User uPont et un utilisateur Facebook et essaye de deviner si
112
    // ce sont les mêmes personnes
113
    private function isSimilar(User $user, array $member)
114
    {
115
        $score = 0;
116
        $firstName = $this->cleanString($user->getFirstName());
117
        $lastName = $this->cleanString($user->getLastName());
118
119
        $firstNameFb = $this->cleanString(substr($member['name'], 0, strpos($member['name'], ' ')));
120
        $lastNameFb = $this->cleanString(substr($member['name'], strpos($member['name'], ' ') + 1));
121
122
        $score += $this->compareStr($firstName, $firstNameFb);
123
        // Le match sur le nom de famille est un facteur bien plus important
124
        // que celui pour le nom
125
        $score += 3 * $this->compareStr($lastName, $lastNameFb); //Parameter may be adapted
126
        return $score;
127
    }
128
129
    // Compare deux chaînes de caractères selon un algorithme fait maison
130
    // dans le but de détecter les personnes ayant un nom différent sur Facebook
131
    // Ex: Vanlaer -> Vnlr
132
    // Favorise le match entre des chaînes ayant mêmes caractères au début
133
    // et à la fin.
134
    // Favorise les séquences de caractères identiques, et également les
135
    // caractères qui matchent et qui sont proches les uns des autres
136
    private function compareStr(string $str1, string $str2)
137
    {
138
        $score = 0;
139
        list($shortName, $longName) = $this->compareStringLength($str1, $str2);
140
        $currentIndex = 0;
141
        $lastIndex = -1;
142
        $matchingRow = 0;
143
        for ($i = 0; $i < strlen($shortName); $i++) {
144
            while ($currentIndex < strlen($longName) && $longName[$currentIndex] != $shortName[$i]) {
145
                $currentIndex += 1;
146
                $matchingRow = 0;
147
            }
148
            if ($currentIndex < strlen($longName)) {
149
                //Parameters may be adapted
150
                if ($i == 0 && $currentIndex == 0) {
151
                    $score += 10;
152
                } elseif ($i == strlen($shortName) - 1 && $currentIndex == strlen($longName) - 1) {
153
                    $score += 10;
154
                }
155
                if ($lastIndex > -1) {
156
                    $score += 5 * exp(-($currentIndex - $lastIndex) / 2);
157
                }
158
                $lastIndex = $currentIndex;
159
                $matchingRow += 1;
160
                $score += exp($matchingRow / 2);
161
                $currentIndex += 1;
162
            }
163
        }
164
        return $score;
165
    }
166
167
    //Compare 2 string et renvoie la plus petite et la plus grande
168
    private function compareStringLength(string $str1, string $str2)
169
    {
170
        if (strlen($str1) > strlen($str2)) {
171
            return [$str2, $str1];
172
        } else {
173
            return [$str1, $str2];
174
        }
175
    }
176
177
    // Nettoie une chaine de caracteres:
178
    // -enlève d'eventuels accents
179
    // -met tout en minuscule
180
    private function cleanString(string $str)
181
    {
182
        $replacePairs = [
183
            'Š' => 'S',
184
            'š' => 's',
185
            'Ž' => 'Z', 'ž' => 'z',
186
            'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'A',
187
            'Ç' => 'C',
188
            'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
189
            'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
190
            'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ø' => 'O', 'Ù' => 'U',
191
            'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y', 'Þ' => 'B', 'ß' => 'Ss',
192
            'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'a',
193
            'ç' => 'c',
194
            'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
195
            'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
196
            'ð' => 'o', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o',
197
            'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ý' => 'y', 'þ' => 'b', 'ÿ' => 'y'
198
        ];
199
        $str = strtr($str, $replacePairs);
200
        return strtolower($str);
201
    }
202
}
203