Loco::makeApiRequest()   B
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 0
cts 16
cp 0
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 13
nc 5
nop 6
crap 30
1
<?php
2
3
namespace Happyr\TranslationBundle\Service;
4
5
use Happyr\TranslationBundle\Exception\HappyrTranslationException;
6
use Happyr\TranslationBundle\Exception\HttpException;
7
use Happyr\TranslationBundle\Http\RequestManager;
8
use Happyr\TranslationBundle\Model\Message;
9
use Happyr\TranslationBundle\Translation\FilesystemUpdater;
10
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
11
use Symfony\Component\Translation\TranslatorInterface;
12
13
/**
14
 * @author Tobias Nyholm <[email protected]>
15
 */
16
class Loco implements TranslationServiceInterface
17
{
18
    const BASE_URL = 'https://localise.biz/api/';
19
20
    /**
21
     * @var RequestManager
22
     */
23
    private $requestManager;
24
25
    /**
26
     * @var array projects
27
     */
28
    private $projects;
29
30
    /**
31
     * @var FilesystemUpdater filesystemService
32
     */
33
    private $filesystemService;
34
35
    /**
36
     * @var TranslatorInterface filesystemService
37
     */
38
    private $translator;
39
40
    /**
41
     * @param TranslatorInterface $translator
42
     * @param RequestManager $requestManager
43
     * @param FilesystemUpdater $fs
44
     * @param array $projects
45
     */
46 1
    public function __construct(RequestManager $requestManager, FilesystemUpdater $fs, TranslatorInterface $translator, array $projects)
47
    {
48 1
        $this->translator = $translator;
49 1
        $this->requestManager = $requestManager;
50 1
        $this->projects = $projects;
51 1
        $this->filesystemService = $fs;
52 1
    }
53
54
    /**
55
     * @param $key
56
     * @param $method
57
     * @param $resource
58
     * @param null $body
59
     * @param string $type
60
     * @param array $extraQuery
61
     * @return array
62
     * @throws HttpException
63
     */
64
    protected function makeApiRequest($key, $method, $resource, $body = null, $type = 'form', $extraQuery = array())
65
    {
66
        $headers = array();
67
        if ($body !== null) {
68
            if ($type === 'form') {
69
                if (is_array($body)) {
70
                    $body = http_build_query($body);
71
                }
72
                $headers['Content-Type'] = 'application/x-www-form-urlencoded';
73
            } elseif ($type === 'json') {
74
                $body = json_encode($body);
75
                $headers['Content-Type'] = 'application/json';
76
            }
77
        }
78
79
        $query = array_merge($extraQuery, ['key' => $key]);
80
        $url = self::BASE_URL . $resource . '?' . http_build_query($query);
81
82
        return $this->requestManager->send($method, $url, $body, $headers);
83
    }
84
85
    /**
86
     * Fetch a translation form Loco.
87
     *
88
     * @param Message $message
89
     */
90
    public function fetchTranslation(Message $message, $updateFs = false)
91
    {
92
        $project = $this->getProject($message);
93
94
        try {
95
            $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale());
96
            $response = $this->makeApiRequest($project['api_key'], 'GET', $resource);
97
        } catch (HttpException $e) {
98
            if ($e->getCode() === 404) {
99
                //Message does not exist
100
                return;
101
            }
102
            throw $e;
103
        }
104
105
        $logoTranslation = $response['translation'];
106
        $messageTranslation = $message->getTranslation();
107
        $message->setTranslation($logoTranslation);
108
109
        // update filesystem
110
        if ($updateFs && $logoTranslation !== $messageTranslation) {
111
            $this->filesystemService->updateMessageCatalog([$message]);
112
        }
113
114
        return $logoTranslation;
115
    }
116
117
    /**
118
     * Update the translation in Loco.
119
     *
120
     * @param Message $message
121
     */
122
    public function updateTranslation(Message $message)
123
    {
124
        $project = $this->getProject($message);
125
126
        try {
127
            $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale());
128
            $this->makeApiRequest($project['api_key'], 'POST', $resource, $message->getTranslation());
129
        } catch (HttpException $e) {
130
            if ($e->getCode() === 404) {
131
                //Asset does not exist
132
                if ($this->createAsset($message)) {
133
                    //Try again
134
                    return $this->updateTranslation($message);
135
                }
136
137
                return false;
138
            }
139
            throw $e;
140
        }
141
142
        $this->filesystemService->updateMessageCatalog([$message]);
143
144
        return true;
145
    }
146
147
    /**
148
     * If there is something wrong with the translation, please flag it.
149
     *
150
     * @param Message $message
151
     * @param int $type 0: Fuzzy, 1: Incorrect, 2: Provisional, 3: Unapproved, 4: Incomplete
152
     *
153
     * @return bool
154
     */
155
    public function flagTranslation(Message $message, $type = 0)
156
    {
157
        $project = $this->getProject($message);
158
        $flags = ['fuzzy', 'incorrect', 'provisional', 'unapproved', 'incomplete'];
159
160
        try {
161
            $resource = sprintf('translations/%s/%s/flag', $message->getId(), $message->getLocale());
162
            $this->makeApiRequest($project['api_key'], 'POST', $resource, ['flag' => $flags[$type]]);
163
        } catch (HttpException $e) {
164
            if ($e->getCode() === 404) {
165
                //Message does not exist
166
                return false;
167
            }
168
            throw $e;
169
        }
170
171
        return true;
172
    }
173
174
    /**
175
     * Create a new asset in Loco.
176
     *
177
     * @param Message $message
178
     *
179
     * @return bool
180
     */
181
    public function createAsset(Message $message)
182
    {
183
        $project = $this->getProject($message);
184
185
        try {
186
            $response = $this->makeApiRequest($project['api_key'], 'POST', 'assets', [
187
                'id' => $message->getId(),
188
                'name' => $message->getId(),
189
                'type' => 'text',
190
                // Tell Loco not to translate the asset
191
                'default' => 'untranslated',
192
            ]);
193
194
            if ($message->hasParameters()) {
195
                // Send those parameter as a note to Loco
196
                $notes = '';
197
                foreach ($message->getParameters() as $key => $value) {
198
                    if (!is_array($value)) {
199
                        $notes .= 'Parameter: ' . $key . ' (i.e. : ' . $value . ")\n";
200
                    } else {
201
                        foreach ($value as $k => $v) {
202
                            $notes .= 'Parameter: ' . $k . ' (i.e. : ' . $v . ")\n";
203
                        }
204
                    }
205
                }
206
207
                $resource = sprintf('assets/%s.json', $message->getId());
208
                $this->makeApiRequest($project['api_key'], 'PATCH', $resource, ['notes' => $notes], 'json');
209
            }
210
        } catch (HttpException $e) {
211
            if ($e->getCode() === 409) {
212
                //conflict.. ignore
213
                return false;
214
            }
215
            throw $e;
216
        }
217
218
        // if this project has multiple domains. Make sure to tag it
219
        if (!empty($project['domains'])) {
220
            $this->addTagToAsset($project, $response['id'], $message->getDomain());
221
        }
222
223
        return true;
224
    }
225
226
    /**
227
     * @param Message $message
228
     *
229
     * @return array
230
     */
231
    protected function getProject(Message $message)
232
    {
233
        if (isset($this->projects[$message->getDomain()])) {
234
            return $this->projects[$message->getDomain()];
235
        }
236
237
        // Return the first project that has the correct domain and locale
238
        foreach ($this->projects as $project) {
239
            if (in_array($message->getDomain(), $project['domains'])) {
240
                if (in_array($message->getLocale(), $project['locales'])) {
241
                    return $project;
242
                }
243
            }
244
        }
245
    }
246
247
    /**
248
     * @param $project
249
     * @param $messageId
250
     * @param $domain
251
     */
252
    protected function addTagToAsset($project, $messageId, $domain)
253
    {
254
        $resource = sprintf('assets/%s/tags', $messageId);
255
        $this->makeApiRequest($project['api_key'], 'POST', $resource, ['name' => $domain]);
256
    }
257
258
    /**
259
     * Download all the translations from Loco. This will replace all the local files.
260
     * This is a quick method of getting all the latest translations and assets.
261
     */
262
    public function downloadAllTranslations()
263
    {
264
        $data = [];
265
        foreach ($this->projects as $name => $config) {
266
            if (empty($config['domains'])) {
267
                $this->getUrls($data, $config, $name, false);
268
            } else {
269
                foreach ($config['domains'] as $domain) {
270
                    $this->getUrls($data, $config, $domain, true);
271
                }
272
            }
273
        }
274
        $this->requestManager->downloadFiles($this->filesystemService, $data);
275
    }
276
277
    /**
278
     * Upload all the translations from the symfony project into Loco. This will override
279
     * every changed strings in loco
280
     */
281 View Code Duplication
    public function uploadAllTranslations()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
282
    {
283
        foreach ($this->projects as $name => $config) {
284
            if (empty($config['domains'])) {
285
                $this->doUploadDomains($config, $name, false);
286
            } else {
287
                foreach ($config['domains'] as $domain) {
288
                    $this->doUploadDomains($config, $domain, true);
289
                }
290
            }
291
        }
292
    }
293
294
    /**
295
     * @param array $config
296
     * @param       $domain
297
     * @param       $useDomainAsFilter
298
     */
299
    protected function doUploadDomains(array &$config, $domain, $useDomainAsFilter)
300
    {
301
        $query = $this->getExportQueryParams($config['api_key']);
302
303
        if ($useDomainAsFilter) {
304
            $query['filter'] = $domain;
305
        }
306
307
        foreach ($config['locales'] as $locale) {
308
            $extension = $this->filesystemService->getFileExtension();
309
            $file = $this->filesystemService->getTargetDir();
310
            $file .= sprintf('/%s.%s.%s', $domain, $locale, $extension);
311
312
            if (is_file($file)) {
313
                $query = [
314
                    'index' => 'id',
315
                    'tag' => $domain,
316
                    'locale'=> $locale
317
                ];
318
319
                $resource = sprintf('import/%s', $extension);
320
                $response = $this->makeApiRequest($config['api_key'], 'POST', $resource, file_get_contents($file), 'form', $query);
321
                $this->flatten($response);
322
            } else {
323
                throw new FileNotFoundException(sprintf("Can't find %s file, perhaps you should generate the translations file ?", $file));
324
            }
325
        }
326
    }
327
328
    /**
329
     * Synchronize all the translations with Loco. This will keep placeholders. This function is slower
330
     * than just to download the translations.
331
     */
332 View Code Duplication
    public function synchronizeAllTranslations()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333
    {
334
        foreach ($this->projects as $name => $config) {
335
            if (empty($config['domains'])) {
336
                $this->doSynchronizeDomain($config, $name, false);
337
            } else {
338
                foreach ($config['domains'] as $domain) {
339
                    $this->doSynchronizeDomain($config, $domain, true);
340
                }
341
            }
342
        }
343
    }
344
345
    /**
346
     * @param array $config
347
     * @param       $domain
348
     * @param       $useDomainAsFilter
349
     */
350
    protected function doSynchronizeDomain(array &$config, $domain, $useDomainAsFilter)
351
    {
352
        $query = $this->getExportQueryParams($config['api_key']);
353
354
        if ($useDomainAsFilter) {
355
            $query['filter'] = $domain;
356
        }
357
358
        foreach ($config['locales'] as $locale) {
359
            $resource = sprintf('export/locale/%s.%s', $locale, 'json');
360
            $response = $this->makeApiRequest($config['api_key'], 'GET', $resource, ['query' => $query]);
361
362
            $this->flatten($response);
363
364
            $messages = array();
365
            foreach ($response as $id => $translation) {
366
                $messages[] = new Message([
367
                    'count' => 1,
368
                    'domain' => $domain,
369
                    'id' => $id,
370
                    'locale' => $locale,
371
                    'state' => 1,
372
                    'translation' => $translation,
373
                ]);
374
            }
375
376
            $this->filesystemService->updateMessageCatalog($messages);
377
        }
378
    }
379
380
    /**
381
     * Flattens an nested array of translations.
382
     *
383
     * The scheme used is:
384
     *   'key' => array('key2' => array('key3' => 'value'))
385
     * Becomes:
386
     *   'key.key2.key3' => 'value'
387
     *
388
     * This function takes an array by reference and will modify it
389
     *
390
     * @param array &$messages The array that will be flattened
391
     * @param array $subnode Current subnode being parsed, used internally for recursive calls
392
     * @param string $path Current path being parsed, used internally for recursive calls
393
     */
394
    private function flatten(array &$messages, array $subnode = null, $path = null)
395
    {
396
        if (null === $subnode) {
397
            $subnode = &$messages;
398
        }
399
        foreach ($subnode as $key => $value) {
400
            if (is_array($value)) {
401
                $nodePath = $path ? $path . '.' . $key : $key;
402
                $this->flatten($messages, $value, $nodePath);
403
                if (null === $path) {
404
                    unset($messages[$key]);
405
                }
406
            } elseif (null !== $path) {
407
                $messages[$path . '.' . $key] = $value;
408
            }
409
        }
410
    }
411
412
    /**
413
     * @param array $data
414
     * @param array $config
415
     * @param string $domain
416
     * @param bool $useDomainAsFilter
417
     */
418
    protected function getUrls(array &$data, array $config, $domain, $useDomainAsFilter)
419
    {
420
        $query = $this->getExportQueryParams($config['api_key']);
421
422
        if ($useDomainAsFilter) {
423
            $query['filter'] = $domain;
424
        }
425
426
        foreach ($config['locales'] as $locale) {
427
            // Build url
428
            $url = sprintf('%sexport/locale/%s.%s?%s', self::BASE_URL, $locale, $this->filesystemService->getFileExtension(), http_build_query($query));
429
            $fileName = sprintf('%s.%s.%s', $domain, $locale, $this->filesystemService->getFileExtension());
430
431
            $data[$url] = $fileName;
432
        }
433
    }
434
435
    /**
436
     * @param array $config
0 ignored issues
show
Bug introduced by
There is no parameter named $config. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
437
     *
438
     * @return array
439
     */
440
    private function getExportQueryParams($key)
441
    {
442
        $data = array(
443
            'index' => 'id',
444
            'status' => 'translated',
445
            'key' => $key,
446
        );
447
        switch ($this->filesystemService->getFileExtension()) {
448
            case 'php':
449
                $data['format'] = 'zend'; // 'Zend' will give us a flat array
450
                break;
451
            case 'xlf':
452
            default:
453
                $data['format'] = 'symfony';
454
        }
455
456
        return $data;
457
    }
458
}
459